UNPKG

@progress/kendo-angular-layout

Version:

Kendo UI for Angular Layout Package - a collection of components to create professional application layoyts

644 lines (641 loc) 27.8 kB
/**----------------------------------------------------------------------------------------- * Copyright © 2025 Progress Software Corporation. All rights reserved. * Licensed under commercial license. See LICENSE.md in the project root for more information *-------------------------------------------------------------------------------------------*/ import { Component, ContentChild, ContentChildren, ElementRef, EventEmitter, HostBinding, HostListener, Input, Output, QueryList, ViewChildren, isDevMode } from '@angular/core'; import { LocalizationService, L10N_PREFIX } from '@progress/kendo-angular-l10n'; import { Keys, shouldShowValidationUI, WatermarkOverlayComponent } from '@progress/kendo-angular-common'; import { validatePackage } from '@progress/kendo-licensing'; import { packageMetadata } from '../package-metadata'; import { PanelBarExpandMode } from './panelbar-expand-mode'; import { PanelBarItemComponent } from './panelbar-item.component'; import { PanelBarService } from "./panelbar.service"; import { PanelBarItemTemplateDirective } from "./panelbar-item-template.directive"; import { parsePanelBarItems } from "../common/util"; import { Subscription } from 'rxjs'; import { isFocusable } from './../common/dom-queries'; import { PanelBarSelectEvent, PanelBarExpandEvent, PanelBarCollapseEvent, PanelBarStateChangeEvent } from './events'; import { NgIf, NgFor } from '@angular/common'; import * as i0 from "@angular/core"; import * as i1 from "./panelbar.service"; import * as i2 from "@progress/kendo-angular-l10n"; /** * Represents the [Kendo UI PanelBar component for Angular]({% slug overview_panelbar %}). */ // TODO: add styles as input prop export class PanelBarComponent { localization; /** * Sets the expand mode of the PanelBar through the `PanelBarExpandMode` enum ([see example]({% slug expandmodes_panelbar %})). * * The available modes are: * - `"single"`&mdash;Expands only one item at a time. Expanding an item collapses the item that was previously expanded. * - `"multiple"`&mdash;The default mode of the PanelBar. * Expands more than one item at a time. Items can also be toggled. * - `"full"`&mdash;Expands only one item at a time. * The expanded area occupies the entire height of the PanelBar. Requires you to set the `height` property. */ expandMode = PanelBarExpandMode.Default; /** * Allows the PanelBar to modify the selected state of the items. */ selectable = true; /** * Sets the animate state of the PanelBar ([see example]({% slug animations_panelbar %})). */ animate = true; /** * Sets the height of the component when the `"full"` expand mode is used. * This option is ignored in the `"multiple"` and `"single"` expand modes. */ height = '400px'; /** * When set to `true`, the PanelBar renders the content of all items and they are persisted in the DOM * ([see example]({% slug templates_panelbar %}#toc-collections)). * By default, this option is set to `false`. */ get keepItemContent() { return this._keepItemContent; } set keepItemContent(keepItemContent) { this._keepItemContent = keepItemContent; this.eventService.onKeepContent(keepItemContent); } /** * Sets the items of the PanelBar as an array of `PanelBarItemModel` instances * ([see example]({% slug items_panelbar %})). */ set items(data) { if (data) { this._items = parsePanelBarItems(data); } } get items() { return this._items; } /** * Fires each time the user interacts with a PanelBar item * ([see example]({% slug routing_panelbar %}#toc-getting-the-selected-item)). * The event data contains a collection of all items that are modified. */ stateChange = new EventEmitter(); /** * Fires when an item is about to be selected. * ([see example]({% slug events_panelbar %})) * This event is preventable. If you cancel it, the item will not be selected. */ select = new EventEmitter(); /** * Fires when an item is about to be expanded. * ([see example]({% slug events_panelbar %})) * This event is preventable. If you cancel it, the item will remain collapsed. */ expand = new EventEmitter(); /** * Fires when an item is about to be collapsed. * ([see example]({% slug events_panelbar %})) * This event is preventable. If you cancel it, the item will remain expanded. */ collapse = new EventEmitter(); /** * Fires when the user clicks an item ([see example]({% slug events_panelbar %})). */ itemClick = new EventEmitter(); hostClasses = true; tabIndex = 0; role = 'tree'; activeDescendant = ''; get hostHeight() { return this.expandMode === PanelBarExpandMode.Full ? this.height : 'auto'; } get overflow() { return this.expandMode === PanelBarExpandMode.Full ? 'hidden' : 'visible'; } get dir() { return this.localization.rtl ? 'rtl' : 'ltr'; } template; contentItems; contentChildItems; viewChildItems; /** * @hidden */ showLicenseWatermark = false; allItems; childrenItems; isViewInit = true; focused = false; _items; _keepItemContent = false; elementRef; eventService; keyBindings; subs = new Subscription(); constructor(elementRef, eventService, localization) { this.localization = localization; const isValid = validatePackage(packageMetadata); this.showLicenseWatermark = shouldShowValidationUI(isValid); /* eslint-disable-line*/ this.keyBindings = this.computedKeys; this.elementRef = elementRef; this.eventService = eventService; this.subs.add(this.eventService.children$.subscribe(event => this.onItemAction(event))); this.subs.add(this.eventService.itemClick.subscribe(ev => this.itemClick.emit(ev))); } /** * @hidden */ invertKeys(original, inverted) { return this.localization.rtl ? inverted : original; } get computedKeys() { return { [Keys.Space]: () => this.selectFocusedItem(), [Keys.Enter]: () => this.selectFocusedItem(), [Keys.ArrowUp]: () => this.focusPreviousItem(), [this.invertKeys(Keys.ArrowLeft, Keys.ArrowRight)]: () => this.collapseItem(), [Keys.ArrowDown]: () => this.focusNextItem(), [this.invertKeys(Keys.ArrowRight, Keys.ArrowLeft)]: () => this.expandItem(), [Keys.End]: () => this.focusLastItem(), [Keys.Home]: () => this.focusFirstItem() }; } ngOnDestroy() { this.subs.unsubscribe(); } ngOnInit() { this.subs.add(this.localization.changes.subscribe(() => this.keyBindings = this.computedKeys)); this.eventService.animate = this.animate; this.eventService.expandMode = this.expandMode; } ngAfterViewChecked() { if (this.items) { this.childrenItems = this.viewChildItems.toArray(); this.allItems = this.viewItems; } else { this.childrenItems = this.contentChildItems.toArray(); this.allItems = this.contentItems.toArray(); } if (this.isViewInit && this.childrenItems.length) { this.isViewInit = false; setTimeout(() => this.updateChildrenHeight()); } this.validateConfiguration(); } ngOnChanges(changes) { if (changes['height'] || changes['expandMode'] || changes['items']) { // eslint-disable-line if (this.childrenItems) { setTimeout(this.updateChildrenHeight); } } if (changes['animate']) { this.eventService.animate = this.animate; } if (changes['expandMode']) { this.eventService.expandMode = this.expandMode; } } get templateRef() { return this.template ? this.template.templateRef : undefined; } /** * @hidden */ onComponentClick(event) { const itemClicked = this.visibleItems().some((item) => { return item.header.nativeElement.contains(event.target); }); if (!isFocusable(event.target) && !this.focused && itemClicked) { this.elementRef.nativeElement.focus(); } } /** * @hidden */ onComponentFocus() { this.eventService.onFocus(); this.focused = true; if (this.allItems.length > 0) { const visibleItems = this.visibleItems(); const focusedItems = visibleItems.filter(item => item.focused); if (!focusedItems.length && visibleItems.length > 0) { visibleItems[0].focused = true; this.activeDescendant = visibleItems[0].itemId; } } } /** * @hidden */ onComponentBlur() { this.eventService.onBlur(); this.focused = false; this.activeDescendant = ''; } /** * @hidden */ onComponentKeyDown(event) { if (event.target === this.elementRef.nativeElement) { if (event.keyCode === Keys.Space || event.keyCode === Keys.ArrowUp || event.keyCode === Keys.ArrowDown || event.keyCode === Keys.ArrowLeft || event.keyCode === Keys.ArrowRight || event.keyCode === Keys.Home || event.keyCode === Keys.End || event.keyCode === Keys.PageUp || event.keyCode === Keys.PageDown) { event.preventDefault(); } const handler = this.keyBindings[event.keyCode]; //TODO: check if next item is disabled and skip operation? if (handler) { handler(); } } } /** * @hidden */ emitEvent(event, item) { let eventArgs; switch (event) { case 'select': eventArgs = new PanelBarSelectEvent(); break; case 'collapse': eventArgs = new PanelBarCollapseEvent(); break; default: eventArgs = new PanelBarExpandEvent(); break; } eventArgs.item = item.serialize(); this[event].emit(eventArgs); return eventArgs; } get viewItems() { let treeItems = []; this.viewChildItems.toArray().forEach(item => { treeItems.push(item); treeItems = treeItems.concat(item.subTreeViewItems()); }); return treeItems; } validateConfiguration() { if (isDevMode()) { if (this.items && (this.contentItems && this.contentItems.length > 0)) { throw new Error('Invalid configuration: mixed template components and items property.'); } } } updateChildrenHeight = () => { let childrenHeight = 0; const panelbarHeight = this.elementRef.nativeElement.offsetHeight; const contentOverflow = this.expandMode === PanelBarExpandMode.Full ? 'auto' : 'visible'; this.childrenItems.forEach(item => { childrenHeight += item.headerHeight(); }); this.childrenItems.forEach(item => { item.contentHeight = PanelBarExpandMode.Full === this.expandMode ? (panelbarHeight - childrenHeight) + 'px' : 'auto'; item.contentOverflow = contentOverflow; }); }; onItemAction(item) { if (!item) { return; } const modifiedItems = new Array(); const selectPreventedItems = []; this.allItems .forEach((currentItem) => { let selectedState = currentItem === item; const focusedState = selectedState; selectedState = this.selectable ? selectedState : currentItem.selected; if (currentItem.selected !== selectedState || currentItem.focused !== focusedState) { const isSelectPrevented = selectedState ? this.emitEvent('select', currentItem).isDefaultPrevented() : false; if (!isSelectPrevented) { currentItem.selected = selectedState; currentItem.focused = focusedState; this.activeDescendant = focusedState ? currentItem.itemId : ''; modifiedItems.push(currentItem); } else { selectPreventedItems.push(currentItem); } } }); if (this.expandMode === PanelBarExpandMode.Multiple) { if ((item.hasChildItems || item.hasContent) && !selectPreventedItems.includes(item)) { const isEventPrevented = item.expanded ? this.emitEvent('collapse', item).isDefaultPrevented() : this.emitEvent('expand', item).isDefaultPrevented(); if (!isEventPrevented) { item.expanded = !item.expanded; if (modifiedItems.indexOf(item) < 0) { modifiedItems.push(item); } } } } else { const siblings = item.parent ? item.parent.childrenItems : this.childrenItems; let preventedCollapseItem; const expandedItems = []; if ((item.hasChildItems || item.hasContent) && !selectPreventedItems.includes(item)) { siblings .forEach((currentItem) => { const expandedState = currentItem === item; if (currentItem.expanded !== expandedState) { const isEventPrevented = currentItem.expanded ? this.emitEvent('collapse', currentItem).isDefaultPrevented() : this.emitEvent('expand', currentItem).isDefaultPrevented(); if (!isEventPrevented) { currentItem.expanded = expandedState; if (currentItem.expanded) { expandedItems.push(currentItem); } if (modifiedItems.indexOf(currentItem) < 0) { modifiedItems.push(currentItem); } } else if (isEventPrevented && currentItem.expanded) { preventedCollapseItem = currentItem; } } else if (currentItem.expanded === expandedState && expandedState) { const isCollapsePrevented = this.emitEvent('collapse', currentItem).isDefaultPrevented(); if (!isCollapsePrevented) { currentItem.expanded = !currentItem.expanded; if (modifiedItems.indexOf(currentItem) < 0) { modifiedItems.push(currentItem); } } } }); expandedItems.forEach(item => { if (preventedCollapseItem && item.id !== preventedCollapseItem.id) { item.expanded = false; if (isDevMode()) { const expandMode = PanelBarExpandMode[this.expandMode].toLowerCase(); console.warn(` The ${expandMode} expandMode allows the expansion of only one item at a time. See https://www.telerik.com/kendo-angular-ui-develop/components/layout/panelbar/expand-modes/`); } } }); } } if (modifiedItems.length > 0) { const eventArgs = new PanelBarStateChangeEvent(); eventArgs.items = modifiedItems.map(currentItem => currentItem.serialize()); this.stateChange.emit(eventArgs); } } isVisible(item) { const visibleItems = this.visibleItems(); return visibleItems.some(i => i === item); } getVisibleParent(item) { const visibleItems = this.visibleItems(); if (!item.parent) { return item; } return visibleItems.some(i => i === item.parent) ? item.parent : this.getVisibleParent(item.parent); } focusItem(action) { const visibleItems = this.visibleItems(); let currentIndex = visibleItems.findIndex(item => item.focused); let currentItem = visibleItems[currentIndex]; let nextItem; if (currentIndex === -1) { const focusedItem = this.allItems.find(item => item.focused); focusedItem.focused = false; currentItem = this.getVisibleParent(focusedItem); currentIndex = visibleItems.findIndex(item => item === currentItem); } switch (action) { case 'lastItem': nextItem = visibleItems[visibleItems.length - 1]; break; case 'firstItem': nextItem = visibleItems[0]; break; case 'nextItem': nextItem = visibleItems[currentIndex < visibleItems.length - 1 ? currentIndex + 1 : 0]; break; case 'previousItem': nextItem = visibleItems[currentIndex > 0 ? currentIndex - 1 : visibleItems.length - 1]; break; default: } if (currentItem && nextItem && currentItem !== nextItem) { this.moveFocus(currentItem, nextItem); } } moveFocus(from, to) { from.focused = false; to.focused = true; this.activeDescendant = to.itemId; const modifiedItems = new Array(from.serialize(), to.serialize()); const eventArgs = new PanelBarStateChangeEvent(); eventArgs.items = modifiedItems; this.stateChange.emit(eventArgs); } focusLastItem() { this.focusItem('lastItem'); } focusFirstItem() { this.focusItem('firstItem'); } focusNextItem() { this.focusItem('nextItem'); } focusPreviousItem() { this.focusItem('previousItem'); } expandItem() { let currentItem = this.allItems.filter(item => item.focused)[0]; if (!this.isVisible(currentItem)) { currentItem.focused = false; currentItem = this.getVisibleParent(currentItem); } if (currentItem.hasChildItems || currentItem.hasContent) { if (!currentItem.expanded) { this.onItemAction(currentItem); } else if (currentItem.hasChildItems) { const firstChildIndex = currentItem.childrenItems.findIndex(item => !item.disabled); if (firstChildIndex > -1) { this.moveFocus(currentItem, currentItem.childrenItems[firstChildIndex]); } } } } collapseItem() { const currentItem = this.allItems.filter(item => item.focused)[0]; if (currentItem.expanded) { this.onItemAction(currentItem); } else if (currentItem.parent) { this.moveFocus(currentItem, currentItem.parent); } } selectFocusedItem() { let focusedItem = this.allItems.filter(item => item.focused)[0]; if (!this.isVisible(focusedItem)) { focusedItem.focused = false; focusedItem = this.getVisibleParent(focusedItem); } if (focusedItem) { focusedItem.onItemAction(); } } visibleItems() { return this.flatVisibleItems(this.childrenItems); } flatVisibleItems(listOfItems = new Array(), flattedItems = new Array()) { listOfItems.forEach(item => { flattedItems.push(item); if (item.expanded && item.hasChildItems) { this.flatVisibleItems(item.childrenItems, flattedItems); } }); return flattedItems; } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: PanelBarComponent, deps: [{ token: i0.ElementRef }, { token: i1.PanelBarService }, { token: i2.LocalizationService }], target: i0.ɵɵFactoryTarget.Component }); static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "16.2.12", type: PanelBarComponent, isStandalone: true, selector: "kendo-panelbar", inputs: { expandMode: "expandMode", selectable: "selectable", animate: "animate", height: "height", keepItemContent: "keepItemContent", items: "items" }, outputs: { stateChange: "stateChange", select: "select", expand: "expand", collapse: "collapse", itemClick: "itemClick" }, host: { listeners: { "click": "onComponentClick($event)", "focus": "onComponentFocus()", "blur": "onComponentBlur()", "keydown": "onComponentKeyDown($event)" }, properties: { "class.k-panelbar": "this.hostClasses", "attr.tabIndex": "this.tabIndex", "attr.role": "this.role", "attr.aria-activedescendant": "this.activeDescendant", "style.height": "this.hostHeight", "style.overflow": "this.overflow", "attr.dir": "this.dir" } }, providers: [ PanelBarService, LocalizationService, { provide: L10N_PREFIX, useValue: 'kendo.panelbar' } ], queries: [{ propertyName: "template", first: true, predicate: PanelBarItemTemplateDirective, descendants: true }, { propertyName: "contentItems", predicate: PanelBarItemComponent, descendants: true }, { propertyName: "contentChildItems", predicate: PanelBarItemComponent }], viewQueries: [{ propertyName: "viewChildItems", predicate: PanelBarItemComponent, descendants: true }], exportAs: ["kendoPanelbar"], usesOnChanges: true, ngImport: i0, template: ` <ng-content *ngIf="contentChildItems && !items" select="kendo-panelbar-item"></ng-content> <ng-template [ngIf]="items?.length"> <ng-container *ngFor="let item of items"> <kendo-panelbar-item *ngIf="!item.hidden" [title]="item.title" [id]="item.id" [icon]="item.icon" [iconClass]="item.iconClass" [svgIcon]="item.svgIcon" [imageUrl]="item.imageUrl" [selected]="!!item.selected" [expanded]="!!item.expanded" [disabled]="!!item.disabled" [template]="templateRef" [items]="item.children" [content]="item.content" > </kendo-panelbar-item> </ng-container> </ng-template> <div kendoWatermarkOverlay *ngIf="showLicenseWatermark"></div> `, isInline: true, dependencies: [{ kind: "directive", type: NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: NgFor, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "component", type: PanelBarItemComponent, selector: "kendo-panelbar-item", inputs: ["title", "id", "icon", "iconClass", "svgIcon", "imageUrl", "disabled", "expanded", "selected", "content", "items", "template"], exportAs: ["kendoPanelbarItem"] }, { kind: "component", type: WatermarkOverlayComponent, selector: "div[kendoWatermarkOverlay]" }] }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: PanelBarComponent, decorators: [{ type: Component, args: [{ exportAs: 'kendoPanelbar', providers: [ PanelBarService, LocalizationService, { provide: L10N_PREFIX, useValue: 'kendo.panelbar' } ], selector: 'kendo-panelbar', template: ` <ng-content *ngIf="contentChildItems && !items" select="kendo-panelbar-item"></ng-content> <ng-template [ngIf]="items?.length"> <ng-container *ngFor="let item of items"> <kendo-panelbar-item *ngIf="!item.hidden" [title]="item.title" [id]="item.id" [icon]="item.icon" [iconClass]="item.iconClass" [svgIcon]="item.svgIcon" [imageUrl]="item.imageUrl" [selected]="!!item.selected" [expanded]="!!item.expanded" [disabled]="!!item.disabled" [template]="templateRef" [items]="item.children" [content]="item.content" > </kendo-panelbar-item> </ng-container> </ng-template> <div kendoWatermarkOverlay *ngIf="showLicenseWatermark"></div> `, standalone: true, imports: [NgIf, NgFor, PanelBarItemComponent, WatermarkOverlayComponent] }] }], ctorParameters: function () { return [{ type: i0.ElementRef }, { type: i1.PanelBarService }, { type: i2.LocalizationService }]; }, propDecorators: { expandMode: [{ type: Input }], selectable: [{ type: Input }], animate: [{ type: Input }], height: [{ type: Input }], keepItemContent: [{ type: Input }], items: [{ type: Input }], stateChange: [{ type: Output }], select: [{ type: Output }], expand: [{ type: Output }], collapse: [{ type: Output }], itemClick: [{ type: Output }], hostClasses: [{ type: HostBinding, args: ['class.k-panelbar'] }], tabIndex: [{ type: HostBinding, args: ['attr.tabIndex'] }], role: [{ type: HostBinding, args: ['attr.role'] }], activeDescendant: [{ type: HostBinding, args: ['attr.aria-activedescendant'] }], hostHeight: [{ type: HostBinding, args: ['style.height'] }], overflow: [{ type: HostBinding, args: ['style.overflow'] }], dir: [{ type: HostBinding, args: ['attr.dir'] }], template: [{ type: ContentChild, args: [PanelBarItemTemplateDirective, { static: false }] }], contentItems: [{ type: ContentChildren, args: [PanelBarItemComponent, { descendants: true }] }], contentChildItems: [{ type: ContentChildren, args: [PanelBarItemComponent] }], viewChildItems: [{ type: ViewChildren, args: [PanelBarItemComponent] }], onComponentClick: [{ type: HostListener, args: ['click', ['$event']] }], onComponentFocus: [{ type: HostListener, args: ['focus'] }], onComponentBlur: [{ type: HostListener, args: ['blur'] }], onComponentKeyDown: [{ type: HostListener, args: ['keydown', ['$event']] }] } });