@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
JavaScript
/**-----------------------------------------------------------------------------------------
* 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"`—Expands only one item at a time. Expanding an item collapses the item that was previously expanded.
* - `"multiple"`—The default mode of the PanelBar.
* Expands more than one item at a time. Items can also be toggled.
* - `"full"`—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']]
}] } });