UNPKG

@progress/kendo-angular-layout

Version:

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

505 lines (504 loc) 19.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 { AnimationBuilder } from '@angular/animations'; import { isFocusable, hasClass } from './../common/dom-queries'; import { Component, ContentChild, EventEmitter, HostBinding, Input, Output, ElementRef, Renderer2, NgZone, ViewChild, isDevMode } from '@angular/core'; import { ExpansionPanelTitleDirective } from './expansionpanel-title.directive'; import { collapse, expand } from './animations'; import { isPresent } from '../common/util'; import { Subscription } from 'rxjs'; import { Keys } from '@progress/kendo-angular-common'; import { L10N_PREFIX, LocalizationService } from '@progress/kendo-angular-l10n'; import { validatePackage } from '@progress/kendo-licensing'; import { packageMetadata } from '../package-metadata'; import { ExpansionPanelActionEvent } from './events/action-event'; import { take } from 'rxjs/operators'; import { chevronDownIcon, chevronUpIcon } from '@progress/kendo-svg-icons'; import { IconWrapperComponent } from '@progress/kendo-angular-icons'; import { NgIf, NgTemplateOutlet } from '@angular/common'; import * as i0 from "@angular/core"; import * as i1 from "@progress/kendo-angular-l10n"; import * as i2 from "@angular/animations"; const DEFAULT_DURATION = 200; const CONTENT_HIDDEN_CLASS = 'k-hidden'; /** * Represents the [Kendo UI ExpansionPanel component for Angular]({% slug overview_expansionpanel %}). * * @example * ```ts-preview * _@Component({ * selector: 'my-app', * template: ` * <kendo-expansionpanel title="Chile" subtitle="South America"> * There are various theories about the origin of the word Chile. * </kendo-expansionpanel> * ` * }) * class AppComponent {} * ``` */ export class ExpansionPanelComponent { renderer; hostElement; ngZone; localizationService; builder; /** * Specifies the primary text in the header of the ExpansionPanel * ([see example](slug:title_expansionpanel#toc-titles-and-subtitles)). */ title = ''; /** * Specifies the secondary text in the header of the ExpansionPanel, which is rendered next to the collapse/expand icon * ([see example](slug:title_expansionpanel#toc-titles-and-subtitles)). */ subtitle = ''; /** * Specifies whether the ExpansionPanel is disabled. If disabled, the ExpansionPanel can be neither expanded nor collapsed * ([see example]({% slug disabled_expansionpanel %})). * * @default false */ disabled = false; /** * Specifies whether the ExpansionPanel is expanded. The property supports two-way binding. * ([see example]({% slug interaction_expansionpanel %}#toc-setting-the-initial-state)). * * @default false */ set expanded(value) { if (value === this.expanded) { return; } this._expanded = value; if (this.expanded) { this.removeContentHiddenClass(); } else { this.addContentHiddenClass(); } } get expanded() { return this._expanded; } /** * Defines an SVGIcon for the expanded state of the component. * The input can take either an [existing Kendo SVG icon](slug:svgicon_list) or a custom one. */ set svgExpandIcon(icon) { if (isDevMode() && icon && this.expandIcon) { throw new Error('Setting both expandIcon/svgExpandIcon options at the same time is not supported.'); } this._svgExpandIcon = icon; } get svgExpandIcon() { return this._svgExpandIcon; } /** * Defines an SVGIcon for the collapsed state of the component. * The input can take either an [existing Kendo SVG icon](slug:svgicon_list) or a custom one. */ set svgCollapseIcon(icon) { if (isDevMode() && icon && this.collapseIcon) { throw new Error('Setting both collapseIcon/svgCollapseIcon options at the same time is not supported.'); } this._svgCollapseIcon = icon; } get svgCollapseIcon() { return this._svgCollapseIcon; } /** * Sets a custom icon via css class(es), for the collapsed state of the component * ([see example]({% slug icons_expansionpanel %}#toc-icons)). */ expandIcon; /** * Sets a custom icon via css class(es), for the expanded state of the component * ([see example]({% slug icons_expansionpanel %}#toc-icons)). */ collapseIcon; /** * Specifies the animation settings of the ExpansionPanel * ([see example]({% slug animations_expansionpanel %})). * * The possible values are: * * Boolean * * (Default) `true` Numeric values represent duration. Default duration is 200ms. * * false * * Number */ animation = true; /** * Fires when the `expanded` property of the component is updated. * Used to provide a two-way binding for the `expanded` property * ([see example](slug:events_expansionpanel)). */ expandedChange = new EventEmitter(); /** * Fires when the expanded state of the ExpansionPanel is about to change. This event is preventable * ([see example](slug:events_expansionpanel)). */ action = new EventEmitter(); /** * Fires when the ExpansionPanel is expanded. If there is animation it will fire when the animation is complete * ([see example](slug:events_expansionpanel)). */ expand = new EventEmitter(); /** * Fires when the ExpansionPanel is collapsed. If there is animation it will fire when the animation is complete * ([see example](slug:events_expansionpanel)). */ collapse = new EventEmitter(); /** * @hidden */ titleTemplate; content; header; hostClass = true; get expandedClass() { return this.expanded && !this.disabled; } direction; /** * @hidden */ focused = false; animationEnd = new EventEmitter(); subscriptions = new Subscription(); _expanded = false; _svgExpandIcon = chevronDownIcon; _svgCollapseIcon = chevronUpIcon; constructor(renderer, hostElement, ngZone, localizationService, builder) { this.renderer = renderer; this.hostElement = hostElement; this.ngZone = ngZone; this.localizationService = localizationService; this.builder = builder; validatePackage(packageMetadata); this.direction = localizationService.rtl ? 'rtl' : 'ltr'; } ngOnInit() { this.renderer.removeAttribute(this.hostElement.nativeElement, 'title'); this.subscriptions = this.localizationService.changes.subscribe(({ rtl }) => { this.direction = rtl ? 'rtl' : 'ltr'; }); const elem = this.hostElement.nativeElement; const header = this.header.nativeElement; this.subscriptions.add(this.renderer.listen(header, 'focus', () => this.focusExpansionPanel(elem))); this.subscriptions.add(this.renderer.listen(header, 'blur', () => this.blurExpansionPanel(elem))); } ngAfterViewInit() { this.initDomEvents(); if (!this.expanded) { this.renderer.addClass(this.content.nativeElement, CONTENT_HIDDEN_CLASS); } } ngOnDestroy() { this.subscriptions.unsubscribe(); } /** * @hidden */ initDomEvents() { if (!this.hostElement) { return; } this.ngZone.runOutsideAngular(() => { const elem = this.hostElement.nativeElement; this.subscriptions.add(this.renderer.listen(elem, 'keydown', this.keyDownHandler.bind(this))); }); } /** * @hidden */ keyDownHandler(ev) { const isEnterOrSpace = ev.keyCode === Keys.Enter || ev.keyCode === Keys.Space; if (this.disabled || !isEnterOrSpace) { return; } if (hasClass(ev.target, 'k-expander-header')) { ev.preventDefault(); this.ngZone.run(() => { this.onHeaderAction(); }); } } /** * @hidden */ onHeaderClick(ev) { const header = this.header.nativeElement; if (!isFocusable(ev.target) || (ev.target === header) && !this.disabled) { this.onHeaderAction(); } } /** * @hidden */ onHeaderAction() { const eventArgs = new ExpansionPanelActionEvent(); eventArgs.action = this.expanded ? 'collapse' : 'expand'; this.action.emit(eventArgs); if (!eventArgs.isDefaultPrevented()) { this.setExpanded(!this.expanded); if (this.expanded) { this.removeContentHiddenClass(); } if (this.animation) { this.animateContent(); return; } if (!this.expanded) { this.addContentHiddenClass(); } this.emitExpandCollapseEvent(); } } /** * @hidden */ get expanderIndicatorClasses() { if (this.expanded) { return !this.collapseIcon ? `chevron-up` : ''; } else { return !this.expandIcon ? `chevron-down` : ''; } } /** * @hidden */ get customExpanderIndicatorClasses() { if (this.expanded) { return this.collapseIcon ? this.collapseIcon : ''; } else { return this.expandIcon ? this.expandIcon : ''; } } /** * @hidden */ get expanderSvgIcon() { return this.expanded ? this.svgCollapseIcon : this.svgExpandIcon; } /** * Toggles the visibility of the ExpansionPanel * ([see example](slug:interaction_expansionpanel#toggling-between-states)). * * @param expanded? - Boolean. Specifies, whether the ExpansionPanel will be expanded or collapsed. */ toggle(expanded) { const previous = this.expanded; const current = isPresent(expanded) ? expanded : !previous; if (current === previous) { return; } this.setExpanded(current); if (this.expanded) { this.removeContentHiddenClass(); } if (this.animation) { this.animateContent(); return; } if (!this.expanded) { this.addContentHiddenClass(); } this.emitExpandCollapseEvent(); } focusExpansionPanel(el) { if (!this.focused) { this.focused = true; this.renderer.addClass(el, 'k-focus'); } } blurExpansionPanel(el) { if (this.focused) { this.focused = false; this.renderer.removeClass(el, 'k-focus'); } } setExpanded(value) { this._expanded = value; this.expandedChange.emit(value); } animateContent() { const duration = typeof this.animation === 'boolean' ? DEFAULT_DURATION : this.animation; const contentHeight = getComputedStyle(this.content.nativeElement).height; const animation = this.expanded ? expand(duration, contentHeight) : collapse(duration, contentHeight); const player = this.createPlayer(animation, this.content.nativeElement); this.animationEnd.pipe(take(1)).subscribe(() => { if (!this.expanded) { this.addContentHiddenClass(); } this.emitExpandCollapseEvent(); }); player.play(); } createPlayer(animation, animatedElement) { const factory = this.builder.build(animation); let player = factory.create(animatedElement); player.onDone(() => { if (player) { this.animationEnd.emit(); player.destroy(); player = null; } }); return player; } emitExpandCollapseEvent() { this[this.expanded ? 'expand' : 'collapse'].emit(); } addContentHiddenClass() { this.renderer.addClass(this.content.nativeElement, CONTENT_HIDDEN_CLASS); } removeContentHiddenClass() { this.renderer.removeClass(this.content.nativeElement, CONTENT_HIDDEN_CLASS); } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: ExpansionPanelComponent, deps: [{ token: i0.Renderer2 }, { token: i0.ElementRef }, { token: i0.NgZone }, { token: i1.LocalizationService }, { token: i2.AnimationBuilder }], target: i0.ɵɵFactoryTarget.Component }); static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "16.2.12", type: ExpansionPanelComponent, isStandalone: true, selector: "kendo-expansionpanel", inputs: { title: "title", subtitle: "subtitle", disabled: "disabled", expanded: "expanded", svgExpandIcon: "svgExpandIcon", svgCollapseIcon: "svgCollapseIcon", expandIcon: "expandIcon", collapseIcon: "collapseIcon", animation: "animation" }, outputs: { expandedChange: "expandedChange", action: "action", expand: "expand", collapse: "collapse" }, host: { properties: { "class.k-expander": "this.hostClass", "class.k-expanded": "this.expandedClass", "attr.dir": "this.direction" } }, providers: [ LocalizationService, { provide: L10N_PREFIX, useValue: 'kendo.expansionpanel' } ], queries: [{ propertyName: "titleTemplate", first: true, predicate: ExpansionPanelTitleDirective, descendants: true }], viewQueries: [{ propertyName: "content", first: true, predicate: ["content"], descendants: true, static: true }, { propertyName: "header", first: true, predicate: ["header"], descendants: true, static: true }], exportAs: ["kendoExpansionPanel"], ngImport: i0, template: ` <div #header [class.k-expander-header]="true" [class.k-disabled]="disabled" [attr.aria-disabled]="disabled" [attr.aria-expanded]="expanded && !disabled" role="button" tabindex="0" [attr.aria-controls]="title" (click)="onHeaderClick($event)" > <ng-container *ngIf="!titleTemplate"> <div *ngIf="title" class="k-expander-title">{{ title }}</div> <span class="k-spacer"></span> <div *ngIf="subtitle" class="k-expander-sub-title"> {{ subtitle }} </div> </ng-container> <ng-template *ngIf="titleTemplate" [ngTemplateOutlet]="titleTemplate?.templateRef"> </ng-template> <span class="k-expander-indicator"> <kendo-icon-wrapper [name]="expanderIndicatorClasses" [customFontClass]="customExpanderIndicatorClasses" [svgIcon]="expanderSvgIcon" > </kendo-icon-wrapper> </span> </div> <div #content [id]="title" class="k-expander-content-wrapper"> <div class="k-expander-content" [attr.aria-hidden]="!expanded"> <ng-content></ng-content> </div> </div> `, isInline: true, dependencies: [{ kind: "directive", type: NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: IconWrapperComponent, selector: "kendo-icon-wrapper", inputs: ["name", "svgIcon", "innerCssClass", "customFontClass", "size"], exportAs: ["kendoIconWrapper"] }] }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: ExpansionPanelComponent, decorators: [{ type: Component, args: [{ exportAs: 'kendoExpansionPanel', providers: [ LocalizationService, { provide: L10N_PREFIX, useValue: 'kendo.expansionpanel' } ], selector: 'kendo-expansionpanel', template: ` <div #header [class.k-expander-header]="true" [class.k-disabled]="disabled" [attr.aria-disabled]="disabled" [attr.aria-expanded]="expanded && !disabled" role="button" tabindex="0" [attr.aria-controls]="title" (click)="onHeaderClick($event)" > <ng-container *ngIf="!titleTemplate"> <div *ngIf="title" class="k-expander-title">{{ title }}</div> <span class="k-spacer"></span> <div *ngIf="subtitle" class="k-expander-sub-title"> {{ subtitle }} </div> </ng-container> <ng-template *ngIf="titleTemplate" [ngTemplateOutlet]="titleTemplate?.templateRef"> </ng-template> <span class="k-expander-indicator"> <kendo-icon-wrapper [name]="expanderIndicatorClasses" [customFontClass]="customExpanderIndicatorClasses" [svgIcon]="expanderSvgIcon" > </kendo-icon-wrapper> </span> </div> <div #content [id]="title" class="k-expander-content-wrapper"> <div class="k-expander-content" [attr.aria-hidden]="!expanded"> <ng-content></ng-content> </div> </div> `, standalone: true, imports: [NgIf, NgTemplateOutlet, IconWrapperComponent] }] }], ctorParameters: function () { return [{ type: i0.Renderer2 }, { type: i0.ElementRef }, { type: i0.NgZone }, { type: i1.LocalizationService }, { type: i2.AnimationBuilder }]; }, propDecorators: { title: [{ type: Input }], subtitle: [{ type: Input }], disabled: [{ type: Input }], expanded: [{ type: Input }], svgExpandIcon: [{ type: Input }], svgCollapseIcon: [{ type: Input }], expandIcon: [{ type: Input }], collapseIcon: [{ type: Input }], animation: [{ type: Input }], expandedChange: [{ type: Output }], action: [{ type: Output }], expand: [{ type: Output }], collapse: [{ type: Output }], titleTemplate: [{ type: ContentChild, args: [ExpansionPanelTitleDirective, { static: false }] }], content: [{ type: ViewChild, args: ['content', { static: true }] }], header: [{ type: ViewChild, args: ['header', { static: true }] }], hostClass: [{ type: HostBinding, args: ['class.k-expander'] }], expandedClass: [{ type: HostBinding, args: ['class.k-expanded'] }], direction: [{ type: HostBinding, args: ['attr.dir'] }] } });