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/misc | |
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/misc')
3 files changed, 210 insertions, 0 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 | } | ||