My account menu -> open entries on hover
authorChocobozzz <me@florianbigard.com>
Wed, 5 Dec 2018 13:21:10 +0000 (14:21 +0100)
committerChocobozzz <me@florianbigard.com>
Wed, 5 Dec 2018 13:21:31 +0000 (14:21 +0100)
client/src/app/+my-account/my-account.component.html
client/src/app/+my-account/my-account.component.scss
client/src/app/+my-account/my-account.component.ts
client/src/app/shared/menu/top-menu-dropdown.component.html [new file with mode: 0644]
client/src/app/shared/menu/top-menu-dropdown.component.scss [new file with mode: 0644]
client/src/app/shared/menu/top-menu-dropdown.component.ts [new file with mode: 0644]
client/src/app/shared/shared.module.ts

index 41333c25a78787728dab52aca0957c03b8168369..3999252beb60fdb7668011bcadc7ee4f1f7a90de 100644 (file)
@@ -1,40 +1,5 @@
 <div class="row">
-  <div class="sub-menu">
-    <a i18n routerLink="/my-account/settings" routerLinkActive="active" class="title-page">My settings</a>
-
-    <div ngbDropdown class="my-library">
-      <span role="button" class="title-page" [ngClass]="{ active: libraryLabel !== '' }" ngbDropdownToggle>
-        <ng-container i18n>My library</ng-container>
-        <ng-container *ngIf="libraryLabel"> - {{ libraryLabel }}</ng-container>
-      </span>
-
-      <div ngbDropdownMenu>
-        <a class="dropdown-item" i18n routerLink="/my-account/video-channels">My channels</a>
-
-        <a class="dropdown-item" i18n routerLink="/my-account/videos">My videos</a>
-
-        <a class="dropdown-item" i18n routerLink="/my-account/subscriptions">My subscriptions</a>
-
-        <a class="dropdown-item" *ngIf="isVideoImportEnabled()" i18n routerLink="/my-account/video-imports">My imports</a>
-      </div>
-    </div>
-
-    <div ngbDropdown class="misc">
-      <span role="button" class="title-page" [ngClass]="{ active: miscLabel !== '' }" ngbDropdownToggle>
-        <ng-container i18n>Misc</ng-container>
-        <ng-container *ngIf="miscLabel"> - {{ miscLabel }}</ng-container>
-      </span>
-
-      <div ngbDropdownMenu>
-        <a class="dropdown-item" i18n routerLink="/my-account/blocklist/accounts">Muted accounts</a>
-
-        <a class="dropdown-item" i18n routerLink="/my-account/blocklist/servers">Muted instances</a>
-
-        <a class="dropdown-item" i18n routerLink="/my-account/ownership">Ownership changes</a>
-      </div>
-    </div>
-
-  </div>
+  <my-top-menu-dropdown [menuEntries]="menuEntries"></my-top-menu-dropdown>
 
   <div class="margin-content">
     <router-outlet></router-outlet>
index 6243c6dcfe21806368e4b98c579445d0c016bdfc..4f111efdf76bb326526a93a8bb9d95004dfa844c 100644 (file)
@@ -1,14 +1,3 @@
-.my-library, .misc {
-  span[role=button] {
-    cursor: pointer;
-  }
-
-  a {
-    display: block;
-  }
+.row {
+  flex-direction: column;
 }
-
-/deep/ .dropdown-toggle::after {
-  position: relative;
-  top: 2px;
-}
\ No newline at end of file
index d728caf0758ded3dea274d8f6941241b4017e24b..d9381ebfa167da6861d7f1448f07d57e3e2e16a7 100644 (file)
@@ -1,38 +1,72 @@
-import { Component, OnDestroy, OnInit } from '@angular/core'
+import { Component } from '@angular/core'
 import { ServerService } from '@app/core'
-import { NavigationStart, Router } from '@angular/router'
-import { filter } from 'rxjs/operators'
 import { I18n } from '@ngx-translate/i18n-polyfill'
-import { Subscription } from 'rxjs'
+import { TopMenuDropdownParam } from '@app/shared/menu/top-menu-dropdown.component'
 
 @Component({
   selector: 'my-my-account',
   templateUrl: './my-account.component.html',
   styleUrls: [ './my-account.component.scss' ]
 })
-export class MyAccountComponent implements OnInit, OnDestroy {
-
-  libraryLabel = ''
-  miscLabel = ''
-
-  private routeSub: Subscription
+export class MyAccountComponent {
+  menuEntries: TopMenuDropdownParam[] = []
 
   constructor (
     private serverService: ServerService,
-    private router: Router,
     private i18n: I18n
-  ) {}
+  ) {
+
+    const libraryEntries: TopMenuDropdownParam = {
+      label: this.i18n('My library'),
+      children: [
+        {
+          label: this.i18n('My channels'),
+          routerLink: '/my-account/videos'
+        },
+        {
+          label: this.i18n('My videos'),
+          routerLink: '/my-account/videos'
+        },
+        {
+          label: this.i18n('My subscriptions'),
+          routerLink: '/my-account/subscriptions'
+        }
+      ]
+    }
 
-  ngOnInit () {
-    this.updateLabels(this.router.url)
+    if (this.isVideoImportEnabled()) {
+      libraryEntries.children.push({
+        label: 'My imports',
+        routerLink: '/my-account/video-imports'
+      })
+    }
 
-    this.routeSub = this.router.events
-        .pipe(filter(event => event instanceof NavigationStart))
-        .subscribe((event: NavigationStart) => this.updateLabels(event.url))
-  }
+    const miscEntries: TopMenuDropdownParam = {
+      label: this.i18n('Misc'),
+      children: [
+        {
+          label: this.i18n('Muted accounts'),
+          routerLink: '/my-account/blocklist/accounts'
+        },
+        {
+          label: this.i18n('Muted instances'),
+          routerLink: '/my-account/blocklist/servers'
+        },
+        {
+          label: this.i18n('Ownership changes'),
+          routerLink: '/my-account/ownership'
+        }
+      ]
+    }
 
-  ngOnDestroy () {
-    if (this.routeSub) this.routeSub.unsubscribe()
+    this.menuEntries = [
+      {
+        label: this.i18n('My settings'),
+        routerLink: '/my-account/settings'
+      },
+      libraryEntries,
+      miscEntries
+    ]
   }
 
   isVideoImportEnabled () {
@@ -41,27 +75,4 @@ export class MyAccountComponent implements OnInit, OnDestroy {
     return importConfig.http.enabled || importConfig.torrent.enabled
   }
 
-  private updateLabels (url: string) {
-    const [ path ] = url.split('?')
-
-    if (path.startsWith('/my-account/video-channels')) {
-      this.libraryLabel = this.i18n('Channels')
-    } else if (path.startsWith('/my-account/videos')) {
-      this.libraryLabel = this.i18n('Videos')
-    } else if (path.startsWith('/my-account/subscriptions')) {
-      this.libraryLabel = this.i18n('Subscriptions')
-    } else if (path.startsWith('/my-account/video-imports')) {
-      this.libraryLabel = this.i18n('Video imports')
-    } else {
-      this.libraryLabel = ''
-    }
-
-    if (path.startsWith('/my-account/blocklist/accounts')) {
-      this.miscLabel = this.i18n('Muted accounts')
-    } else if (path.startsWith('/my-account/blocklist/servers')) {
-      this.miscLabel = this.i18n('Muted instances')
-    } else {
-      this.miscLabel = ''
-    }
-  }
 }
diff --git a/client/src/app/shared/menu/top-menu-dropdown.component.html b/client/src/app/shared/menu/top-menu-dropdown.component.html
new file mode 100644 (file)
index 0000000..2d6d1c4
--- /dev/null
@@ -0,0 +1,18 @@
+<div class="sub-menu">
+  <ng-container *ngFor="let menuEntry of menuEntries">
+
+    <a *ngIf="menuEntry.routerLink" [routerLink]="menuEntry.routerLink" routerLinkActive="active" class="title-page">{{ menuEntry.label }}</a>
+
+    <div *ngIf="!menuEntry.routerLink" ngbDropdown class="parent-entry" #dropdown="ngbDropdown" (mouseleave)="closeDropdownIfHovered(dropdown)">
+      <span (mouseenter)="openDropdownOnHover(dropdown)" role="button" class="title-page" [ngClass]="{ active: !!suffixLabels[menuEntry.label] }" ngbDropdownToggle>
+        <ng-container i18n>{{ menuEntry.label }}</ng-container>
+        <ng-container *ngIf="!!suffixLabels[menuEntry.label]"> - {{ suffixLabels[menuEntry.label] }}</ng-container>
+      </span>
+
+      <div ngbDropdownMenu>
+        <a *ngFor="let menuChild of menuEntry.children" class="dropdown-item" [routerLink]="menuChild.routerLink">{{ menuChild.label }}</a>
+      </div>
+    </div>
+
+  </ng-container>
+</div>
diff --git a/client/src/app/shared/menu/top-menu-dropdown.component.scss b/client/src/app/shared/menu/top-menu-dropdown.component.scss
new file mode 100644 (file)
index 0000000..f3ef8f8
--- /dev/null
@@ -0,0 +1,14 @@
+.parent-entry {
+  span[role=button] {
+    cursor: pointer;
+  }
+
+  a {
+    display: block;
+  }
+}
+
+/deep/ .dropdown-toggle::after {
+  position: relative;
+  top: 2px;
+}
diff --git a/client/src/app/shared/menu/top-menu-dropdown.component.ts b/client/src/app/shared/menu/top-menu-dropdown.component.ts
new file mode 100644 (file)
index 0000000..272b721
--- /dev/null
@@ -0,0 +1,75 @@
+import { Component, Input, OnDestroy, OnInit } from '@angular/core'
+import { filter, take } from 'rxjs/operators'
+import { NavigationStart, Router } from '@angular/router'
+import { Subscription } from 'rxjs'
+import { NgbDropdown } from '@ng-bootstrap/ng-bootstrap'
+import { drop } from 'lodash-es'
+
+export type TopMenuDropdownParam = {
+  label: string
+  routerLink?: string
+
+  children?: {
+    label: string
+    routerLink: string
+  }[]
+}
+
+@Component({
+  selector: 'my-top-menu-dropdown',
+  templateUrl: './top-menu-dropdown.component.html',
+  styleUrls: [ './top-menu-dropdown.component.scss' ]
+})
+export class TopMenuDropdownComponent implements OnInit, OnDestroy {
+  @Input() menuEntries: TopMenuDropdownParam[] = []
+
+  suffixLabels: { [ parentLabel: string ]: string }
+
+  private openedOnHover = false
+  private routeSub: Subscription
+
+  constructor (private router: Router) {}
+
+  ngOnInit () {
+    this.updateChildLabels(window.location.pathname)
+
+    this.routeSub = this.router.events
+                        .pipe(filter(event => event instanceof NavigationStart))
+                        .subscribe(() => this.updateChildLabels(window.location.pathname))
+  }
+
+  ngOnDestroy () {
+    if (this.routeSub) this.routeSub.unsubscribe()
+  }
+
+  openDropdownOnHover (dropdown: NgbDropdown) {
+    this.openedOnHover = true
+    dropdown.open()
+
+    // Menu was closed
+    dropdown.openChange
+            .pipe(take(1))
+            .subscribe(e => this.openedOnHover = false)
+  }
+
+  closeDropdownIfHovered (dropdown: NgbDropdown) {
+    if (this.openedOnHover === false) return
+
+    dropdown.close()
+    this.openedOnHover = false
+  }
+
+  private updateChildLabels (path: string) {
+    this.suffixLabels = {}
+
+    for (const entry of this.menuEntries) {
+      if (!entry.children) continue
+
+      for (const child of entry.children) {
+        if (path.startsWith(child.routerLink)) {
+          this.suffixLabels[entry.label] = child.label
+        }
+      }
+    }
+  }
+}
index a2fa27b72ac4209ecb41ad3be09702930da4e5ae..9810e948583292f5d720fdf0b62a9796fb2eb58a 100644 (file)
@@ -61,6 +61,7 @@ import { OverviewService } from '@app/shared/overview'
 import { UserBanModalComponent } from '@app/shared/moderation'
 import { UserModerationDropdownComponent } from '@app/shared/moderation/user-moderation-dropdown.component'
 import { BlocklistService } from '@app/shared/blocklist'
+import { TopMenuDropdownComponent } from '@app/shared/menu/top-menu-dropdown.component'
 
 @NgModule({
   imports: [
@@ -102,7 +103,8 @@ import { BlocklistService } from '@app/shared/blocklist'
     RemoteSubscribeComponent,
     InstanceFeaturesTableComponent,
     UserBanModalComponent,
-    UserModerationDropdownComponent
+    UserModerationDropdownComponent,
+    TopMenuDropdownComponent
   ],
 
   exports: [
@@ -141,6 +143,7 @@ import { BlocklistService } from '@app/shared/blocklist'
     InstanceFeaturesTableComponent,
     UserBanModalComponent,
     UserModerationDropdownComponent,
+    TopMenuDropdownComponent,
 
     NumberFormatterPipe,
     ObjectLengthPipe,