diff --git a/src/layout/appmenu.ts b/src/layout/appmenu.ts new file mode 100644 index 0000000..660b04b --- /dev/null +++ b/src/layout/appmenu.ts @@ -0,0 +1,162 @@ +import { Component, inject } from '@angular/core'; +import { AppConfigService } from '@/src/service/appconfigservice'; +import { CommonModule } from '@angular/common'; +import { AppMenuItem } from '@/src/layout/appmenuitem'; + +@Component({ + standalone:true, + imports: [ + CommonModule, + AppMenuItem + ], + template: ` + `, +}) +export class AppMenu { + + model: any[] = []; + + configService = inject(AppConfigService); + + ngOnInit() { + this.model = [ + { + label: 'Home', + items: [{ label: 'Dashboard', icon: 'pi pi-fw pi-home', to: '/' }] + }, + { + label: 'UI Components', + items: [ + { label: 'Form Layout', icon: 'pi pi-fw pi-id-card', to: '/uikit/formlayout' }, + { label: 'Input', icon: 'pi pi-fw pi-check-square', to: '/uikit/input' }, + { label: 'Button', icon: 'pi pi-fw pi-mobile', to: '/uikit/button', class: 'rotated-icon' }, + { label: 'Table', icon: 'pi pi-fw pi-table', to: '/uikit/table' }, + { label: 'List', icon: 'pi pi-fw pi-list', to: '/uikit/list' }, + { label: 'Tree', icon: 'pi pi-fw pi-share-alt', to: '/uikit/tree' }, + { label: 'Panel', icon: 'pi pi-fw pi-tablet', to: '/uikit/panel' }, + { label: 'Overlay', icon: 'pi pi-fw pi-clone', to: '/uikit/overlay' }, + { label: 'Media', icon: 'pi pi-fw pi-image', to: '/uikit/media' }, + { label: 'Menu', icon: 'pi pi-fw pi-bars', to: '/uikit/menu' }, + { label: 'Message', icon: 'pi pi-fw pi-comment', to: '/uikit/message' }, + { label: 'File', icon: 'pi pi-fw pi-file', to: '/uikit/file' }, + { label: 'Chart', icon: 'pi pi-fw pi-chart-bar', to: '/uikit/charts' }, + { label: 'Timeline', icon: 'pi pi-fw pi-calendar', to: '/uikit/timeline' }, + { label: 'Misc', icon: 'pi pi-fw pi-circle', to: '/uikit/misc' } + ] + }, + { + label: 'Pages', + icon: 'pi pi-fw pi-briefcase', + to: '/pages', + items: [ + { + label: 'Landing', + icon: 'pi pi-fw pi-globe', + to: '/landing' + }, + { + label: 'Auth', + icon: 'pi pi-fw pi-user', + items: [ + { + label: 'Login', + icon: 'pi pi-fw pi-sign-in', + to: '/auth/login' + }, + { + label: 'Error', + icon: 'pi pi-fw pi-times-circle', + to: '/auth/error' + }, + { + label: 'Access Denied', + icon: 'pi pi-fw pi-lock', + to: '/auth/access' + } + ] + }, + { + label: 'Crud', + icon: 'pi pi-fw pi-pencil', + to: '/pages/crud' + }, + { + label: 'Not Found', + icon: 'pi pi-fw pi-exclamation-circle', + to: '/pages/notfound' + }, + { + label: 'Empty', + icon: 'pi pi-fw pi-circle-off', + to: '/pages/empty' + } + ] + }, + { + label: 'Hierarchy', + items: [ + { + label: 'Submenu 1', + icon: 'pi pi-fw pi-bookmark', + items: [ + { + label: 'Submenu 1.1', + icon: 'pi pi-fw pi-bookmark', + items: [ + { label: 'Submenu 1.1.1', icon: 'pi pi-fw pi-bookmark' }, + { label: 'Submenu 1.1.2', icon: 'pi pi-fw pi-bookmark' }, + { label: 'Submenu 1.1.3', icon: 'pi pi-fw pi-bookmark' } + ] + }, + { + label: 'Submenu 1.2', + icon: 'pi pi-fw pi-bookmark', + items: [{ label: 'Submenu 1.2.1', icon: 'pi pi-fw pi-bookmark' }] + } + ] + }, + { + label: 'Submenu 2', + icon: 'pi pi-fw pi-bookmark', + items: [ + { + label: 'Submenu 2.1', + icon: 'pi pi-fw pi-bookmark', + items: [ + { label: 'Submenu 2.1.1', icon: 'pi pi-fw pi-bookmark' }, + { label: 'Submenu 2.1.2', icon: 'pi pi-fw pi-bookmark' } + ] + }, + { + label: 'Submenu 2.2', + icon: 'pi pi-fw pi-bookmark', + items: [{ label: 'Submenu 2.2.1', icon: 'pi pi-fw pi-bookmark' }] + } + ] + } + ] + }, + { + label: 'Get Started', + items: [ + { + label: 'Documentation', + icon: 'pi pi-fw pi-book', + to: '/documentation' + }, + { + label: 'View Source', + icon: 'pi pi-fw pi-github', + url: 'https://github.com/primefaces/sakai-ng', + target: '_blank' + } + ] + } + ]; + } +} diff --git a/src/layout/appmenuitem.ts b/src/layout/appmenuitem.ts new file mode 100644 index 0000000..f5f3fda --- /dev/null +++ b/src/layout/appmenuitem.ts @@ -0,0 +1,152 @@ +import { ChangeDetectorRef, Component, Host, HostBinding, Input } from '@angular/core'; +import { NavigationEnd, Router, RouterModule } from '@angular/router'; +import { animate, state, style, transition, trigger } from '@angular/animations'; +import { Subscription } from 'rxjs'; +import { filter } from 'rxjs/operators'; +import { AppConfigService } from '@/src/service/appconfigservice'; +import { MenuService } from '@/src/app/layout/app.menu.service'; +import { CommonModule } from '@angular/common'; +import { RippleModule } from 'primeng/ripple'; +import { MenuItem} from 'primeng/api'; + +@Component({ + // eslint-disable-next-line @angular-eslint/component-selector + selector: '[app-menuitem]', + imports: [CommonModule, RouterModule, RippleModule], + template: ` + +
{{item.label}}
+ + + {{item.label}} + + + + + {{item.label}} + + + + +
+ `, + animations: [ + trigger('children', [ + state('collapsed', style({ + height: '0' + })), + state('expanded', style({ + height: '*' + })), + transition('collapsed <=> expanded', animate('400ms cubic-bezier(0.86, 0, 0.07, 1)')) + ]) + ] +}) +export class AppMenuItem { + + @Input() item: MenuItem; + + @Input() index!: number; + + @Input() @HostBinding('class.layout-root-menuitem') root!: boolean; + + @Input() parentKey!: string; + + active = false; + + menuSourceSubscription: Subscription; + + menuResetSubscription: Subscription; + + key: string = ""; + + constructor(public configService: AppConfigService , private cd: ChangeDetectorRef, public router: Router, private menuService: MenuService) { + this.menuSourceSubscription = this.menuService.menuSource$.subscribe(value => { + Promise.resolve(null).then(() => { + if (value.routeEvent) { + this.active = (value.key === this.key || value.key.startsWith(this.key + '-')) ? true : false; + } + else { + if (value.key !== this.key && !value.key.startsWith(this.key + '-')) { + this.active = false; + } + } + }); + }); + + this.menuResetSubscription = this.menuService.resetSource$.subscribe(() => { + this.active = false; + }); + + this.router.events.pipe(filter(event => event instanceof NavigationEnd)) + .subscribe(params => { + if (this.item.routerLink) { + this.updateActiveStateFromRoute(); + } + }); + } + + ngOnInit() { + this.key = this.parentKey ? this.parentKey + '-' + this.index : String(this.index); + + if (this.item.routerLink) { + this.updateActiveStateFromRoute(); + } + } + + updateActiveStateFromRoute() { + let activeRoute = this.router.isActive(this.item.routerLink[0], { paths: 'exact', queryParams: 'ignored', matrixParams: 'ignored', fragment: 'ignored' }); + + if (activeRoute) { + this.menuService.onMenuStateChange({ key: this.key, routeEvent: true }); + } + } + + itemClick(event: Event) { + // avoid processing disabled items + if (this.item.disabled) { + event.preventDefault(); + return; + } + + // execute command + if (this.item.command) { + this.item.command({ originalEvent: event, item: this.item }); + } + + // toggle active state + if (this.item.items) { + this.active = !this.active; + } + + this.menuService.onMenuStateChange({ key: this.key }); + } + + get submenuAnimation() { + return this.root ? 'expanded' : (this.active ? 'expanded' : 'collapsed'); + } + + @HostBinding('class.active-menuitem') + get activeClass() { + return this.active && !this.root; + } + + ngOnDestroy() { + if (this.menuSourceSubscription) { + this.menuSourceSubscription.unsubscribe(); + } + + if (this.menuResetSubscription) { + this.menuResetSubscription.unsubscribe(); + } + } +} diff --git a/src/service/appmenuservice.ts b/src/service/appmenuservice.ts new file mode 100644 index 0000000..fb49ed5 --- /dev/null +++ b/src/service/appmenuservice.ts @@ -0,0 +1,27 @@ +import { Injectable } from '@angular/core'; +import { Subject } from 'rxjs'; + +interface MenuChangeEvent { + key: string; + routeEvent?: boolean; +} + +@Injectable({ + providedIn: 'root' +}) +export class MenuService { + + private menuSource = new Subject(); + private resetSource = new Subject(); + + menuSource$ = this.menuSource.asObservable(); + resetSource$ = this.resetSource.asObservable(); + + onMenuStateChange(event: MenuChangeEvent) { + this.menuSource.next(event); + } + + reset() { + this.resetSource.next(true); + } +}