UNPKG

@deepkit/desktop-ui

Version:

Library for desktop UI widgets in Angular 10+

242 lines (208 loc) 6.79 kB
/* * Deepkit Framework * Copyright (C) 2021 Deepkit UG, Marc J. Schmidt * * This program is free software: you can redistribute it and/or modify * it under the terms of the MIT License. * * You should have received a copy of the MIT License along with this program. */ import { ChangeDetectorRef, Component, ElementRef, EventEmitter, HostBinding, HostListener, Injectable, Injector, Input, OnChanges, OnDestroy, Optional, Output, SimpleChanges, SkipSelf, } from '@angular/core'; import { ActivatedRoute, NavigationEnd, Router, UrlTree } from '@angular/router'; import { ngValueAccessor, ValueAccessorBase } from '../../core/form'; import { Subscription } from 'rxjs'; import { arrayRemoveItem } from '@deepkit/core'; import { isRouteActive } from '../../core/utils'; @Component({ selector: 'dui-list-title', standalone: false, template: ` <ng-content></ng-content>`, styleUrls: ['./list-title.component.scss'], }) export class ListTitleComponent { constructor() { } } @Component({ selector: 'dui-list', standalone: false, template: ` <ng-content></ng-content> `, styleUrls: ['./list.component.scss'], host: { '[class.white]': 'white !== false', '[class.overlay-scrollbar-small]': 'true', '[class.focusable]': 'focusable', '[class.delimiter-line]': 'delimiterLine !== false', }, providers: [ngValueAccessor(ListComponent)], }) @Injectable() export class ListComponent extends ValueAccessorBase<any> { static ids: number = 0; @Input() white: boolean | '' = false; id = ++ListComponent.ids; @Input() focusable: boolean = true; @Input() delimiterLine: boolean | '' = false; @HostBinding('tabindex') tabIndex: number = 1; items: ListItemComponent[] = []; protected itemMap = new Map<string, ListItemComponent>(); constructor( protected injector: Injector, public readonly cd: ChangeDetectorRef, public host: ElementRef<HTMLElement>, @SkipSelf() public readonly cdParent: ChangeDetectorRef, ) { super(injector, cd, cdParent); } public deregister(item: ListItemComponent) { arrayRemoveItem(this.items, item); this.itemMap.delete(item.id + ''); } public register(item: ListItemComponent) { this.items.push(item); this.itemMap.set(item.id + '', item); } protected getSortedList(): ListItemComponent[] { const list = Array.from(this.host.nativeElement.querySelectorAll(`dui-list-item[list-id="${this.id}"]`)); return list.map(v => this.itemMap.get(v.getAttribute('list-item-id')!)!); } @HostListener('keydown', ['$event']) public async onKeyDown(event: KeyboardEvent) { if (event.key === 'ArrowDown') { event.preventDefault(); const selectedItem = this.getSelectedItem(); if (selectedItem) { const items = this.getSortedList(); const position = items.indexOf(selectedItem); if (items[position + 1]) { await items[position + 1].select(); } } } if (event.key === 'ArrowUp') { event.preventDefault(); const selectedItem = this.getSelectedItem(); if (selectedItem) { const items = this.getSortedList(); const position = items.indexOf(selectedItem); if (items[position - 1]) { await items[position - 1].select(); } } } } public getSelectedItem(): ListItemComponent | undefined { for (const item of this.items) { if (item.isSelected()) { return item; } } return; } } @Component({ selector: 'dui-list-item', standalone: false, template: ` <ng-content></ng-content> `, host: { '[class.selected]': 'isSelected()', '[attr.list-id]': 'list.id', '[attr.list-item-id]': 'id', }, styleUrls: ['./list-item.component.scss'], }) export class ListItemComponent implements OnChanges, OnDestroy { static ids: number = 0; id = ++ListItemComponent.ids; @Input() value: any; @Input() routerLink?: string | UrlTree | any[]; @Input() routerLinkExact?: boolean; @Input() queryParams?: { [name: string]: any }; @Input() active?: boolean; /** * When position is dynamic, it might be handy to define the position * explicitly to make arrow-up/arrow-down navigation possible. */ @Input() position: number = 0; @Output() onSelect = new EventEmitter<any>(); protected routerSub?: Subscription; constructor( public list: ListComponent, public element: ElementRef, @SkipSelf() public cd: ChangeDetectorRef, @Optional() public router?: Router, @Optional() activatedRoute?: ActivatedRoute, ) { this.element.nativeElement.removeAttribute('tabindex'); list.register(this); this.list.registerOnChange(() => { this.cd.detectChanges(); }); if (this.router) { this.routerSub = this.router.events.subscribe((event) => { if (event instanceof NavigationEnd) { this.cd.detectChanges(); } }); } } ngOnDestroy(): void { this.list.deregister(this); if (this.routerSub) { this.routerSub.unsubscribe(); } } ngOnChanges(changes: SimpleChanges): void { this.cd.detectChanges(); } public async select() { if (this.routerLink && this.router) { if ('string' === typeof this.routerLink) { await this.router.navigateByUrl(this.routerLink); } else if (Array.isArray(this.routerLink)) { await this.router.navigate(this.routerLink, { queryParams: this.queryParams }); } else { await this.router.navigateByUrl(this.router.serializeUrl(this.routerLink!)); } } else { this.list.innerValue = this.value; } this.onSelect.emit(this.value); } public isSelected(): boolean { if (this.active !== undefined) return this.active; if (this.value !== undefined) { return this.list.innerValue === this.value; } if (this.routerLink && this.router) { return isRouteActive(this); } return false; } @HostListener('mousedown') public onClick() { this.list.innerValue = this.value; this.onSelect.emit(this.value); } }