UNPKG

@progress/kendo-angular-buttons

Version:
457 lines (456 loc) 16.9 kB
/**----------------------------------------------------------------------------------------- * Copyright © 2025 Progress Software Corporation. All rights reserved. * Licensed under commercial license. See LICENSE.md in the project root for more information *-------------------------------------------------------------------------------------------*/ import { EventEmitter, ElementRef, NgZone, ChangeDetectorRef, Component, Input, ViewContainerRef, Output, ViewChild, TemplateRef } from '@angular/core'; import { Subscription, fromEvent, merge } from 'rxjs'; import { filter, tap } from 'rxjs/operators'; import { FocusService } from './../focusable/focus.service'; import { KeyEvents } from './../navigation/key-events'; import { NavigationService } from './../navigation/navigation.service'; import { NavigationAction } from './../navigation/navigation-action'; import { isDocumentAvailable, guid, Keys, isChanged, hasObservers } from '@progress/kendo-angular-common'; import { LocalizationService } from '@progress/kendo-angular-l10n'; import { validatePackage } from '@progress/kendo-licensing'; import { packageMetadata } from '../package-metadata'; import { PreventableEvent } from '../preventable-event'; import { PopupService } from '@progress/kendo-angular-popup'; import { isPresent } from '../util'; import { PopupContainerService } from './container.service'; import { MultiTabStop } from '@progress/kendo-angular-common'; import { ListComponent } from './list.component'; import * as i0 from "@angular/core"; import * as i1 from "./../focusable/focus.service"; import * as i2 from "./../navigation/navigation.service"; import * as i3 from "@progress/kendo-angular-popup"; import * as i4 from "@progress/kendo-angular-l10n"; import * as i5 from "./container.service"; /** * @hidden */ export class ListButton extends MultiTabStop { focusService; navigationService; wrapperRef; _zone; popupService; elRef; cdr; containerService; listId = guid(); buttonId = guid(); _data; _open = false; _disabled = false; _active = false; _popupSettings = { animate: true, popupClass: '' }; _isFocused = false; _itemClick; _blur; wrapper; subs = new Subscription(); direction; popupRef; popupSubs = new Subscription(); button; buttonList; popupTemplate; container; /** * Sets the disabled state of the DropDownButton. */ set disabled(value) { if (value && this.openState) { this.openState = false; } this._disabled = value; } get disabled() { return this._disabled; } /** * Specifies the [`tabIndex`](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/tabindex) of the component. */ tabIndex = 0; /** * The CSS classes that will be rendered on the main button. * Supports the type of values that are supported by [`ngClass`](link:site.data.urls.angular['ngclassapi']). */ buttonClass; /** * Fires each time the popup is about to open. * This event is preventable. If you cancel the event, the popup will remain closed. */ open = new EventEmitter(); /** * Fires each time the popup is about to close. * This event is preventable. If you cancel the event, the popup will remain open. */ close = new EventEmitter(); /** * Needed by the kendoToggleButtonTabStop directive * * @hidden */ escape = new EventEmitter(); /** * @hidden */ get componentTabIndex() { return this.disabled ? (-1) : this.tabIndex; } get appendTo() { const { appendTo } = this.popupSettings; if (!appendTo || appendTo === 'root') { return undefined; } return appendTo === 'component' ? this.containerService.container : appendTo; } /** * Configures the popup of the DropDownButton. * * The available options are: * - `animate: Boolean`—Controls the popup animation. By default, the open and close animations are enabled. * - `popupClass: String`—Specifies a list of CSS classes that are used to style the popup. * - `appendTo: "root" | "component" | ViewContainerRef`—Specifies the component to which the popup will be appended. * - `align: "left" | "center" | "right"`—Specifies the alignment of the popup. */ set popupSettings(settings) { this._popupSettings = Object.assign({ animate: true, popupClass: '' }, settings); } get popupSettings() { return this._popupSettings; } /** * @hidden */ get anchorAlign() { const align = { horizontal: this.popupSettings.align || 'left', vertical: 'bottom' }; if (this.direction === 'rtl' && !isPresent(this.popupSettings.align)) { align.horizontal = 'right'; } return align; } /** * @hidden */ get popupAlign() { const align = { horizontal: this.popupSettings.align || 'left', vertical: 'top' }; if (this.direction === 'rtl' && !isPresent(this.popupSettings.align)) { align.horizontal = 'right'; } return align; } isClosePrevented = false; constructor(focusService, navigationService, wrapperRef, _zone, popupService, elRef, localization, cdr, containerService) { super(); this.focusService = focusService; this.navigationService = navigationService; this.wrapperRef = wrapperRef; this._zone = _zone; this.popupService = popupService; this.elRef = elRef; this.cdr = cdr; this.containerService = containerService; validatePackage(packageMetadata); this.focusService = focusService; this.navigationService = navigationService; this.wrapper = wrapperRef.nativeElement; this.subs.add(localization.changes.subscribe(({ rtl }) => (this.direction = rtl ? 'rtl' : 'ltr'))); this.subscribeEvents(); } ngOnChanges(changes) { if (isChanged("popupSettings", changes) && isPresent(this.popupRef)) { const popup = this.popupRef.popup.instance; const newSettings = changes['popupSettings'].currentValue; popup.popupClass = newSettings.popupClass; popup.animate = newSettings.animate; popup.popupAlign = this.popupAlign; } } get popupClasses() { const popupClasses = ['k-menu-popup']; if (this._popupSettings.popupClass) { popupClasses.push(this._popupSettings.popupClass); } return popupClasses.join(' '); } get openState() { return this._open; } /** * @hidden */ set openState(open) { if (this.disabled) { return; } this._open = open; } /** * Returns the current open state of the popup. */ get isOpen() { return this._open; } /** * @hidden */ togglePopupVisibility() { if (this._disabled) { return; } this._toggle(!this.openState, true); if (!this.isClosePrevented) { this.focusService.focus(this.openState ? 0 : -1); } } /** * @hidden */ onItemClick(index) { this.emitItemClickHandler(index); this.togglePopupVisibility(); if (isDocumentAvailable() && !this.isClosePrevented) { this.focusButton(); } } ngOnDestroy() { this.openState = false; this.subs.unsubscribe(); this.destroyPopup(); } subscribeEvents() { if (!isDocumentAvailable()) { return; } this.subscribeListItemFocusEvent(); this.subscribeComponentBlurredEvent(); this.subscribeNavigationEvents(); } subscribeListItemFocusEvent() { this.subs.add(this.focusService.onFocus.subscribe(() => { this._isFocused = true; })); } subscribeComponentBlurredEvent() { this._zone.runOutsideAngular(() => { this.subs.add(this.navigationService.tab.pipe(filter(() => this._isFocused), tap(() => this.focusButton())).subscribe(this.handleTab.bind(this))); this.subs.add(fromEvent(document, 'click') .pipe(filter((event) => !this.wrapperContains(event.target)), filter(() => this._isFocused)) .subscribe(() => this._zone.run(() => this.blurWrapper()))); }); } subscribeNavigationEvents() { this.subs.add(this.navigationService.navigate .subscribe(this.onArrowKeyNavigate.bind(this))); this.subs.add(this.navigationService.enterup.subscribe(this.onNavigationEnterUp.bind(this))); this.subs.add(this.navigationService.open.subscribe(this.onNavigationOpen.bind(this))); this.subs.add(merge(this.navigationService.close, this.navigationService.esc).subscribe(this.onNavigationClose.bind(this))); } /** * Toggles the visibility of the popup. * If the `toggle` method is used to open or close the popup, the `open` and `close` events will not be fired. * * @param open - The state of the popup. */ toggle(open) { if (this.disabled) { return; } const value = open === undefined ? !this.openState : open; this._toggle(value, false); } /** * @hidden */ keyDownHandler(event, isHost) { this.keyHandler(event, null, isHost); } /** * @hidden */ keyUpHandler(event) { this.keyHandler(event, KeyEvents.keyup); } /** * @hidden */ keyHandler(event, keyEvent, isHost) { if (this._disabled) { return; } const eventData = event; if (!isHost) { eventData.stopImmediatePropagation(); } const focused = this.focusService.focused || 0; const action = this.navigationService.process({ altKey: eventData.altKey, current: focused, keyCode: eventData.keyCode, keyEvent: keyEvent, max: this._data ? this._data.length - 1 : 0, min: 0, target: event.target }); if (action !== NavigationAction.Undefined && action !== NavigationAction.Tab && (action !== NavigationAction.Enter || (action === NavigationAction.Enter && this.openState))) { if (!(event.keyCode === Keys.Space && action === NavigationAction.EnterUp)) { eventData.preventDefault(); } } } emitItemClickHandler(index) { const dataItem = this._data[index]; if (this._itemClick && !dataItem.disabled) { this._itemClick.emit(dataItem); } if (dataItem && dataItem.click && !dataItem.disabled) { dataItem.click(dataItem); } this.focusService.focus(index); } focusWrapper() { if (this.openState) { this.togglePopupVisibility(); this.focusButton(); } } wrapperContains(element) { return this.wrapper === element || this.wrapper.contains(element); } blurWrapper(emit = true) { if (!this._isFocused) { return; } if (this.openState) { this.togglePopupVisibility(); } this._isFocused = false; if (emit) { this._blur.emit(); this.cdr.markForCheck(); } } focusButton() { if (this.button) { this.button.nativeElement.focus(); } } handleTab() { this.blurWrapper(); } onNavigationEnterUp(_args) { if (!this._disabled && !this.openState) { this._active = false; } if (this.openState) { const focused = this.focusService.focused; if (isPresent(focused) && focused !== -1) { this.emitItemClickHandler(focused); } } this.togglePopupVisibility(); if (!this.openState && isDocumentAvailable()) { this.button.nativeElement.focus(); } } onNavigationOpen() { if (!this._disabled && !this.openState) { this.togglePopupVisibility(); } } onNavigationClose(e) { if (this.openState && !this.isClosePrevented) { this.togglePopupVisibility(); if (isDocumentAvailable()) { e?.esc && hasObservers(this.escape) && this.escape.emit(); this.button.nativeElement.focus(); } } } onArrowKeyNavigate({ index }) { this.focusService.focus(index); } _toggle(open, emitEvent) { if (this.openState === open) { return; } const eventArgs = new PreventableEvent(); if (emitEvent) { if (open && !this.openState) { this.open.emit(eventArgs); } else if (!open && this.openState) { this.close.emit(eventArgs); } if (eventArgs.isDefaultPrevented()) { this.isClosePrevented = true; return; } } this.openState = open; this.destroyPopup(); if (this.openState) { this.createPopup(); } } createPopup() { this.popupRef = this.popupService.open({ anchor: this.elRef, anchorAlign: this.anchorAlign, animate: this.popupSettings.animate, appendTo: this.appendTo, content: this.containerService.template, popupAlign: this.popupAlign, popupClass: this.popupClasses }); this.popupSubs = this.popupRef.popupAnchorViewportLeave.subscribe(() => { this.togglePopupVisibility(); }); } destroyPopup() { if (this.popupRef) { this.popupRef.close(); this.popupRef = null; this.popupSubs.unsubscribe(); this.isClosePrevented = false; } } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: ListButton, deps: [{ token: i1.FocusService }, { token: i2.NavigationService }, { token: i0.ElementRef }, { token: i0.NgZone }, { token: i3.PopupService }, { token: i0.ElementRef }, { token: i4.LocalizationService }, { token: i0.ChangeDetectorRef }, { token: i5.PopupContainerService }], target: i0.ɵɵFactoryTarget.Component }); static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "16.2.12", type: ListButton, selector: "ng-component", inputs: { disabled: "disabled", tabIndex: "tabIndex", buttonClass: "buttonClass", popupSettings: "popupSettings" }, outputs: { open: "open", close: "close", escape: "escape" }, viewQueries: [{ propertyName: "button", first: true, predicate: ["button"], descendants: true, read: ElementRef }, { propertyName: "buttonList", first: true, predicate: ["buttonList"], descendants: true }, { propertyName: "popupTemplate", first: true, predicate: ["popupTemplate"], descendants: true }, { propertyName: "container", first: true, predicate: ["container"], descendants: true, read: ViewContainerRef }], usesInheritance: true, usesOnChanges: true, ngImport: i0, template: '', isInline: true }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: ListButton, decorators: [{ type: Component, args: [{ template: '' }] }], ctorParameters: function () { return [{ type: i1.FocusService }, { type: i2.NavigationService }, { type: i0.ElementRef }, { type: i0.NgZone }, { type: i3.PopupService }, { type: i0.ElementRef }, { type: i4.LocalizationService }, { type: i0.ChangeDetectorRef }, { type: i5.PopupContainerService }]; }, propDecorators: { button: [{ type: ViewChild, args: ['button', { read: ElementRef }] }], buttonList: [{ type: ViewChild, args: ['buttonList'] }], popupTemplate: [{ type: ViewChild, args: ['popupTemplate'] }], container: [{ type: ViewChild, args: ['container', { read: ViewContainerRef }] }], disabled: [{ type: Input }], tabIndex: [{ type: Input }], buttonClass: [{ type: Input }], open: [{ type: Output }], close: [{ type: Output }], escape: [{ type: Output }], popupSettings: [{ type: Input }] } });