diff options
author | Rigel Kent <sendmemail@rigelk.eu> | 2020-02-05 20:54:37 +0100 |
---|---|---|
committer | Chocobozzz <chocobozzz@cpy.re> | 2020-02-13 10:25:22 +0100 |
commit | 24e7916c6897bbb38e057cdf1a102286006be964 (patch) | |
tree | 7621dd83d532ba04b725f4feeb902ccbdb6669ff /client/src/app/shared | |
parent | eb7c7a517902eae425b05d1ca9cb7f99f76ee71f (diff) | |
download | PeerTube-24e7916c6897bbb38e057cdf1a102286006be964.tar.gz PeerTube-24e7916c6897bbb38e057cdf1a102286006be964.tar.zst PeerTube-24e7916c6897bbb38e057cdf1a102286006be964.zip |
Add ListOverflow component to prevent sub-menu overflow
Diffstat (limited to 'client/src/app/shared')
8 files changed, 217 insertions, 4 deletions
diff --git a/client/src/app/shared/misc/list-overflow.component.html b/client/src/app/shared/misc/list-overflow.component.html new file mode 100644 index 000000000..986572801 --- /dev/null +++ b/client/src/app/shared/misc/list-overflow.component.html | |||
@@ -0,0 +1,35 @@ | |||
1 | <div #itemsParent class="d-flex align-items-center text-nowrap w-100 list-overflow-parent"> | ||
2 | <span [id]="getId(id)" #itemsRendered *ngFor="let item of items; index as id"> | ||
3 | <ng-container *ngTemplateOutlet="itemTemplate; context: {item: item}"></ng-container> | ||
4 | </span> | ||
5 | |||
6 | <ng-container *ngIf="isMenuDisplayed()"> | ||
7 | <button *ngIf="isInMobileView" class="btn btn-outline-secondary btn-sm list-overflow-menu" (click)="toggleModal()"> | ||
8 | <span class="glyphicon glyphicon-chevron-down"></span> | ||
9 | </button> | ||
10 | |||
11 | <div *ngIf="!isInMobileView" class="list-overflow-menu" ngbDropdown container="body" #dropdown="ngbDropdown" (mouseleave)="closeDropdownIfHovered(dropdown)" (mouseenter)="openDropdownOnHover(dropdown)"> | ||
12 | <button class="btn btn-outline-secondary btn-sm" [ngClass]="{ routeActive: active }" | ||
13 | ngbDropdownAnchor (click)="dropdownAnchorClicked(dropdown)" role="button" | ||
14 | > | ||
15 | <span class="glyphicon glyphicon-chevron-down"></span> | ||
16 | </button> | ||
17 | |||
18 | <div ngbDropdownMenu> | ||
19 | <a *ngFor="let item of items | slice:showItemsUntilIndexExcluded:items.length" | ||
20 | [routerLink]="item.routerLink" routerLinkActive="active" class="dropdown-item"> | ||
21 | {{ item.label }} | ||
22 | </a> | ||
23 | </div> | ||
24 | </div> | ||
25 | </ng-container> | ||
26 | </div > | ||
27 | |||
28 | <ng-template #modal let-close="close" let-dismiss="dismiss"> | ||
29 | <div class="modal-body"> | ||
30 | <a *ngFor="let item of items | slice:showItemsUntilIndexExcluded:items.length" | ||
31 | [routerLink]="item.routerLink" routerLinkActive="active" (click)="dismissOtherModals()"> | ||
32 | {{ item.label }} | ||
33 | </a> | ||
34 | </div> | ||
35 | </ng-template> | ||
diff --git a/client/src/app/shared/misc/list-overflow.component.scss b/client/src/app/shared/misc/list-overflow.component.scss new file mode 100644 index 000000000..e26100aca --- /dev/null +++ b/client/src/app/shared/misc/list-overflow.component.scss | |||
@@ -0,0 +1,61 @@ | |||
1 | @import '_mixins'; | ||
2 | |||
3 | :host { | ||
4 | width: 100%; | ||
5 | } | ||
6 | |||
7 | .list-overflow-parent { | ||
8 | overflow: hidden; | ||
9 | } | ||
10 | |||
11 | .list-overflow-menu { | ||
12 | position: absolute; | ||
13 | right: 0; | ||
14 | } | ||
15 | |||
16 | button { | ||
17 | width: 30px; | ||
18 | border: none; | ||
19 | |||
20 | &::after { | ||
21 | display: none; | ||
22 | } | ||
23 | |||
24 | &.routeActive { | ||
25 | &::after { | ||
26 | display: inherit; | ||
27 | border: 2px solid var(--mainColor); | ||
28 | position: relative; | ||
29 | right: 95%; | ||
30 | top: 50%; | ||
31 | } | ||
32 | } | ||
33 | } | ||
34 | |||
35 | ::ng-deep .dropdown-menu { | ||
36 | margin-top: 0 !important; | ||
37 | position: static; | ||
38 | right: auto; | ||
39 | bottom: auto | ||
40 | } | ||
41 | |||
42 | .modal-body { | ||
43 | a { | ||
44 | @include disable-default-a-behaviour; | ||
45 | |||
46 | color: currentColor; | ||
47 | box-sizing: border-box; | ||
48 | display: block; | ||
49 | font-size: 1.2rem; | ||
50 | padding: 9px 12px; | ||
51 | text-align: initial; | ||
52 | text-transform: unset; | ||
53 | width: 100%; | ||
54 | |||
55 | &.active { | ||
56 | color: var(--mainBackgroundColor) !important; | ||
57 | background-color: var(--mainHoverColor); | ||
58 | opacity: .9; | ||
59 | } | ||
60 | } | ||
61 | } | ||
diff --git a/client/src/app/shared/misc/list-overflow.component.ts b/client/src/app/shared/misc/list-overflow.component.ts new file mode 100644 index 000000000..4f92c0f7c --- /dev/null +++ b/client/src/app/shared/misc/list-overflow.component.ts | |||
@@ -0,0 +1,114 @@ | |||
1 | import { | ||
2 | Component, | ||
3 | Input, | ||
4 | TemplateRef, | ||
5 | ViewChildren, | ||
6 | ViewChild, | ||
7 | QueryList, | ||
8 | ChangeDetectionStrategy, | ||
9 | ElementRef, | ||
10 | ChangeDetectorRef, | ||
11 | HostListener | ||
12 | } from '@angular/core' | ||
13 | import { NgbModal, NgbDropdown } from '@ng-bootstrap/ng-bootstrap' | ||
14 | import { uniqueId, lowerFirst } from 'lodash-es' | ||
15 | import { ScreenService } from './screen.service' | ||
16 | import { take } from 'rxjs/operators' | ||
17 | |||
18 | export interface ListOverflowItem { | ||
19 | label: string | ||
20 | routerLink: string | any[] | ||
21 | } | ||
22 | |||
23 | @Component({ | ||
24 | selector: 'list-overflow', | ||
25 | templateUrl: './list-overflow.component.html', | ||
26 | styleUrls: [ './list-overflow.component.scss' ], | ||
27 | changeDetection: ChangeDetectionStrategy.OnPush | ||
28 | }) | ||
29 | export class ListOverflowComponent<T extends ListOverflowItem> { | ||
30 | @ViewChild('modal', { static: true }) modal: ElementRef | ||
31 | @ViewChild('itemsParent', { static: true }) parent: ElementRef<HTMLDivElement> | ||
32 | @ViewChildren('itemsRendered') itemsRendered: QueryList<ElementRef> | ||
33 | @Input() items: T[] | ||
34 | @Input() itemTemplate: TemplateRef<{item: T}> | ||
35 | |||
36 | showItemsUntilIndexExcluded: number | ||
37 | active = false | ||
38 | isInTouchScreen = false | ||
39 | isInMobileView = false | ||
40 | |||
41 | private openedOnHover = false | ||
42 | |||
43 | constructor ( | ||
44 | private cdr: ChangeDetectorRef, | ||
45 | private modalService: NgbModal, | ||
46 | private screenService: ScreenService | ||
47 | ) {} | ||
48 | |||
49 | isMenuDisplayed () { | ||
50 | return !!this.showItemsUntilIndexExcluded | ||
51 | } | ||
52 | |||
53 | @HostListener('window:resize', ['$event']) | ||
54 | onWindowResize () { | ||
55 | this.isInTouchScreen = !!this.screenService.isInTouchScreen() | ||
56 | this.isInMobileView = !!this.screenService.isInMobileView() | ||
57 | |||
58 | const parentWidth = this.parent.nativeElement.getBoundingClientRect().width | ||
59 | let showItemsUntilIndexExcluded: number | ||
60 | let accWidth = 0 | ||
61 | |||
62 | for (const [index, el] of this.itemsRendered.toArray().entries()) { | ||
63 | accWidth += el.nativeElement.getBoundingClientRect().width | ||
64 | if (showItemsUntilIndexExcluded === undefined) { | ||
65 | showItemsUntilIndexExcluded = (parentWidth < accWidth) ? index : undefined | ||
66 | } | ||
67 | |||
68 | const e = document.getElementById(this.getId(index)) | ||
69 | const shouldBeVisible = showItemsUntilIndexExcluded ? index < showItemsUntilIndexExcluded : true | ||
70 | e.style.visibility = shouldBeVisible ? 'inherit' : 'hidden' | ||
71 | } | ||
72 | |||
73 | this.showItemsUntilIndexExcluded = showItemsUntilIndexExcluded | ||
74 | this.cdr.markForCheck() | ||
75 | } | ||
76 | |||
77 | openDropdownOnHover (dropdown: NgbDropdown) { | ||
78 | this.openedOnHover = true | ||
79 | dropdown.open() | ||
80 | |||
81 | // Menu was closed | ||
82 | dropdown.openChange | ||
83 | .pipe(take(1)) | ||
84 | .subscribe(() => this.openedOnHover = false) | ||
85 | } | ||
86 | |||
87 | dropdownAnchorClicked (dropdown: NgbDropdown) { | ||
88 | if (this.openedOnHover) { | ||
89 | this.openedOnHover = false | ||
90 | return | ||
91 | } | ||
92 | |||
93 | return dropdown.toggle() | ||
94 | } | ||
95 | |||
96 | closeDropdownIfHovered (dropdown: NgbDropdown) { | ||
97 | if (this.openedOnHover === false) return | ||
98 | |||
99 | dropdown.close() | ||
100 | this.openedOnHover = false | ||
101 | } | ||
102 | |||
103 | toggleModal () { | ||
104 | this.modalService.open(this.modal, { centered: true }) | ||
105 | } | ||
106 | |||
107 | dismissOtherModals () { | ||
108 | this.modalService.dismissAll() | ||
109 | } | ||
110 | |||
111 | getId (id: number | string = uniqueId()): string { | ||
112 | return lowerFirst(this.constructor.name) + '_' + id | ||
113 | } | ||
114 | } | ||
diff --git a/client/src/app/shared/moderation/user-ban-modal.component.ts b/client/src/app/shared/moderation/user-ban-modal.component.ts index cf0e1577a..1647e3691 100644 --- a/client/src/app/shared/moderation/user-ban-modal.component.ts +++ b/client/src/app/shared/moderation/user-ban-modal.component.ts | |||
@@ -39,7 +39,7 @@ export class UserBanModalComponent extends FormReactive implements OnInit { | |||
39 | 39 | ||
40 | openModal (user: User | User[]) { | 40 | openModal (user: User | User[]) { |
41 | this.usersToBan = user | 41 | this.usersToBan = user |
42 | this.openedModal = this.modalService.open(this.modal) | 42 | this.openedModal = this.modalService.open(this.modal, { centered: true }) |
43 | } | 43 | } |
44 | 44 | ||
45 | hide () { | 45 | hide () { |
diff --git a/client/src/app/shared/shared.module.ts b/client/src/app/shared/shared.module.ts index 759de7020..98211c727 100644 --- a/client/src/app/shared/shared.module.ts +++ b/client/src/app/shared/shared.module.ts | |||
@@ -5,6 +5,7 @@ import { FormsModule, ReactiveFormsModule } from '@angular/forms' | |||
5 | import { RouterModule } from '@angular/router' | 5 | import { RouterModule } from '@angular/router' |
6 | import { MarkdownTextareaComponent } from '@app/shared/forms/markdown-textarea.component' | 6 | import { MarkdownTextareaComponent } from '@app/shared/forms/markdown-textarea.component' |
7 | import { HelpComponent } from '@app/shared/misc/help.component' | 7 | import { HelpComponent } from '@app/shared/misc/help.component' |
8 | import { ListOverflowComponent } from '@app/shared/misc/list-overflow.component' | ||
8 | import { InfiniteScrollerDirective } from '@app/shared/video/infinite-scroller.directive' | 9 | import { InfiniteScrollerDirective } from '@app/shared/video/infinite-scroller.directive' |
9 | import { BytesPipe, KeysPipe, NgPipesModule } from 'ngx-pipes' | 10 | import { BytesPipe, KeysPipe, NgPipesModule } from 'ngx-pipes' |
10 | import { SharedModule as PrimeSharedModule } from 'primeng/api' | 11 | import { SharedModule as PrimeSharedModule } from 'primeng/api' |
@@ -156,6 +157,7 @@ import { ClipboardModule } from '@angular/cdk/clipboard' | |||
156 | InfiniteScrollerDirective, | 157 | InfiniteScrollerDirective, |
157 | TextareaAutoResizeDirective, | 158 | TextareaAutoResizeDirective, |
158 | HelpComponent, | 159 | HelpComponent, |
160 | ListOverflowComponent, | ||
159 | 161 | ||
160 | ReactiveFileComponent, | 162 | ReactiveFileComponent, |
161 | PeertubeCheckboxComponent, | 163 | PeertubeCheckboxComponent, |
@@ -227,6 +229,7 @@ import { ClipboardModule } from '@angular/cdk/clipboard' | |||
227 | InfiniteScrollerDirective, | 229 | InfiniteScrollerDirective, |
228 | TextareaAutoResizeDirective, | 230 | TextareaAutoResizeDirective, |
229 | HelpComponent, | 231 | HelpComponent, |
232 | ListOverflowComponent, | ||
230 | InputReadonlyCopyComponent, | 233 | InputReadonlyCopyComponent, |
231 | 234 | ||
232 | ReactiveFileComponent, | 235 | ReactiveFileComponent, |
diff --git a/client/src/app/shared/video/modals/video-blacklist.component.ts b/client/src/app/shared/video/modals/video-blacklist.component.ts index bdd9c7b99..6ef9c250b 100644 --- a/client/src/app/shared/video/modals/video-blacklist.component.ts +++ b/client/src/app/shared/video/modals/video-blacklist.component.ts | |||
@@ -46,7 +46,7 @@ export class VideoBlacklistComponent extends FormReactive implements OnInit { | |||
46 | } | 46 | } |
47 | 47 | ||
48 | show () { | 48 | show () { |
49 | this.openedModal = this.modalService.open(this.modal, { keyboard: false }) | 49 | this.openedModal = this.modalService.open(this.modal, { centered: true, keyboard: false }) |
50 | } | 50 | } |
51 | 51 | ||
52 | hide () { | 52 | hide () { |
diff --git a/client/src/app/shared/video/modals/video-download.component.ts b/client/src/app/shared/video/modals/video-download.component.ts index c1ceca263..6909c4279 100644 --- a/client/src/app/shared/video/modals/video-download.component.ts +++ b/client/src/app/shared/video/modals/video-download.component.ts | |||
@@ -48,7 +48,7 @@ export class VideoDownloadComponent { | |||
48 | this.video = video | 48 | this.video = video |
49 | this.videoCaptions = videoCaptions && videoCaptions.length ? videoCaptions : undefined | 49 | this.videoCaptions = videoCaptions && videoCaptions.length ? videoCaptions : undefined |
50 | 50 | ||
51 | this.activeModal = this.modalService.open(this.modal) | 51 | this.activeModal = this.modalService.open(this.modal, { centered: true }) |
52 | 52 | ||
53 | this.resolutionId = this.getVideoFiles()[0].resolution.id | 53 | this.resolutionId = this.getVideoFiles()[0].resolution.id |
54 | if (this.videoCaptions) this.subtitleLanguageId = this.videoCaptions[0].language.id | 54 | if (this.videoCaptions) this.subtitleLanguageId = this.videoCaptions[0].language.id |
diff --git a/client/src/app/shared/video/modals/video-report.component.ts b/client/src/app/shared/video/modals/video-report.component.ts index ee991fade..988fa03d4 100644 --- a/client/src/app/shared/video/modals/video-report.component.ts +++ b/client/src/app/shared/video/modals/video-report.component.ts | |||
@@ -53,7 +53,7 @@ export class VideoReportComponent extends FormReactive implements OnInit { | |||
53 | } | 53 | } |
54 | 54 | ||
55 | show () { | 55 | show () { |
56 | this.openedModal = this.modalService.open(this.modal, { keyboard: false }) | 56 | this.openedModal = this.modalService.open(this.modal, { centered: true, keyboard: false }) |
57 | } | 57 | } |
58 | 58 | ||
59 | hide () { | 59 | hide () { |