Add ListOverflow component to prevent sub-menu overflow

pull/2485/head
Rigel Kent 2020-02-05 20:54:37 +01:00 committed by Chocobozzz
parent eb7c7a5179
commit 24e7916c68
26 changed files with 283 additions and 53 deletions

View File

@ -51,7 +51,7 @@ export class ContactAdminModalComponent extends FormReactive implements OnInit {
}
show () {
this.openedModal = this.modalService.open(this.modal, { keyboard: false })
this.openedModal = this.modalService.open(this.modal, { centered: true, keyboard: false })
}
hide () {

View File

@ -38,12 +38,12 @@
</div>
</div>
<div class="links">
<a i18n routerLink="video-channels" routerLinkActive="active" class="title-page">Video channels</a>
<div class="links w-100">
<ng-template #linkTemplate let-item="item">
<a [routerLink]="item.routerLink" routerLinkActive="active" class="title-page">{{ item.label }}</a>
</ng-template>
<a i18n routerLink="videos" routerLinkActive="active" class="title-page">Videos</a>
<a i18n routerLink="about" routerLinkActive="active" class="title-page">About</a>
<list-overflow [items]="links" [itemTemplate]="linkTemplate"></list-overflow>
</div>
</div>

View File

@ -10,6 +10,7 @@ import { User, UserRight } from '../../../../shared'
import { I18n } from '@ngx-translate/i18n-polyfill'
import { VideoChannelService } from '@app/shared/video-channel/video-channel.service'
import { VideoChannel } from '@app/shared/video-channel/video-channel.model'
import { ListOverflowItem } from '@app/shared/misc/list-overflow.component'
@Component({
templateUrl: './accounts.component.html',
@ -19,6 +20,7 @@ export class AccountsComponent implements OnInit, OnDestroy {
account: Account
accountUser: User
videoChannels: VideoChannel[] = []
links: ListOverflowItem[] = []
isAccountManageable = false
accountFollowerTitle = ''
@ -70,6 +72,12 @@ export class AccountsComponent implements OnInit, OnDestroy {
err => this.notifier.error(err.message)
)
this.links = [
{ label: this.i18n('Video channels'), routerLink: 'video-channels' },
{ label: this.i18n('Videos'), routerLink: 'videos' },
{ label: this.i18n('About'), routerLink: 'about' }
]
}
ngOnDestroy () {

View File

@ -1,28 +1,10 @@
<div class="row">
<div class="sub-menu">
<a i18n *ngIf="hasUsersRight()" routerLink="/admin/users" routerLinkActive="active" class="title-page">
Users
</a>
<ng-template #linkTemplate let-item="item">
<a [routerLink]="item.routerLink" routerLinkActive="active" class="title-page">{{ item.label }}</a>
</ng-template>
<a i18n *ngIf="hasServerFollowRight()" routerLink="/admin/follows" routerLinkActive="active" class="title-page">
Follows & redundancies
</a>
<a i18n *ngIf="hasVideoAbusesRight() || hasVideoBlacklistRight()" routerLink="/admin/moderation" routerLinkActive="active" class="title-page">
Moderation
</a>
<a i18n *ngIf="hasConfigRight()" routerLink="/admin/config" routerLinkActive="active" class="title-page">
Configuration
</a>
<a i18n *ngIf="hasPluginsRight()" routerLink="/admin/plugins" routerLinkActive="active" class="title-page">
Plugins/Themes
</a>
<a i18n *ngIf="hasJobsRight() || hasLogsRight() || hasDebugRight()" routerLink="/admin/system" routerLinkActive="active" class="title-page">
System
</a>
<list-overflow [items]="items" [itemTemplate]="linkTemplate"></list-overflow>
</div>
<div class="margin-content">

View File

@ -1,12 +1,28 @@
import { Component } from '@angular/core'
import { Component, OnInit } from '@angular/core'
import { UserRight } from '../../../../shared'
import { AuthService } from '../core/auth/auth.service'
import { ListOverflowItem } from '@app/shared/misc/list-overflow.component'
import { I18n } from '@ngx-translate/i18n-polyfill'
@Component({
templateUrl: './admin.component.html'
})
export class AdminComponent {
constructor (private auth: AuthService) {}
export class AdminComponent implements OnInit {
items: ListOverflowItem[] = []
constructor (
private auth: AuthService,
private i18n: I18n
) {}
ngOnInit () {
if (this.hasUsersRight()) this.items.push({ label: this.i18n('Users'), routerLink: '/admin/users' })
if (this.hasServerFollowRight()) this.items.push({ label: this.i18n('Follows & redundancies'), routerLink: '/admin/follows' })
if (this.hasVideoAbusesRight() || this.hasVideoBlacklistRight()) this.items.push({ label: this.i18n('Moderation'), routerLink: '/admin/moderation' })
if (this.hasConfigRight()) this.items.push({ label: this.i18n('Configuration'), routerLink: '/admin/config' })
if (this.hasPluginsRight()) this.items.push({ label: this.i18n('Plugins/Themes'), routerLink: '/admin/plugins' })
if (this.hasJobsRight() || this.hasLogsRight() || this.hasDebugRight()) this.items.push({ label: this.i18n('System'), routerLink: '/admin/system' })
}
hasUsersRight () {
return this.auth.getUser().hasRight(UserRight.MANAGE_USERS)

View File

@ -38,7 +38,7 @@ export class ModerationCommentModalComponent extends FormReactive implements OnI
openModal (abuseToComment: VideoAbuse) {
this.abuseToComment = abuseToComment
this.openedModal = this.modalService.open(this.modal)
this.openedModal = this.modalService.open(this.modal, { centered: true })
this.form.patchValue({
moderationComment: this.abuseToComment.moderationComment

View File

@ -53,7 +53,7 @@ export class MyAccountAcceptOwnershipComponent extends FormReactive implements O
show (videoChangeOwnership: VideoChangeOwnership) {
this.videoChangeOwnership = videoChangeOwnership
this.modalService
.open(this.modal)
.open(this.modal, { centered: true })
.result
.then(() => this.acceptOwnership())
.catch(() => this.videoChangeOwnership = undefined)

View File

@ -43,7 +43,7 @@ export class VideoChangeOwnershipComponent extends FormReactive implements OnIni
show (video: Video) {
this.video = video
this.modalService
.open(this.modal)
.open(this.modal, { centered: true })
.result
.then(() => this.changeOwnership())
.catch((_) => _) // Called when closing (cancel) the modal without validating, do nothing

View File

@ -29,10 +29,12 @@
</div>
</div>
<div class="links">
<a i18n routerLink="videos" routerLinkActive="active" class="title-page">Videos</a>
<a i18n routerLink="video-playlists" routerLinkActive="active" class="title-page">Video playlists</a>
<a i18n routerLink="about" routerLinkActive="active" class="title-page">About</a>
<div class="links w-100">
<ng-template #linkTemplate let-item="item">
<a [routerLink]="item.routerLink" routerLinkActive="active" class="title-page">{{ item.label }}</a>
</ng-template>
<list-overflow [items]="links" [itemTemplate]="linkTemplate"></list-overflow>
</div>
</div>

View File

@ -9,6 +9,7 @@ import { AuthService, Notifier } from '@app/core'
import { Hotkey, HotkeysService } from 'angular2-hotkeys'
import { SubscribeButtonComponent } from '@app/shared/user-subscription/subscribe-button.component'
import { I18n } from '@ngx-translate/i18n-polyfill'
import { ListOverflowItem } from '@app/shared/misc/list-overflow.component'
@Component({
templateUrl: './video-channels.component.html',
@ -19,6 +20,7 @@ export class VideoChannelsComponent implements OnInit, OnDestroy {
videoChannel: VideoChannel
hotkeys: Hotkey[]
links: ListOverflowItem[] = []
isChannelManageable = false
private routeSub: Subscription
@ -62,6 +64,12 @@ export class VideoChannelsComponent implements OnInit, OnDestroy {
}, undefined, this.i18n('Subscribe to the account'))
]
if (this.isUserLoggedIn()) this.hotkeysService.add(this.hotkeys)
this.links = [
{ label: this.i18n('Videos'), routerLink: 'videos' },
{ label: this.i18n('Video playlists'), routerLink: 'video-playlists' },
{ label: this.i18n('About'), routerLink: 'about' }
]
}
ngOnDestroy () {

View File

@ -21,7 +21,7 @@ export class LanguageChooserComponent {
}
show () {
this.modalService.open(this.modal)
this.modalService.open(this.modal, { centered: true })
}
buildLanguageLink (lang: { id: string }) {

View File

@ -24,7 +24,7 @@ export class InstanceConfigWarningModalComponent {
show (about: About) {
this.about = about
const ref = this.modalService.open(this.modal)
const ref = this.modalService.open(this.modal, { centered: true })
ref.result.finally(() => {
if (this.stopDisplayModal === true) this.doNotOpenAgain()

View File

@ -18,7 +18,8 @@ export class WelcomeModalComponent {
) { }
show () {
this.modalService.open(this.modal,{
this.modalService.open(this.modal, {
centered: true,
backdrop: 'static',
keyboard: false,
size: 'lg'

View File

@ -0,0 +1,35 @@
<div #itemsParent class="d-flex align-items-center text-nowrap w-100 list-overflow-parent">
<span [id]="getId(id)" #itemsRendered *ngFor="let item of items; index as id">
<ng-container *ngTemplateOutlet="itemTemplate; context: {item: item}"></ng-container>
</span>
<ng-container *ngIf="isMenuDisplayed()">
<button *ngIf="isInMobileView" class="btn btn-outline-secondary btn-sm list-overflow-menu" (click)="toggleModal()">
<span class="glyphicon glyphicon-chevron-down"></span>
</button>
<div *ngIf="!isInMobileView" class="list-overflow-menu" ngbDropdown container="body" #dropdown="ngbDropdown" (mouseleave)="closeDropdownIfHovered(dropdown)" (mouseenter)="openDropdownOnHover(dropdown)">
<button class="btn btn-outline-secondary btn-sm" [ngClass]="{ routeActive: active }"
ngbDropdownAnchor (click)="dropdownAnchorClicked(dropdown)" role="button"
>
<span class="glyphicon glyphicon-chevron-down"></span>
</button>
<div ngbDropdownMenu>
<a *ngFor="let item of items | slice:showItemsUntilIndexExcluded:items.length"
[routerLink]="item.routerLink" routerLinkActive="active" class="dropdown-item">
{{ item.label }}
</a>
</div>
</div>
</ng-container>
</div >
<ng-template #modal let-close="close" let-dismiss="dismiss">
<div class="modal-body">
<a *ngFor="let item of items | slice:showItemsUntilIndexExcluded:items.length"
[routerLink]="item.routerLink" routerLinkActive="active" (click)="dismissOtherModals()">
{{ item.label }}
</a>
</div>
</ng-template>

View File

@ -0,0 +1,61 @@
@import '_mixins';
:host {
width: 100%;
}
.list-overflow-parent {
overflow: hidden;
}
.list-overflow-menu {
position: absolute;
right: 0;
}
button {
width: 30px;
border: none;
&::after {
display: none;
}
&.routeActive {
&::after {
display: inherit;
border: 2px solid var(--mainColor);
position: relative;
right: 95%;
top: 50%;
}
}
}
::ng-deep .dropdown-menu {
margin-top: 0 !important;
position: static;
right: auto;
bottom: auto
}
.modal-body {
a {
@include disable-default-a-behaviour;
color: currentColor;
box-sizing: border-box;
display: block;
font-size: 1.2rem;
padding: 9px 12px;
text-align: initial;
text-transform: unset;
width: 100%;
&.active {
color: var(--mainBackgroundColor) !important;
background-color: var(--mainHoverColor);
opacity: .9;
}
}
}

View File

@ -0,0 +1,114 @@
import {
Component,
Input,
TemplateRef,
ViewChildren,
ViewChild,
QueryList,
ChangeDetectionStrategy,
ElementRef,
ChangeDetectorRef,
HostListener
} from '@angular/core'
import { NgbModal, NgbDropdown } from '@ng-bootstrap/ng-bootstrap'
import { uniqueId, lowerFirst } from 'lodash-es'
import { ScreenService } from './screen.service'
import { take } from 'rxjs/operators'
export interface ListOverflowItem {
label: string
routerLink: string | any[]
}
@Component({
selector: 'list-overflow',
templateUrl: './list-overflow.component.html',
styleUrls: [ './list-overflow.component.scss' ],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ListOverflowComponent<T extends ListOverflowItem> {
@ViewChild('modal', { static: true }) modal: ElementRef
@ViewChild('itemsParent', { static: true }) parent: ElementRef<HTMLDivElement>
@ViewChildren('itemsRendered') itemsRendered: QueryList<ElementRef>
@Input() items: T[]
@Input() itemTemplate: TemplateRef<{item: T}>
showItemsUntilIndexExcluded: number
active = false
isInTouchScreen = false
isInMobileView = false
private openedOnHover = false
constructor (
private cdr: ChangeDetectorRef,
private modalService: NgbModal,
private screenService: ScreenService
) {}
isMenuDisplayed () {
return !!this.showItemsUntilIndexExcluded
}
@HostListener('window:resize', ['$event'])
onWindowResize () {
this.isInTouchScreen = !!this.screenService.isInTouchScreen()
this.isInMobileView = !!this.screenService.isInMobileView()
const parentWidth = this.parent.nativeElement.getBoundingClientRect().width
let showItemsUntilIndexExcluded: number
let accWidth = 0
for (const [index, el] of this.itemsRendered.toArray().entries()) {
accWidth += el.nativeElement.getBoundingClientRect().width
if (showItemsUntilIndexExcluded === undefined) {
showItemsUntilIndexExcluded = (parentWidth < accWidth) ? index : undefined
}
const e = document.getElementById(this.getId(index))
const shouldBeVisible = showItemsUntilIndexExcluded ? index < showItemsUntilIndexExcluded : true
e.style.visibility = shouldBeVisible ? 'inherit' : 'hidden'
}
this.showItemsUntilIndexExcluded = showItemsUntilIndexExcluded
this.cdr.markForCheck()
}
openDropdownOnHover (dropdown: NgbDropdown) {
this.openedOnHover = true
dropdown.open()
// Menu was closed
dropdown.openChange
.pipe(take(1))
.subscribe(() => this.openedOnHover = false)
}
dropdownAnchorClicked (dropdown: NgbDropdown) {
if (this.openedOnHover) {
this.openedOnHover = false
return
}
return dropdown.toggle()
}
closeDropdownIfHovered (dropdown: NgbDropdown) {
if (this.openedOnHover === false) return
dropdown.close()
this.openedOnHover = false
}
toggleModal () {
this.modalService.open(this.modal, { centered: true })
}
dismissOtherModals () {
this.modalService.dismissAll()
}
getId (id: number | string = uniqueId()): string {
return lowerFirst(this.constructor.name) + '_' + id
}
}

View File

@ -39,7 +39,7 @@ export class UserBanModalComponent extends FormReactive implements OnInit {
openModal (user: User | User[]) {
this.usersToBan = user
this.openedModal = this.modalService.open(this.modal)
this.openedModal = this.modalService.open(this.modal, { centered: true })
}
hide () {

View File

@ -5,6 +5,7 @@ import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { RouterModule } from '@angular/router'
import { MarkdownTextareaComponent } from '@app/shared/forms/markdown-textarea.component'
import { HelpComponent } from '@app/shared/misc/help.component'
import { ListOverflowComponent } from '@app/shared/misc/list-overflow.component'
import { InfiniteScrollerDirective } from '@app/shared/video/infinite-scroller.directive'
import { BytesPipe, KeysPipe, NgPipesModule } from 'ngx-pipes'
import { SharedModule as PrimeSharedModule } from 'primeng/api'
@ -156,6 +157,7 @@ import { ClipboardModule } from '@angular/cdk/clipboard'
InfiniteScrollerDirective,
TextareaAutoResizeDirective,
HelpComponent,
ListOverflowComponent,
ReactiveFileComponent,
PeertubeCheckboxComponent,
@ -227,6 +229,7 @@ import { ClipboardModule } from '@angular/cdk/clipboard'
InfiniteScrollerDirective,
TextareaAutoResizeDirective,
HelpComponent,
ListOverflowComponent,
InputReadonlyCopyComponent,
ReactiveFileComponent,

View File

@ -46,7 +46,7 @@ export class VideoBlacklistComponent extends FormReactive implements OnInit {
}
show () {
this.openedModal = this.modalService.open(this.modal, { keyboard: false })
this.openedModal = this.modalService.open(this.modal, { centered: true, keyboard: false })
}
hide () {

View File

@ -48,7 +48,7 @@ export class VideoDownloadComponent {
this.video = video
this.videoCaptions = videoCaptions && videoCaptions.length ? videoCaptions : undefined
this.activeModal = this.modalService.open(this.modal)
this.activeModal = this.modalService.open(this.modal, { centered: true })
this.resolutionId = this.getVideoFiles()[0].resolution.id
if (this.videoCaptions) this.subtitleLanguageId = this.videoCaptions[0].language.id

View File

@ -53,7 +53,7 @@ export class VideoReportComponent extends FormReactive implements OnInit {
}
show () {
this.openedModal = this.modalService.open(this.modal, { keyboard: false })
this.openedModal = this.modalService.open(this.modal, { centered: true, keyboard: false })
}
hide () {

View File

@ -56,7 +56,7 @@ export class VideoCaptionAddModalComponent extends FormReactive implements OnIni
show () {
this.closingModal = false
this.openedModal = this.modalService.open(this.modal, { keyboard: false })
this.openedModal = this.modalService.open(this.modal, { centered: true, keyboard: false })
}
hide () {

View File

@ -72,7 +72,7 @@ export class VideoShareComponent {
controls: true
}
this.modalService.open(this.modal)
this.modalService.open(this.modal, { centered: true })
}
getVideoIframeCode () {

View File

@ -21,7 +21,7 @@ export class VideoSupportComponent {
) { }
show () {
this.modalService.open(this.modal)
this.modalService.open(this.modal, { centered: true })
this.markdownService.enhancedMarkdownToHTML(this.video.support)
.then(r => this.videoHTMLSupport = r)

View File

@ -252,7 +252,7 @@ table {
padding-left: 50px;
.title-page {
font-size: 15px;
font-size: 17px;
}
}
}

View File

@ -30,8 +30,10 @@ $icon-font-path: '~@neos21/bootstrap3-glyphicons/assets/fonts/';
.dropdown-item {
padding: 3px 15px;
&:active {
color: #000 !important;
&.active {
color: var(--mainBackgroundColor) !important;
background-color: var(--mainHoverColor);
opacity: .9;
}
}
@ -48,14 +50,12 @@ $icon-font-path: '~@neos21/bootstrap3-glyphicons/assets/fonts/';
@media screen and (min-width: 768px) {
.modal:before {
display: inline-block;
vertical-align: middle;
content: " ";
height: 100%;
}
.modal-dialog {
display: inline-block;
text-align: left;
vertical-align: middle;
min-width: 500px;