@progress/kendo-angular-navigation
Version:
Kendo UI Navigation for Angular
361 lines (360 loc) • 15.6 kB
JavaScript
/**-----------------------------------------------------------------------------------------
* Copyright © 2025 Progress Software Corporation. All rights reserved.
* Licensed under commercial license. See LICENSE.md in the project root for more information
*-------------------------------------------------------------------------------------------*/
/* eslint-disable @typescript-eslint/no-inferrable-types */
/* eslint-disable @typescript-eslint/no-explicit-any */
import { Component, ContentChild, Input, Output, EventEmitter, ViewChild, HostBinding, ElementRef, ChangeDetectorRef, NgZone, ViewChildren, QueryList, isDevMode, Renderer2 } from '@angular/core';
import { Subscription, ReplaySubject, merge, Subject } from 'rxjs';
import { filter, map, share, startWith } from 'rxjs/operators';
import { ResizeSensorComponent, isDocumentAvailable } from '@progress/kendo-angular-common';
import { validatePackage } from '@progress/kendo-licensing';
import { packageMetadata } from '../package-metadata';
import { LocalizationService, L10N_PREFIX } from '@progress/kendo-angular-l10n';
import { BreadCrumbItemTemplateDirective } from './template-directives/item-template.directive';
import { outerWidth } from '../common/util';
import { BreadCrumbListComponent } from './list.component';
import { collapsed, expanded, collapseFirst, expandFirst } from './util';
import { DEFAULT_SIZE, getStylingClasses } from './models/constants';
import { NgIf, NgClass, AsyncPipe } from '@angular/common';
import * as i0 from "@angular/core";
import * as i1 from "@progress/kendo-angular-l10n";
/**
* Represents the [Kendo UI Breadcrumb component for Angular](slug:overview_breadcrumb).
*
* Use the Breadcrumb component to allow users to navigate through a hierarchical structure. The component automatically handles overflow
* scenarios and supports customizable separators, templates, and collapse modes.
*
* @example
* ```typescript
* @Component({
* selector: 'my-app',
* template: `
* <kendo-breadcrumb
* [items]="items"
* (itemClick)="onItemClick($event)">
* </kendo-breadcrumb>
* `
* })
* class AppComponent {
* public items: BreadCrumbItem[] = [
* { text: 'Home', title: 'Home', icon: 'home' },
* { text: 'Kids', title: 'Kids' },
* { text: '8y-16y', title: '8y-16y', disabled: true },
* { text: 'New collection', title: 'New collection' },
* { text: 'Jeans', title: 'Jeans' }
* ];
*
* public onItemClick(item: BreadCrumbItem): void {
* console.log(item);
* }
* }
* ```
*/
export class BreadCrumbComponent {
localization;
el;
cdr;
zone;
renderer;
/**
* Configures the collection of items that will be rendered in the Breadcrumb.
*/
set items(items) {
this._items = items || [];
this.updateItems.next(this._items);
}
get items() {
return this._items;
}
/**
* Specifies the name of a [built-in font icon](slug:icon_list) in a Kendo UI theme to be rendered as a separator.
*/
separatorIcon;
/**
* Defines an [`SVGIcon`](slug:api_icons_svgicon) to be rendered as a separator.
*/
separatorSVGIcon;
/**
* Controls the collapse mode of the Breadcrumb.
* For more information and example, refer to the [Collapse Modes]({% slug collapse_modes_breadcrumb %}) article.
*
* @default `auto`
*/
set collapseMode(mode) {
if (isDevMode() && ['auto', 'wrap', 'none'].indexOf(mode) < 0) {
throw new Error('Invalid collapse mode. Allowed values are "auto", "wrap" or "none". \nFor more details see https://www.telerik.com/kendo-angular-ui/components/navigation/api/BreadCrumbCollapseMode/');
}
this._collapseMode = mode || 'auto';
this.updateItems.next(this.items);
}
get collapseMode() {
return this._collapseMode;
}
/**
* Determines the padding of all Breadcrumb elements.
*
* @default `medium`
*/
set size(size) {
const newSize = size ? size : DEFAULT_SIZE;
this.handleClasses(newSize, 'size');
this._size = newSize;
}
get size() {
return this._size;
}
/**
* Fires when you click a Breadcrumb item. The event will not be fired by disabled items and the last item.
*/
itemClick = new EventEmitter();
/**
* @hidden
*/
resizeSensor;
/**
* @hidden
*/
itemsContainers;
/**
* @hidden
*/
listComponent;
/**
* @hidden
*/
itemTemplate;
hostClasses = true;
get wrapMode() {
return this.collapseMode === 'wrap';
}
hostAriaLabel = 'Breadcrumb';
get getDir() {
return this.direction;
}
itemsData$;
firstItem$;
_items = [];
_collapseMode = 'auto';
_size = DEFAULT_SIZE;
updateItems = new ReplaySubject();
afterViewInit = new Subject();
subscriptions = new Subscription();
direction = 'ltr';
constructor(localization, el, cdr, zone, renderer) {
this.localization = localization;
this.el = el;
this.cdr = cdr;
this.zone = zone;
this.renderer = renderer;
validatePackage(packageMetadata);
const updateItems$ = this.updateItems.asObservable().pipe(startWith([]));
this.direction = localization.rtl ? 'rtl' : 'ltr';
this.itemsData$ = updateItems$.pipe(map(items => items.filter(Boolean)), map(items => items.map((item, index, collection) => ({
context: {
collapsed: false,
isLast: index === collection.length - 1,
isFirst: index === 0
},
data: item
}))), share());
this.firstItem$ = updateItems$.pipe(map(items => {
if (items.length > 0) {
return [
{
context: {
collapsed: false,
isLast: items.length === 1,
isFirst: true
},
data: items[0]
}
];
}
return [];
}), share());
}
ngOnInit() {
this.subscriptions.add(this.localization.changes.subscribe(({ rtl }) => (this.direction = rtl ? 'rtl' : 'ltr')));
this.handleClasses(this.size, 'size');
}
ngAfterViewInit() {
this.attachResizeHandler();
this.afterViewInit.next();
}
ngOnDestroy() {
this.subscriptions.unsubscribe();
}
handleResize() {
const autoCollapseCandidates = [
...this.listComponent.renderedItems.toArray().filter(ri => !ri.item.context.isFirst && !ri.item.context.isLast)
];
const componentWidth = Math.floor(outerWidth(this.el.nativeElement));
const itemsContainerWidth = Math.round(this.itemsContainers
.toArray()
.map(el => outerWidth(el.nativeElement))
.reduce((acc, curr) => acc + curr, 0));
const nextExpandWidth = Math.ceil(([...autoCollapseCandidates].reverse().find(collapsed) || { width: 0 }).width);
// // shrink
if (componentWidth <= itemsContainerWidth && autoCollapseCandidates.find(expanded)) {
collapseFirst(autoCollapseCandidates);
// needed by resize sensor
this.cdr.detectChanges();
return this.handleResize();
}
// expand
if (componentWidth > itemsContainerWidth + nextExpandWidth && autoCollapseCandidates.find(collapsed)) {
expandFirst([...autoCollapseCandidates].reverse());
// needed by resize sensor
this.cdr.detectChanges();
return this.handleResize();
}
}
shouldResize() {
return isDocumentAvailable() && this.collapseMode === 'auto';
}
attachResizeHandler() {
// resize when:
// the component is initialized
// the container is resized
// items are added/removed
this.subscriptions.add(merge(this.resizeSensor.resize, this.itemsData$, this.afterViewInit.asObservable())
.pipe(filter(() => this.shouldResize()))
.subscribe(() => {
this.resizeHandler();
}));
}
handleClasses(value, input) {
const elem = this.el.nativeElement;
const classes = getStylingClasses(input, this[input], value);
if (classes.toRemove) {
this.renderer.removeClass(elem, classes.toRemove);
}
if (classes.toAdd) {
this.renderer.addClass(elem, classes.toAdd);
}
}
resizeHandler = () => {
this.zone.runOutsideAngular(() => setTimeout(() => {
this.zone.run(() => {
if (this.listComponent) {
this.handleResize();
this.resizeSensor.acceptSize();
}
});
}));
};
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: BreadCrumbComponent, deps: [{ token: i1.LocalizationService }, { token: i0.ElementRef }, { token: i0.ChangeDetectorRef }, { token: i0.NgZone }, { token: i0.Renderer2 }], target: i0.ɵɵFactoryTarget.Component });
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "16.2.12", type: BreadCrumbComponent, isStandalone: true, selector: "kendo-breadcrumb", inputs: { items: "items", separatorIcon: "separatorIcon", separatorSVGIcon: "separatorSVGIcon", collapseMode: "collapseMode", size: "size" }, outputs: { itemClick: "itemClick" }, host: { properties: { "class.k-breadcrumb": "this.hostClasses", "class.k-breadcrumb-wrap": "this.wrapMode", "attr.aria-label": "this.hostAriaLabel", "attr.dir": "this.getDir" } }, providers: [
LocalizationService,
{
provide: L10N_PREFIX,
useValue: 'kendo.breadcrumb'
}
], queries: [{ propertyName: "itemTemplate", first: true, predicate: BreadCrumbItemTemplateDirective, descendants: true }], viewQueries: [{ propertyName: "resizeSensor", first: true, predicate: ["resizeSensor"], descendants: true, static: true }, { propertyName: "listComponent", first: true, predicate: BreadCrumbListComponent, descendants: true, static: true }, { propertyName: "itemsContainers", predicate: ["itemsContainer"], descendants: true, read: ElementRef }], exportAs: ["kendoBreadCrumb"], ngImport: i0, template: `
<ol
#itemsContainer
kendoBreadCrumbList
class="k-breadcrumb-root-item-container"
*ngIf="collapseMode === 'wrap'"
[items]="firstItem$ | async"
[itemTemplate]="itemTemplate?.templateRef"
[collapseMode]="collapseMode"
[separatorIcon]="separatorIcon"
[separatorSVGIcon]="separatorSVGIcon"
(itemClick)="itemClick.emit($event)"
></ol>
<ol
#itemsContainer
kendoBreadCrumbList
class="k-breadcrumb-container"
[items]="itemsData$ | async"
[itemTemplate]="itemTemplate?.templateRef"
[collapseMode]="collapseMode"
[separatorIcon]="separatorIcon"
[separatorSVGIcon]="separatorSVGIcon"
(itemClick)="itemClick.emit($event)"
[ngClass]="{ '!k-flex-wrap': collapseMode === 'wrap', 'k-flex-none': collapseMode === 'none' }"
></ol>
<kendo-resize-sensor [rateLimit]="1000" #resizeSensor></kendo-resize-sensor>
`, isInline: true, dependencies: [{ kind: "directive", type: NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: BreadCrumbListComponent, selector: "[kendoBreadCrumbList]", inputs: ["items", "itemTemplate", "collapseMode", "separatorIcon", "separatorSVGIcon"], outputs: ["itemClick"] }, { kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "component", type: ResizeSensorComponent, selector: "kendo-resize-sensor", inputs: ["rateLimit"], outputs: ["resize"] }, { kind: "pipe", type: AsyncPipe, name: "async" }] });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: BreadCrumbComponent, decorators: [{
type: Component,
args: [{
exportAs: 'kendoBreadCrumb',
selector: 'kendo-breadcrumb',
providers: [
LocalizationService,
{
provide: L10N_PREFIX,
useValue: 'kendo.breadcrumb'
}
],
template: `
<ol
#itemsContainer
kendoBreadCrumbList
class="k-breadcrumb-root-item-container"
*ngIf="collapseMode === 'wrap'"
[items]="firstItem$ | async"
[itemTemplate]="itemTemplate?.templateRef"
[collapseMode]="collapseMode"
[separatorIcon]="separatorIcon"
[separatorSVGIcon]="separatorSVGIcon"
(itemClick)="itemClick.emit($event)"
></ol>
<ol
#itemsContainer
kendoBreadCrumbList
class="k-breadcrumb-container"
[items]="itemsData$ | async"
[itemTemplate]="itemTemplate?.templateRef"
[collapseMode]="collapseMode"
[separatorIcon]="separatorIcon"
[separatorSVGIcon]="separatorSVGIcon"
(itemClick)="itemClick.emit($event)"
[ngClass]="{ '!k-flex-wrap': collapseMode === 'wrap', 'k-flex-none': collapseMode === 'none' }"
></ol>
<kendo-resize-sensor [rateLimit]="1000" #resizeSensor></kendo-resize-sensor>
`,
standalone: true,
imports: [NgIf, BreadCrumbListComponent, NgClass, ResizeSensorComponent, AsyncPipe]
}]
}], ctorParameters: function () { return [{ type: i1.LocalizationService }, { type: i0.ElementRef }, { type: i0.ChangeDetectorRef }, { type: i0.NgZone }, { type: i0.Renderer2 }]; }, propDecorators: { items: [{
type: Input
}], separatorIcon: [{
type: Input
}], separatorSVGIcon: [{
type: Input
}], collapseMode: [{
type: Input
}], size: [{
type: Input
}], itemClick: [{
type: Output
}], resizeSensor: [{
type: ViewChild,
args: ['resizeSensor', { static: true }]
}], itemsContainers: [{
type: ViewChildren,
args: ['itemsContainer', { read: ElementRef }]
}], listComponent: [{
type: ViewChild,
args: [BreadCrumbListComponent, { static: true }]
}], itemTemplate: [{
type: ContentChild,
args: [BreadCrumbItemTemplateDirective]
}], hostClasses: [{
type: HostBinding,
args: ['class.k-breadcrumb']
}], wrapMode: [{
type: HostBinding,
args: ['class.k-breadcrumb-wrap']
}], hostAriaLabel: [{
type: HostBinding,
args: ['attr.aria-label']
}], getDir: [{
type: HostBinding,
args: ['attr.dir']
}] } });