UNPKG

@progress/kendo-angular-navigation

Version:

Kendo UI Navigation for Angular

361 lines (360 loc) 15.6 kB
/**----------------------------------------------------------------------------------------- * 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'] }] } });