diff options
author | Kim <1877318+kimsible@users.noreply.github.com> | 2020-04-28 14:53:43 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2020-04-28 14:53:43 +0200 |
commit | b15fe00f7409b27573e162192530bc73e3f918b1 (patch) | |
tree | a71df67cee37a60f4de573ca9347aa3262cd8463 /client/src/app/shared | |
parent | 4682468d4d07e0864155dd2b403d93754786ea13 (diff) | |
download | PeerTube-b15fe00f7409b27573e162192530bc73e3f918b1.tar.gz PeerTube-b15fe00f7409b27573e162192530bc73e3f918b1.tar.zst PeerTube-b15fe00f7409b27573e162192530bc73e3f918b1.zip |
Add maximized mode to markdown-textarea + CSS improvements (#2660)
* Add arrows-angle-contract/expand bootstrap icons
* Add grey textarea-background-color
* Add maximized support to markdown-textarea + improve column display
* Refactor CSS + add ResizeObservable
* Replace bootstrap icons with softies
* Add ResizeObserver typing definition
* Add focus on textarea + Fix Observables
* Propage component changes on markdown plugins
* Ignore ResizeObserver not implemented in typescript yet
* Move observers from constructor to click event
* Add scss and css variables
* Replace textareaWidth with textareaMaxWidth to fix others textareas
* Clean unused css rules
* Fix ResizeObserver unknown by TypeScript compiler
* Set max-width: 100% for small and mobile views
* Fix textarea/preview height on maximized mode
* Add common padding textarea/preview side-by-side
* Hide scrollbar sub-menu on small-views
* Add maximized mode for mobile views
* Fix sass calculate syntax
* Revert custom CSS variable for inputBorderRadius and inputBorderColor
* Remove unsued methods
* Fix missing implement method
Co-authored-by: kimsible <kimsible@users.noreply.github.com>
Diffstat (limited to 'client/src/app/shared')
4 files changed, 269 insertions, 32 deletions
diff --git a/client/src/app/shared/forms/markdown-textarea.component.html b/client/src/app/shared/forms/markdown-textarea.component.html index 3cadb3619..a519f3e0a 100644 --- a/client/src/app/shared/forms/markdown-textarea.component.html +++ b/client/src/app/shared/forms/markdown-textarea.component.html | |||
@@ -1,12 +1,12 @@ | |||
1 | <div class="root" [ngStyle]="{ 'flex-direction': flexDirection }"> | 1 | <div class="root" [ngClass]="{ 'maximized': isMaximized }" [ngStyle]="{ 'max-width': textareaMaxWidth }"> |
2 | <textarea | 2 | <textarea #textarea |
3 | [(ngModel)]="content" (ngModelChange)="onModelChange()" | 3 | [(ngModel)]="content" (ngModelChange)="onModelChange()" |
4 | class="form-control" [ngClass]="classes" | 4 | class="form-control" [ngClass]="classes" |
5 | [ngStyle]="{ width: textareaWidth, height: textareaHeight, 'margin-right': textareaMarginRight }" | 5 | [ngStyle]="{ height: textareaHeight }" |
6 | [id]="name" [name]="name"> | 6 | [id]="name" [name]="name"> |
7 | </textarea> | 7 | </textarea> |
8 | 8 | ||
9 | <div ngbNav #nav="ngbNav" class="nav-pills previews"> | 9 | <div ngbNav #nav="ngbNav" class="nav-pills nav-preview"> |
10 | <ng-container ngbNavItem *ngIf="truncate !== undefined"> | 10 | <ng-container ngbNavItem *ngIf="truncate !== undefined"> |
11 | <a ngbNavLink i18n>Truncated preview</a> | 11 | <a ngbNavLink i18n>Truncated preview</a> |
12 | 12 | ||
@@ -22,6 +22,14 @@ | |||
22 | <div [innerHTML]="previewHTML"></div> | 22 | <div [innerHTML]="previewHTML"></div> |
23 | </ng-template> | 23 | </ng-template> |
24 | </ng-container> | 24 | </ng-container> |
25 | |||
26 | <my-button | ||
27 | *ngIf="!isMaximized" icon="fullscreen" (click)="onMaximizeClick()" | ||
28 | ></my-button> | ||
29 | |||
30 | <my-button | ||
31 | *ngIf="isMaximized" icon="exit-fullscreen" (click)="onMaximizeClick()" | ||
32 | ></my-button> | ||
25 | </div> | 33 | </div> |
26 | 34 | ||
27 | <div [ngbNavOutlet]="nav"></div> | 35 | <div [ngbNavOutlet]="nav"></div> |
diff --git a/client/src/app/shared/forms/markdown-textarea.component.scss b/client/src/app/shared/forms/markdown-textarea.component.scss index bd02343de..065cd2dec 100644 --- a/client/src/app/shared/forms/markdown-textarea.component.scss +++ b/client/src/app/shared/forms/markdown-textarea.component.scss | |||
@@ -1,36 +1,250 @@ | |||
1 | @import '_variables'; | 1 | @import '_variables'; |
2 | @import '_mixins'; | 2 | @import '_mixins'; |
3 | 3 | ||
4 | .root { | 4 | $nav-preview-tab-height: 30px; |
5 | display: flex; | 5 | $base-padding: 15px; |
6 | $input-border-color: #C6C6C6; | ||
7 | $input-border-radius: 3px; | ||
8 | |||
9 | @mixin in-small-view { | ||
10 | .root { | ||
11 | display: flex; | ||
12 | flex-direction: column; | ||
13 | |||
14 | textarea { | ||
15 | @include peertube-textarea(100%, 150px); | ||
16 | |||
17 | background-color: var(--textareaBackgroundColor); | ||
18 | font-family: courier, monospace; | ||
19 | font-size: 13px; | ||
20 | border-bottom: none; | ||
21 | border-bottom-left-radius: unset; | ||
22 | border-bottom-right-radius: unset; | ||
23 | } | ||
6 | 24 | ||
7 | textarea { | 25 | .nav-preview { |
8 | @include peertube-textarea(100%, 150px); | 26 | display: block; |
27 | text-align: right; | ||
28 | padding-top: 10px; | ||
29 | padding-bottom: 10px; | ||
30 | padding-left: 10px; | ||
31 | padding-right: 10px; | ||
32 | border-top: 1px dashed $input-border-color; | ||
33 | border-left: 1px solid $input-border-color; | ||
34 | border-right: 1px solid $input-border-color; | ||
35 | border-bottom: 1px solid $input-border-color; | ||
36 | border-bottom-right-radius: $input-border-radius; | ||
9 | 37 | ||
10 | margin-bottom: 15px; | 38 | border-bottom-left-radius: $input-border-radius; |
39 | ::ng-deep { | ||
40 | .nav-link { | ||
41 | display: none !important; | ||
42 | } | ||
43 | |||
44 | .grey-button { | ||
45 | padding: 0 12px 0 12px; | ||
46 | } | ||
47 | } | ||
48 | } | ||
49 | |||
50 | ::ng-deep { | ||
51 | .tab-content { | ||
52 | display: none; | ||
53 | } | ||
54 | } | ||
11 | } | 55 | } |
56 | } | ||
57 | |||
58 | @mixin nav-preview-medium { | ||
59 | display: flex; | ||
60 | flex-grow: 1; | ||
61 | border-bottom-left-radius: unset; | ||
62 | border-bottom-right-radius: unset; | ||
63 | border-bottom: 2px solid var(--mainColor); | ||
12 | 64 | ||
13 | .previews { | 65 | :first-child { |
14 | max-height: 150px; | 66 | margin-left: auto; |
15 | overflow-y: auto; | ||
16 | flex-grow: 1; | ||
17 | } | 67 | } |
18 | 68 | ||
19 | ::ng-deep { | 69 | ::ng-deep { |
20 | .nav-link { | 70 | .nav-link { |
21 | display: flex !important; | 71 | display: flex !important; |
22 | align-items: center; | 72 | align-items: center; |
23 | height: 30px !important; | 73 | height: $nav-preview-tab-height !important; |
24 | padding: 0 15px !important; | 74 | padding: 0 15px !important; |
25 | font-size: 85% !important; | 75 | font-size: 85% !important; |
26 | opacity: .7; | 76 | opacity: .7; |
27 | } | 77 | } |
28 | 78 | ||
29 | .tab-content { | 79 | .grey-button { |
30 | min-height: 75px; | 80 | margin-left: 5px; |
31 | padding: 15px; | 81 | } |
32 | font-size: 15px; | 82 | } |
33 | word-wrap: break-word; | 83 | } |
84 | |||
85 | @mixin content-preview-base { | ||
86 | display: block; | ||
87 | min-height: 75px; | ||
88 | padding: $base-padding; | ||
89 | overflow-y: auto; | ||
90 | font-size: 15px; | ||
91 | word-wrap: break-word; | ||
92 | } | ||
93 | |||
94 | @mixin maximized-base { | ||
95 | flex-direction: row; | ||
96 | z-index: #{z(header) - 1}; | ||
97 | position: fixed; | ||
98 | top: $header-height; | ||
99 | left: $menu-width; | ||
100 | max-height: none !important; | ||
101 | max-width: none !important; | ||
102 | width: calc(100% - #{$menu-width}); | ||
103 | height: calc(100vh - #{$header-height}) !important; | ||
104 | |||
105 | $nav-preview-vertical-padding: 40px; | ||
106 | |||
107 | .nav-preview { | ||
108 | @include nav-preview-medium(); | ||
109 | padding-top: #{$nav-preview-vertical-padding / 2}; | ||
110 | padding-bottom: #{$nav-preview-vertical-padding / 2}; | ||
111 | padding-left: 0px; | ||
112 | padding-right: 0px; | ||
113 | position: absolute; | ||
114 | background-color: var(--mainBackgroundColor); | ||
115 | width: 100% !important; | ||
116 | border-top: none; | ||
117 | border-left: none; | ||
118 | border-right: none; | ||
119 | |||
120 | :last-child { | ||
121 | margin-right: $not-expanded-horizontal-margins; | ||
122 | } | ||
123 | } | ||
124 | |||
125 | ::ng-deep .tab-content { | ||
126 | @include content-preview-base(); | ||
127 | background-color: var(--mainBackgroundColor); | ||
128 | scrollbar-color: var(--actionButtonColor) var(--mainBackgroundColor); | ||
129 | } | ||
130 | |||
131 | textarea, | ||
132 | ::ng-deep .tab-content { | ||
133 | max-height: none !important; | ||
134 | max-width: none !important; | ||
135 | margin-top: #{$nav-preview-tab-height + $nav-preview-vertical-padding} !important; | ||
136 | height: calc(100vh - #{$header-height + $nav-preview-tab-height + $nav-preview-vertical-padding}) !important; | ||
137 | width: 50% !important; | ||
138 | border: none !important; | ||
139 | border-radius: unset !important; | ||
140 | } | ||
141 | |||
142 | :host-context(.expanded) { | ||
143 | .root.maximized { | ||
144 | left: 0; | ||
145 | width: 100%; | ||
146 | } | ||
147 | } | ||
148 | } | ||
149 | |||
150 | @mixin maximized-in-small-view { | ||
151 | .root.maximized { | ||
152 | @include maximized-base(); | ||
153 | |||
154 | textarea { | ||
155 | display: none; | ||
34 | } | 156 | } |
157 | |||
158 | ::ng-deep .tab-content { | ||
159 | width: 100% !important; | ||
160 | } | ||
161 | } | ||
162 | } | ||
163 | |||
164 | @mixin maximized-tabs-in-mobile-view { | ||
165 | // Ellipsis on tabs for mobile view | ||
166 | .root.maximized { | ||
167 | .nav-preview { | ||
168 | ::ng-deep .nav-link { | ||
169 | @include ellipsis(); | ||
170 | |||
171 | display: block !important; | ||
172 | max-width: 45% !important; | ||
173 | padding: 5px 0 !important; | ||
174 | margin-right: 10px !important; | ||
175 | text-align: center; | ||
176 | |||
177 | &:not(.active) { | ||
178 | max-width: 15% !important; | ||
179 | } | ||
180 | |||
181 | &.active { | ||
182 | padding: 5px 15px !important; | ||
183 | } | ||
184 | } | ||
185 | } | ||
186 | } | ||
187 | } | ||
188 | |||
189 | @mixin in-medium-view { | ||
190 | .root { | ||
191 | .nav-preview { | ||
192 | @include nav-preview-medium(); | ||
193 | } | ||
194 | |||
195 | ::ng-deep .tab-content { | ||
196 | @include content-preview-base(); | ||
197 | max-height: 210px; | ||
198 | border-bottom: 1px solid $input-border-color; | ||
199 | border-left: 1px solid $input-border-color; | ||
200 | border-right: 1px solid $input-border-color; | ||
201 | border-bottom-left-radius: $input-border-radius; | ||
202 | border-bottom-right-radius: $input-border-radius; | ||
203 | } | ||
204 | } | ||
205 | } | ||
206 | |||
207 | @mixin maximized-in-medium-view { | ||
208 | .root.maximized { | ||
209 | @include maximized-base(); | ||
210 | |||
211 | textarea { | ||
212 | display: block; | ||
213 | padding: $base-padding; | ||
214 | border-right: 1px dashed $input-border-color !important; | ||
215 | resize: none; | ||
216 | scrollbar-color: var(--actionButtonColor) var(--textareaBackgroundColor); | ||
217 | |||
218 | &:focus { | ||
219 | box-shadow: none; | ||
220 | } | ||
221 | } | ||
222 | } | ||
223 | } | ||
224 | |||
225 | @include in-small-view(); | ||
226 | @include maximized-in-small-view(); | ||
227 | |||
228 | @media only screen and (max-width: $mobile-view) { | ||
229 | @include maximized-tabs-in-mobile-view(); | ||
230 | } | ||
231 | |||
232 | @media only screen and (max-width: #{$mobile-view + $menu-width}) { | ||
233 | :host-context(.main-col:not(.expanded)) { | ||
234 | @include maximized-tabs-in-mobile-view(); | ||
235 | } | ||
236 | } | ||
237 | |||
238 | @media only screen and (min-width: $small-view) { | ||
239 | :host-context(.expanded) { | ||
240 | @include in-medium-view(); | ||
241 | } | ||
242 | |||
243 | @include maximized-in-medium-view(); | ||
244 | } | ||
245 | |||
246 | @media only screen and (min-width: #{$small-view + $menu-width}) { | ||
247 | :host-context(.main-col:not(.expanded)) { | ||
248 | @include in-medium-view(); | ||
35 | } | 249 | } |
36 | } | 250 | } |
diff --git a/client/src/app/shared/forms/markdown-textarea.component.ts b/client/src/app/shared/forms/markdown-textarea.component.ts index cbcfdfe78..dde7b4d98 100644 --- a/client/src/app/shared/forms/markdown-textarea.component.ts +++ b/client/src/app/shared/forms/markdown-textarea.component.ts | |||
@@ -1,5 +1,5 @@ | |||
1 | import { debounceTime, distinctUntilChanged } from 'rxjs/operators' | 1 | import { debounceTime, distinctUntilChanged } from 'rxjs/operators' |
2 | import { Component, forwardRef, Input, OnInit } from '@angular/core' | 2 | import { Component, forwardRef, Input, OnInit, ViewChild, ElementRef } from '@angular/core' |
3 | import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms' | 3 | import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms' |
4 | import { Subject } from 'rxjs' | 4 | import { Subject } from 'rxjs' |
5 | import truncate from 'lodash-es/truncate' | 5 | import truncate from 'lodash-es/truncate' |
@@ -22,18 +22,18 @@ import { MarkdownService } from '@app/shared/renderer' | |||
22 | export class MarkdownTextareaComponent implements ControlValueAccessor, OnInit { | 22 | export class MarkdownTextareaComponent implements ControlValueAccessor, OnInit { |
23 | @Input() content = '' | 23 | @Input() content = '' |
24 | @Input() classes: string[] | { [klass: string]: any[] | any } = [] | 24 | @Input() classes: string[] | { [klass: string]: any[] | any } = [] |
25 | @Input() textareaWidth = '100%' | 25 | @Input() textareaMaxWidth = '100%' |
26 | @Input() textareaHeight = '150px' | 26 | @Input() textareaHeight = '150px' |
27 | @Input() previewColumn = false | ||
28 | @Input() truncate: number | 27 | @Input() truncate: number |
29 | @Input() markdownType: 'text' | 'enhanced' = 'text' | 28 | @Input() markdownType: 'text' | 'enhanced' = 'text' |
30 | @Input() markdownVideo = false | 29 | @Input() markdownVideo = false |
31 | @Input() name = 'description' | 30 | @Input() name = 'description' |
32 | 31 | ||
33 | textareaMarginRight = '0' | 32 | @ViewChild('textarea') textareaElement: ElementRef |
34 | flexDirection = 'column' | 33 | |
35 | truncatedPreviewHTML = '' | 34 | truncatedPreviewHTML = '' |
36 | previewHTML = '' | 35 | previewHTML = '' |
36 | isMaximized = false | ||
37 | 37 | ||
38 | private contentChanged = new Subject<string>() | 38 | private contentChanged = new Subject<string>() |
39 | 39 | ||
@@ -51,11 +51,6 @@ export class MarkdownTextareaComponent implements ControlValueAccessor, OnInit { | |||
51 | .subscribe(() => this.updatePreviews()) | 51 | .subscribe(() => this.updatePreviews()) |
52 | 52 | ||
53 | this.contentChanged.next(this.content) | 53 | this.contentChanged.next(this.content) |
54 | |||
55 | if (this.previewColumn) { | ||
56 | this.flexDirection = 'row' | ||
57 | this.textareaMarginRight = '15px' | ||
58 | } | ||
59 | } | 54 | } |
60 | 55 | ||
61 | propagateChange = (_: any) => { /* empty */ } | 56 | propagateChange = (_: any) => { /* empty */ } |
@@ -80,8 +75,26 @@ export class MarkdownTextareaComponent implements ControlValueAccessor, OnInit { | |||
80 | this.contentChanged.next(this.content) | 75 | this.contentChanged.next(this.content) |
81 | } | 76 | } |
82 | 77 | ||
83 | arePreviewsDisplayed () { | 78 | onMaximizeClick () { |
84 | return this.screenService.isInSmallView() === false | 79 | this.isMaximized = !this.isMaximized |
80 | |||
81 | // Make sure textarea have the focus | ||
82 | this.textareaElement.nativeElement.focus() | ||
83 | |||
84 | // Make sure the window has no scrollbars | ||
85 | if (!this.isMaximized) { | ||
86 | this.unlockBodyScroll() | ||
87 | } else { | ||
88 | this.lockBodyScroll() | ||
89 | } | ||
90 | } | ||
91 | |||
92 | private lockBodyScroll () { | ||
93 | document.getElementById('content').classList.add('lock-scroll') | ||
94 | } | ||
95 | |||
96 | private unlockBodyScroll () { | ||
97 | document.getElementById('content').classList.remove('lock-scroll') | ||
85 | } | 98 | } |
86 | 99 | ||
87 | private async updatePreviews () { | 100 | private async updatePreviews () { |
diff --git a/client/src/app/shared/images/global-icon.component.ts b/client/src/app/shared/images/global-icon.component.ts index a8e5a7020..d2700f6c3 100644 --- a/client/src/app/shared/images/global-icon.component.ts +++ b/client/src/app/shared/images/global-icon.component.ts | |||
@@ -54,7 +54,9 @@ const icons = { | |||
54 | 'users': require('!!raw-loader?!../../../assets/images/global/users.svg').default, | 54 | 'users': require('!!raw-loader?!../../../assets/images/global/users.svg').default, |
55 | 'search': require('!!raw-loader?!../../../assets/images/global/search.svg').default, | 55 | 'search': require('!!raw-loader?!../../../assets/images/global/search.svg').default, |
56 | 'refresh': require('!!raw-loader?!../../../assets/images/global/refresh.svg').default, | 56 | 'refresh': require('!!raw-loader?!../../../assets/images/global/refresh.svg').default, |
57 | 'npm': require('!!raw-loader?!../../../assets/images/global/npm.svg').default | 57 | 'npm': require('!!raw-loader?!../../../assets/images/global/npm.svg').default, |
58 | 'fullscreen': require('!!raw-loader?!../../../assets/images/global/fullscreen.svg').default, | ||
59 | 'exit-fullscreen': require('!!raw-loader?!../../../assets/images/global/exit-fullscreen.svg').default | ||
58 | } | 60 | } |
59 | 61 | ||
60 | export type GlobalIconName = keyof typeof icons | 62 | export type GlobalIconName = keyof typeof icons |