UNPKG

@progress/kendo-angular-buttons

Version:
1,408 lines (1,398 loc) 221 kB
/**----------------------------------------------------------------------------------------- * Copyright © 2025 Progress Software Corporation. All rights reserved. * Licensed under commercial license. See LICENSE.md in the project root for more information *-------------------------------------------------------------------------------------------*/ import * as i0 from '@angular/core'; import { Injectable, isDevMode, EventEmitter, Component, Optional, Input, Output, HostBinding, HostListener, ContentChildren, Directive, InjectionToken, Inject, ElementRef, ViewContainerRef, ViewChild, ContentChild, forwardRef, NgModule } from '@angular/core'; import { Subject, Subscription, fromEvent, merge } from 'rxjs'; import * as i12 from '@progress/kendo-angular-common'; import { isDocumentAvailable, isFirefox, isSafari, isChanged, hasObservers, Keys, TemplateContextDirective, MultiTabStop, guid, isPresent as isPresent$1, EventsOutsideAngularDirective, anyChanged, ToggleButtonTabStopDirective, ResizeBatchService, KENDO_TOGGLEBUTTONTABSTOP } from '@progress/kendo-angular-common'; export { ToggleButtonTabStopDirective } from '@progress/kendo-angular-common'; import { caretAltDownIcon, xCircleIcon, moreVerticalIcon } from '@progress/kendo-svg-icons'; import * as i1 from '@progress/kendo-angular-l10n'; import { LocalizationService, L10N_PREFIX, ComponentMessages } from '@progress/kendo-angular-l10n'; import { validatePackage } from '@progress/kendo-licensing'; import { IconWrapperComponent, IconsService } from '@progress/kendo-angular-icons'; import { NgIf, NgClass, NgStyle, NgFor, NgTemplateOutlet } from '@angular/common'; import { filter, tap, take } from 'rxjs/operators'; import * as i3 from '@progress/kendo-angular-popup'; import { PopupService } from '@progress/kendo-angular-popup'; import * as i4 from '@angular/animations'; import { sequence, query, style, stagger, animate } from '@angular/animations'; /** * @hidden */ class KendoButtonService { buttonClicked = new Subject(); buttonClicked$ = this.buttonClicked.asObservable(); click(button) { this.buttonClicked.next(button); } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: KendoButtonService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: KendoButtonService }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: KendoButtonService, decorators: [{ type: Injectable }] }); /** * @hidden */ const packageMetadata = { name: '@progress/kendo-angular-buttons', productName: 'Kendo UI for Angular', productCode: 'KENDOUIANGULAR', productCodes: ['KENDOUIANGULAR'], publishDate: 1745303781, version: '18.5.2', licensingDocsUrl: 'https://www.telerik.com/kendo-angular-ui/my-license/' }; /** * @hidden */ const resolvedPromise = Promise.resolve(null); /** * @hidden */ const isPresent = (value) => value !== null && value !== undefined; /** * @hidden */ const tick = (f) => (resolvedPromise.then(f)); /** * @hidden */ function isDocumentNode(container) { return container.nodeType === 9; } /** * @hidden */ function closest(element, selector) { if (element.closest) { return element.closest(selector); } const matches = Element.prototype.matches ? (el, sel) => el.matches(sel) : (el, sel) => el.msMatchesSelector(sel); let node = element; while (node && !isDocumentNode(node)) { if (matches(node, selector)) { return node; } node = node.parentNode; } } /** * @hidden */ const replaceMessagePlaceholder = (message, name, value) => message.replace(new RegExp(`\{\\s*${name}\\s*\}`, 'g'), value); /** * @hidden */ const SIZES = { small: 'sm', medium: 'md', large: 'lg' }; const ROUNDNESS = { small: 'sm', medium: 'md', large: 'lg', full: 'full' }; /** * @hidden * * Returns the styling classes to be added and removed */ const getStylingClasses = (componentType, stylingOption, previousValue, newValue) => { switch (stylingOption) { case 'size': return { toRemove: `k-${componentType}-${SIZES[previousValue]}`, toAdd: newValue !== 'none' ? `k-${componentType}-${SIZES[newValue]}` : '' }; case 'rounded': return { toRemove: `k-rounded-${ROUNDNESS[previousValue]}`, toAdd: newValue !== 'none' ? `k-rounded-${ROUNDNESS[newValue]}` : '' }; case 'fillMode': return { toRemove: `k-${componentType}-${previousValue}`, toAdd: newValue !== 'none' ? `k-${componentType}-${newValue}` : '' }; default: break; } }; /** * @hidden * * Returns the themeColor classes to be added and removed */ const getThemeColorClasses = (componentType, prevFillMode, fillMode, previousValue, newValue) => { return { toRemove: `k-${componentType}-${prevFillMode}-${previousValue}`, toAdd: newValue !== 'none' ? `k-${componentType}-${fillMode}-${newValue}` : '' }; }; /** * @hidden * * Checks for an empty object - {} */ const isObjectEmpty = (obj) => obj && Object.keys(obj).length === 0 && obj.constructor === Object; const DEFAULT_ROUNDED$3 = 'medium'; const DEFAULT_SIZE$2 = 'medium'; const DEFAULT_THEME_COLOR$2 = 'base'; const DEFAULT_FILL_MODE$3 = 'solid'; /** * Represents the Kendo UI Button component for Angular. * * As of package v17, the `span[kendoButton]` and `kendo-button` selectors are removed. * Please use the `button[kendoButton]` selector only. */ class ButtonComponent { renderer; service; ngZone; /** * @hidden * @default false * required for DropDownButton arrow icon. */ arrowIcon = false; /** * Provides visual styling that indicates if the Button is active. * * @default false */ toggleable = false; /** * Backwards-compatible alias * * @hidden */ get togglable() { return this.toggleable; } /** * @hidden */ set togglable(value) { this.toggleable = value; } /** * Sets the selected state of the Button. * * @default false */ get selected() { return this._selected || false; } set selected(value) { this._selected = value; } /** * @hidden */ set tabIndex(index) { this.element.tabIndex = index; } get tabIndex() { return this.element.tabIndex; } /** * Defines a URL which is used for an `img` element inside the Button. * The URL can be relative or absolute. If relative, it is evaluated with relation to the web page URL. */ imageUrl; /** * Defines a CSS class&mdash;or multiple classes separated by spaces&mdash; * which are applied to a `span` element inside the Button. Allows the usage of custom icons. */ set iconClass(value) { if (isDevMode() && value && (this.icon || this.svgIcon)) { throw new Error('Setting both icon/svgIcon and iconClass options at the same time is not supported.'); } this._iconClass = value; } get iconClass() { return this._iconClass; } /** * Defines the name for an existing font icon in the Kendo UI theme. */ set icon(name) { if (isDevMode() && name && this.iconClass) { throw new Error('Setting both icon/svgIcon and iconClass options at the same time is not supported.'); } this._icon = name; } get icon() { return this._icon; } /** * If set to `true`, it disables the Button. * * @default false */ set disabled(disabled) { //Required, because in FF focused buttons are not blurred on disabled if (disabled && isDocumentAvailable() && isFirefox(navigator.userAgent)) { this.blur(); } this.isDisabled = disabled; this.renderer.setProperty(this.element, 'disabled', disabled); } get disabled() { return this.isDisabled; } /** * The size property specifies the padding of the Button * ([see example]({% slug appearance_button %}#toc-size)). * * @default 'medium' */ set size(size) { const newSize = size ? size : DEFAULT_SIZE$2; this.handleClasses(newSize, 'size'); this._size = newSize; } get size() { return this._size; } /** * The rounded property specifies the border radius of the Button * ([see example](slug:appearance_button#toc-roundness)). * * @default 'medium' */ set rounded(rounded) { const newRounded = rounded ? rounded : DEFAULT_ROUNDED$3; this.handleClasses(newRounded, 'rounded'); this._rounded = newRounded; } get rounded() { return this._rounded; } /** * The fillMode property specifies the background and border styles of the Button * ([see example](slug:appearance_button#toc-fill-mode)). * * @default 'solid' */ set fillMode(fillMode) { const newFillMode = fillMode ? fillMode : DEFAULT_FILL_MODE$3; this.handleClasses(newFillMode, 'fillMode'); this._fillMode = newFillMode; } get fillMode() { return this._fillMode; } /** * The Button allows you to specify predefined theme colors. * The theme color will be applied as a background and border color while also amending the text color accordingly * ([see example](slug:appearance_button#toc-theme-colors)). * * @default 'base' */ set themeColor(themeColor) { const newThemeColor = themeColor ? themeColor : DEFAULT_THEME_COLOR$2; this.handleThemeColor(newThemeColor); this._themeColor = newThemeColor; } get themeColor() { return this._themeColor; } /** * Defines an SVGIcon to be rendered within the button. */ set svgIcon(icon) { if (isDevMode() && icon && this.iconClass) { throw new Error('Setting both icon/svgIcon and iconClass options at the same time is not supported.'); } this._svgIcon = icon; } get svgIcon() { return this._svgIcon; } /** * Fires each time the selected state of a toggleable button is changed. * * The event argument is the new selected state (boolean). */ selectedChange = new EventEmitter(); /** * Fires each time the user clicks the button. */ click = new EventEmitter(); element; isDisabled = false; caretAltDownIcon = caretAltDownIcon; _size = DEFAULT_SIZE$2; _rounded = DEFAULT_ROUNDED$3; _fillMode = DEFAULT_FILL_MODE$3; _themeColor = DEFAULT_THEME_COLOR$2; _focused = false; direction; _selected; subs = new Subscription(); _iconClass; _icon; _svgIcon; set isFocused(isFocused) { this.toggleClass('k-focus', isFocused); this._focused = isFocused; } get isFocused() { return this._focused; } get classButton() { return true; } get isToggleable() { return this.toggleable; } get iconButtonClass() { const hasIcon = this.icon || this.iconClass || this.imageUrl || this.svgIcon; return hasIcon && !this.hasText; } get classDisabled() { return this.isDisabled; } get classActive() { return this.selected; } get getDirection() { return this.direction; } /** * @hidden */ onFocus() { this.isFocused = true; } /** * @hidden */ onBlur() { this.isFocused = false; } /** * @hidden */ set primary(value) { this.themeColor = value ? 'primary' : 'base'; } /** * @hidden */ set look(value) { switch (value) { case 'default': this.fillMode = 'solid'; break; default: this.fillMode = value; break; } } /** * Alias for ElementRef.nativeElement to workaround * ViewChild() selectors that used to return the host element before v11. * * @hidden */ get nativeElement() { return this.element; } constructor(element, renderer, service, localization, ngZone) { this.renderer = renderer; this.service = service; this.ngZone = ngZone; validatePackage(packageMetadata); this.direction = localization.rtl ? 'rtl' : 'ltr'; this.subs.add(localization.changes.subscribe(({ rtl }) => (this.direction = rtl ? 'rtl' : 'ltr'))); this.element = element.nativeElement; } ngOnInit() { if (!this.element.hasAttribute('role') && this.togglable) { this.toggleAriaPressed(this.toggleable); } this.ngZone.runOutsideAngular(() => { this.subs.add(this.renderer.listen(this.element, 'click', this._onButtonClick.bind(this))); this.subs.add(this.renderer.listen(this.element, 'mousedown', (event) => { const isBrowserSafari = isDocumentAvailable() && isSafari(navigator.userAgent); if (!this.isDisabled && isBrowserSafari) { event.preventDefault(); this.element.focus(); } })); }); } ngOnChanges(change) { if (isChanged('togglable', change) || isChanged('toggleable', change)) { this.toggleAriaPressed(this.toggleable); } } ngAfterViewInit() { const stylingOptions = ['size', 'rounded', 'fillMode']; stylingOptions.forEach(input => { this.handleClasses(this[input], input); }); } ngOnDestroy() { this.subs.unsubscribe(); } /** * @hidden */ get hasText() { return isDocumentAvailable() && this.element.textContent.trim().length > 0; } /** * Focuses the Button component. */ focus() { if (isDocumentAvailable()) { this.element.focus(); this.isFocused = true; } } /** * Blurs the Button component. */ blur() { if (isDocumentAvailable()) { this.element.blur(); this.isFocused = false; } } /** * @hidden */ setAttribute(attribute, value) { this.renderer.setAttribute(this.element, attribute, value); } /** * @hidden */ removeAttribute(attribute) { this.renderer.removeAttribute(this.element, attribute); } /** * @hidden * * Internal setter that triggers selectedChange */ setSelected(value) { const changed = this.selected !== value; this.selected = value; this.setAttribute('aria-pressed', this.selected.toString()); this.toggleClass('k-selected', this.selected); if (changed && hasObservers(this.selectedChange)) { this.ngZone.run(() => { this.selectedChange.emit(value); }); } } toggleAriaPressed(shouldSet) { if (!isDocumentAvailable()) { return; } if (shouldSet) { this.setAttribute('aria-pressed', this.selected.toString()); } else { this.removeAttribute('aria-pressed'); } } toggleClass(className, add) { if (add) { this.renderer.addClass(this.element, className); } else { this.renderer.removeClass(this.element, className); } } _onButtonClick() { if (!this.disabled && this.service) { this.ngZone.run(() => { this.service.click(this); }); } if (this.togglable && !this.service) { this.setSelected(!this.selected); } } handleClasses(value, input) { const elem = this.element; const classes = getStylingClasses('button', input, this[input], value); if (input === 'fillMode') { this.handleThemeColor(this.themeColor, this[input], value); } if (classes.toRemove) { this.renderer.removeClass(elem, classes.toRemove); } if (classes.toAdd) { this.renderer.addClass(elem, classes.toAdd); } } handleThemeColor(value, prevFillMode, fillMode) { const elem = this.element; const removeFillMode = prevFillMode ? prevFillMode : this.fillMode; const addFillMode = fillMode ? fillMode : this.fillMode; const themeColorClass = getThemeColorClasses('button', removeFillMode, addFillMode, this.themeColor, value); this.renderer.removeClass(elem, themeColorClass.toRemove); if (addFillMode !== 'none' && fillMode !== 'none') { if (themeColorClass.toAdd) { this.renderer.addClass(elem, themeColorClass.toAdd); } } } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: ButtonComponent, deps: [{ token: i0.ElementRef }, { token: i0.Renderer2 }, { token: KendoButtonService, optional: true }, { token: i1.LocalizationService }, { token: i0.NgZone }], target: i0.ɵɵFactoryTarget.Component }); static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "16.2.12", type: ButtonComponent, isStandalone: true, selector: "button[kendoButton]", inputs: { arrowIcon: "arrowIcon", toggleable: "toggleable", togglable: "togglable", selected: "selected", tabIndex: "tabIndex", imageUrl: "imageUrl", iconClass: "iconClass", icon: "icon", disabled: "disabled", size: "size", rounded: "rounded", fillMode: "fillMode", themeColor: "themeColor", svgIcon: "svgIcon", primary: "primary", look: "look" }, outputs: { selectedChange: "selectedChange", click: "click" }, host: { listeners: { "focus": "onFocus()", "blur": "onBlur()" }, properties: { "class.k-button": "this.classButton", "class.k-toggle-button": "this.isToggleable", "class.k-icon-button": "this.iconButtonClass", "class.k-disabled": "this.classDisabled", "class.k-selected": "this.classActive", "attr.dir": "this.getDirection" } }, providers: [ LocalizationService, { provide: L10N_PREFIX, useValue: 'kendo.button' } ], exportAs: ["kendoButton"], usesOnChanges: true, ngImport: i0, template: ` <kendo-icon-wrapper *ngIf="icon || svgIcon" innerCssClass="k-button-icon" [name]="icon" [svgIcon]="svgIcon"></kendo-icon-wrapper> <span *ngIf="imageUrl" class="k-button-icon k-icon"> <img [src]="imageUrl" class="k-image" role="presentation" /> </span> <span *ngIf="iconClass" class="k-button-icon" [ngClass]="iconClass"></span> <span class="k-button-text"><ng-content></ng-content></span> <span *ngIf="$any(arrowIcon).iconClass" class="k-button-icon" [ngClass]="$any(arrowIcon).iconClass"></span> <span *ngIf="arrowIcon && !$any(arrowIcon).iconClass" class="k-button-arrow"> <kendo-icon-wrapper [name]="$any(arrowIcon).icon || 'caret-alt-down'" [svgIcon]="$any(arrowIcon).svgIcon || caretAltDownIcon"></kendo-icon-wrapper> </span> `, isInline: true, dependencies: [{ kind: "directive", type: NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: IconWrapperComponent, selector: "kendo-icon-wrapper", inputs: ["name", "svgIcon", "innerCssClass", "customFontClass", "size"], exportAs: ["kendoIconWrapper"] }, { kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }] }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: ButtonComponent, decorators: [{ type: Component, args: [{ exportAs: 'kendoButton', providers: [ LocalizationService, { provide: L10N_PREFIX, useValue: 'kendo.button' } ], selector: 'button[kendoButton]', template: ` <kendo-icon-wrapper *ngIf="icon || svgIcon" innerCssClass="k-button-icon" [name]="icon" [svgIcon]="svgIcon"></kendo-icon-wrapper> <span *ngIf="imageUrl" class="k-button-icon k-icon"> <img [src]="imageUrl" class="k-image" role="presentation" /> </span> <span *ngIf="iconClass" class="k-button-icon" [ngClass]="iconClass"></span> <span class="k-button-text"><ng-content></ng-content></span> <span *ngIf="$any(arrowIcon).iconClass" class="k-button-icon" [ngClass]="$any(arrowIcon).iconClass"></span> <span *ngIf="arrowIcon && !$any(arrowIcon).iconClass" class="k-button-arrow"> <kendo-icon-wrapper [name]="$any(arrowIcon).icon || 'caret-alt-down'" [svgIcon]="$any(arrowIcon).svgIcon || caretAltDownIcon"></kendo-icon-wrapper> </span> `, standalone: true, imports: [NgIf, IconWrapperComponent, NgClass] }] }], ctorParameters: function () { return [{ type: i0.ElementRef }, { type: i0.Renderer2 }, { type: KendoButtonService, decorators: [{ type: Optional }] }, { type: i1.LocalizationService }, { type: i0.NgZone }]; }, propDecorators: { arrowIcon: [{ type: Input }], toggleable: [{ type: Input }], togglable: [{ type: Input }], selected: [{ type: Input }], tabIndex: [{ type: Input }], imageUrl: [{ type: Input }], iconClass: [{ type: Input }], icon: [{ type: Input }], disabled: [{ type: Input }], size: [{ type: Input }], rounded: [{ type: Input }], fillMode: [{ type: Input }], themeColor: [{ type: Input }], svgIcon: [{ type: Input }], selectedChange: [{ type: Output }], click: [{ type: Output }], classButton: [{ type: HostBinding, args: ['class.k-button'] }], isToggleable: [{ type: HostBinding, args: ['class.k-toggle-button'] }], iconButtonClass: [{ type: HostBinding, args: ['class.k-icon-button'] }], classDisabled: [{ type: HostBinding, args: ['class.k-disabled'] }], classActive: [{ type: HostBinding, args: ['class.k-selected'] }], getDirection: [{ type: HostBinding, args: ['attr.dir'] }], onFocus: [{ type: HostListener, args: ['focus'] }], onBlur: [{ type: HostListener, args: ['blur'] }], primary: [{ type: Input }], look: [{ type: Input }] } }); /** * @hidden */ class PreventableEvent { prevented = false; /** * Prevents the default action for a specified event. * In this way, the source component suppresses the built-in behavior that follows the event. */ preventDefault() { this.prevented = true; } /** * If the event is prevented by any of its subscribers, returns `true`. * * @returns `true` if the default action was prevented. Otherwise, returns `false`. */ isDefaultPrevented() { return this.prevented; } } /** * @hidden */ const tabindex = 'tabindex'; /** * Represents the Kendo UI ButtonGroup component for Angular. */ class ButtonGroupComponent { service; renderer; element; /** * By default, the ButtonGroup is enabled. * To disable the whole group of buttons, set its `disabled` attribute to `true`. * * To disable a specific button, set its own `disabled` attribute to `true` * and leave the `disabled` attribute of the ButtonGroup undefined. * If you define the `disabled` attribute of the ButtonGroup, it will take * precedence over the `disabled` attributes of the underlying buttons and they will be ignored. * * For more information on how to configure the Button, refer to * its [API documentation]({% slug api_buttons_buttoncomponent %}). */ disabled; /** * The selection mode of the ButtonGroup. * @default 'multiple' */ selection = 'multiple'; /** * Sets the width of the ButtonGroup. * If the width of the ButtonGroup is set: * - The buttons resize automatically to fill the full width of the group wrapper. * - The buttons acquire the same width. */ width; /** * Specifies the [`tabIndex`](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/tabindex) of the component. */ set tabIndex(value) { this._tabIndex = value; this.currentTabIndex = value; } get tabIndex() { return this._tabIndex; } /** * When this option is set to `true` (default), the component is a single tab-stop, * and focus is moved through the inner buttons via the arrow keys. * * When the option is set to `false`, the inner buttons are part of the natural tab sequence of the page. * * @default true */ navigable = true; /** * Fires every time keyboard navigation occurs. */ navigate = new EventEmitter(); buttons; _tabIndex = 0; currentTabIndex = 0; lastFocusedIndex = -1; direction; subs = new Subscription(); wrapperClasses = true; get disabledClass() { return this.disabled; } get stretchedClass() { return !!this.width; } role = 'group'; get dir() { return this.direction; } get ariaDisabled() { return this.disabled; } get wrapperWidth() { return this.width; } get wrapperTabIndex() { return this.disabled ? undefined : this.navigable ? this.currentTabIndex : undefined; } constructor(service, localization, renderer, element) { this.service = service; this.renderer = renderer; this.element = element; validatePackage(packageMetadata); this.subs.add(localization.changes.subscribe(({ rtl }) => this.direction = rtl ? 'rtl' : 'ltr')); } ngOnInit() { this.subs.add(this.service.buttonClicked$.subscribe((button) => { let newSelectionValue; if (this.isSelectionSingle()) { newSelectionValue = true; this.deactivate(this.buttons.filter(current => current !== button)); } else { if (this.navigable) { this.defocus(this.buttons.toArray()); } newSelectionValue = !button.selected; } if (button.togglable) { button.setSelected(newSelectionValue); } if (this.navigable) { this.currentTabIndex = -1; this.renderer.setAttribute(button, tabindex, '0'); } })); this.handleSubs('focus', () => this.navigable, this.focusHandler); this.handleSubs('keydown', () => this.navigable && !this.disabled, (event) => this.navigateFocus(event)); this.handleSubs('focusout', (event) => this.navigable && event.relatedTarget && event.relatedTarget.parentNode !== this.element.nativeElement, () => { this.lastFocusedIndex = this.buttons.toArray().findIndex(button => button.tabIndex !== -1); this.defocus(this.buttons.toArray()); this.currentTabIndex = this.tabIndex; }); this.subs.add(fromEvent(this.element.nativeElement, 'focusout') .pipe(filter((event) => this.navigable && event.relatedTarget && event.relatedTarget.parentNode !== this.element.nativeElement)) .subscribe(() => { this.defocus(this.buttons.toArray()); this.currentTabIndex = this.tabIndex; })); } ngOnChanges(changes) { if (isChanged('disabled', changes)) { this.buttons.forEach((button) => { if (isPresent(this.disabled)) { button.disabled = this.disabled; } }); } if (isChanged('navigable', changes)) { if (changes['navigable'].currentValue) { this.defocus(this.buttons.toArray()); this.currentTabIndex = 0; } else { this.currentTabIndex = -1; this.buttons.forEach((button) => this.renderer.setAttribute(button, tabindex, '0')); } } } ngAfterContentInit() { if (!this.navigable) { return; } this.defocus(this.buttons.toArray()); } ngAfterViewChecked() { if (this.buttons.length) { this.renderer.addClass(this.buttons.first.element, 'k-group-start'); this.renderer.addClass(this.buttons.last.element, 'k-group-end'); } } ngOnDestroy() { this.subs.unsubscribe(); } ngAfterContentChecked() { this.verifySettings(); } navigateFocus(event) { const navigationButtons = this.buttons.toArray().filter(button => !button.disabled); const focusedIndex = navigationButtons.findIndex(current => current.element.tabIndex !== -1); const firstIndex = 0; const lastIndex = navigationButtons.length - 1; const eventArgs = new PreventableEvent(); if (event.keyCode === Keys.ArrowRight && focusedIndex < lastIndex) { this.navigate.emit(eventArgs); if (!eventArgs.isDefaultPrevented()) { this.defocus(navigationButtons); this.focus(navigationButtons.filter((_current, index) => { return index === focusedIndex + 1; })); } } if (event.keyCode === Keys.ArrowLeft && focusedIndex > firstIndex) { this.navigate.emit(eventArgs); if (!eventArgs.isDefaultPrevented()) { this.defocus(navigationButtons); this.focus(navigationButtons.filter((_current, index) => { return index === focusedIndex - 1; })); } } } deactivate(buttons) { buttons.forEach((button) => { button.setSelected(false); if (this.navigable) { this.renderer.setAttribute(button, tabindex, '-1'); } }); } activate(buttons) { buttons.forEach((button) => { button.setSelected(true); if (this.navigable) { this.renderer.setAttribute(button, tabindex, '0'); } button.focus(); }); } defocus(buttons) { buttons.forEach((button) => { this.renderer.setAttribute(button, tabindex, '-1'); }); } focus(buttons) { buttons.forEach((button) => { this.renderer.setAttribute(button, tabindex, '0'); button.focus(); }); } verifySettings() { if (isDevMode()) { if (this.isSelectionSingle() && this.buttons.filter(button => button.selected).length > 1) { throw new Error('Having multiple selected buttons with single selection mode is not supported'); } } } isSelectionSingle() { return this.selection === 'single'; } handleSubs(eventName, predicate, handler) { this.subs.add(fromEvent(this.element.nativeElement, eventName) .pipe(filter(predicate)) .subscribe(handler)); } focusHandler = () => { this.currentTabIndex = -1; this.defocus(this.buttons.toArray()); const firstFocusableIndex = this.buttons.toArray().findIndex(current => !current.disabled); const index = this.lastFocusedIndex === -1 ? firstFocusableIndex : this.lastFocusedIndex; this.focus(this.buttons.filter((_current, i) => { return i === index; })); }; static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: ButtonGroupComponent, deps: [{ token: KendoButtonService }, { token: i1.LocalizationService }, { token: i0.Renderer2 }, { token: i0.ElementRef }], target: i0.ɵɵFactoryTarget.Component }); static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "16.2.12", type: ButtonGroupComponent, isStandalone: true, selector: "kendo-buttongroup", inputs: { disabled: "disabled", selection: "selection", width: "width", tabIndex: "tabIndex", navigable: "navigable" }, outputs: { navigate: "navigate" }, host: { properties: { "class.k-button-group": "this.wrapperClasses", "class.k-disabled": "this.disabledClass", "class.k-button-group-stretched": "this.stretchedClass", "attr.role": "this.role", "attr.dir": "this.dir", "attr.aria-disabled": "this.ariaDisabled", "style.width": "this.wrapperWidth", "attr.tabindex": "this.wrapperTabIndex" } }, providers: [ KendoButtonService, LocalizationService, { provide: L10N_PREFIX, useValue: 'kendo.buttongroup' } ], queries: [{ propertyName: "buttons", predicate: ButtonComponent }], exportAs: ["kendoButtonGroup"], usesOnChanges: true, ngImport: i0, template: ` <ng-content select="[kendoButton]"></ng-content> `, isInline: true }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: ButtonGroupComponent, decorators: [{ type: Component, args: [{ exportAs: 'kendoButtonGroup', providers: [ KendoButtonService, LocalizationService, { provide: L10N_PREFIX, useValue: 'kendo.buttongroup' } ], selector: 'kendo-buttongroup', template: ` <ng-content select="[kendoButton]"></ng-content> `, standalone: true }] }], ctorParameters: function () { return [{ type: KendoButtonService }, { type: i1.LocalizationService }, { type: i0.Renderer2 }, { type: i0.ElementRef }]; }, propDecorators: { disabled: [{ type: Input, args: ['disabled'] }], selection: [{ type: Input, args: ['selection'] }], width: [{ type: Input, args: ['width'] }], tabIndex: [{ type: Input }], navigable: [{ type: Input }], navigate: [{ type: Output }], buttons: [{ type: ContentChildren, args: [ButtonComponent] }], wrapperClasses: [{ type: HostBinding, args: ['class.k-button-group'] }], disabledClass: [{ type: HostBinding, args: ['class.k-disabled'] }], stretchedClass: [{ type: HostBinding, args: ['class.k-button-group-stretched'] }], role: [{ type: HostBinding, args: ['attr.role'] }], dir: [{ type: HostBinding, args: ['attr.dir'] }], ariaDisabled: [{ type: HostBinding, args: ['attr.aria-disabled'] }], wrapperWidth: [{ type: HostBinding, args: ['style.width'] }], wrapperTabIndex: [{ type: HostBinding, args: ['attr.tabindex'] }] } }); const DEFAULT_SIZE$1 = 'medium'; const DEFAULT_ROUNDED$2 = 'medium'; const DEFAULT_THEME_COLOR$1 = 'base'; const DEFAULT_FILL_MODE$2 = 'solid'; /** * Displays a Chip that represents an input, attribute or an action. */ class ChipComponent { element; renderer; ngZone; localizationService; /** * Sets the label text of the Chip. */ label; /** * Defines the name for an existing icon in a Kendo UI theme. * The icon is rendered inside the Chip by a `span.k-icon` element. */ icon; /** * Defines an [`SVGIcon`](slug:api_icons_svgicon) icon to be rendered inside the Chip using * a [`KendoSVGIcon`](slug:api_icons_svgiconcomponent) component. */ svgIcon; /** * Defines a CSS class — or multiple classes separated by spaces — * which are applied to a span element. * Allows the usage of custom icons. */ iconClass; /** * Use these settings to render an avatar within the Chip. */ avatarSettings; /** * Specifies the selected state of the Chip. * @default false */ selected = false; /** * Specifies if the Chip will be removable or not. * If the property is set to `true`, the Chip renders a remove icon. * @default false */ removable = false; /** * Specifies a custom remove font icon class that will be rendered when the Chip is removable. * [see example]({% slug icons %}) */ removeIcon; /** * Specifies a custom remove SVG icon that will be rendered when the Chip is removable. */ removeSvgIcon; /** * @hidden * * Determines whether the Chip has a menu. */ hasMenu = false; /** * @hidden * * Specifies a custom menu font icon class that will be rendered when the Chip has menu. */ menuIcon; /** * @hidden * * Specifies a custom menu SVG icon that will be rendered when the Chip has menu. */ menuSvgIcon; /** * If set to `true`, the Chip will be disabled. * @default false */ disabled = false; /** * The size property specifies the padding of the Chip * ([see example]({% slug appearance_chip %}#toc-size)). * * The possible values are: * * `small` * * `medium` (default) * * `large` * * `none` */ set size(size) { const newSize = size ? size : DEFAULT_SIZE$1; !this.sizeIsSet && (this.sizeIsSet = true); this.handleClasses(newSize, 'size'); this._size = newSize; } get size() { return this._size; } /** * The rounded property specifies the border radius of the Chip * ([see example](slug:appearance_chip#toc-roundness)). * * The possible values are: * * `small` * * `medium` (default) * * `large` * * `full` * * `none` */ set rounded(rounded) { const newRounded = rounded ? rounded : DEFAULT_ROUNDED$2; this.handleClasses(newRounded, 'rounded'); this._rounded = newRounded; } get rounded() { return this._rounded; } /** * The fillMode property specifies the background and border styles of the Chip * ([see example](slug:appearance_chip#toc-fill-mode)). * * The possible values are: * * `solid` (default) * * `outline` * * `none` */ set fillMode(fillMode) { const newFillMode = fillMode ? fillMode : DEFAULT_FILL_MODE$2; this.handleClasses(newFillMode, 'fillMode'); this._fillMode = newFillMode; } get fillMode() { return this._fillMode; } /** * The Chip allows you to specify predefined theme colors. * The theme color will be applied as a background and border color while also amending the text color accordingly * ([see example](slug:appearance_chip#toc-theme-colors)). * * The possible values are: * * `base` (default) * * `info` * * `success` * * `warning` * * `error` * * `none` */ set themeColor(themeColor) { const newThemeColor = themeColor ? themeColor : DEFAULT_THEME_COLOR$1; this.handleThemeColor(newThemeColor); this._themeColor = newThemeColor; } get themeColor() { return this._themeColor; } /** * Fires each time the user clicks the remove icon of the Chip. */ remove = new EventEmitter(); /** * @hidden * * Fires each time the user clicks the menu icon of the Chip. */ menuToggle = new EventEmitter(); /** * Fires each time the user clicks the content of the Chip. */ contentClick = new EventEmitter(); tabIndex = 0; hostClass = true; get hasIconClass() { return Boolean(this.icon || this.iconClass || (this.avatarSettings && !isObjectEmpty(this.avatarSettings))); } get disabledClass() { return this.disabled; } get selectedClass() { return this.selected; } get focusedClass() { return this.focused; } /** * @hidden */ direction; /** * @hidden */ defaultRemoveIcon = xCircleIcon; /** * @hidden */ defaultMenuIcon = moreVerticalIcon; /** * @hidden */ sizeIsSet = false; _size = 'medium'; _rounded = 'medium'; _fillMode = 'solid'; _themeColor = 'base'; focused = false; subs = new Subscription(); constructor(element, renderer, ngZone, localizationService) { this.element = element; this.renderer = renderer; this.ngZone = ngZone; this.localizationService = localizationService; validatePackage(packageMetadata); this.direction = localizationService.rtl ? 'rtl' : 'ltr'; } ngOnInit() { this.subs.add(this.localizationService.changes .subscribe(({ rtl }) => this.direction = rtl ? 'rtl' : 'ltr')); this.renderer.setAttribute(this.element.nativeElement, 'role', 'button'); } ngOnDestroy() { this.subs.unsubscribe(); } ngOnChanges(changes) { if (changes && changes['selected']) { const hasAriaSelected = this.element.nativeElement.hasAttribute('aria-selected'); if (!hasAriaSelected) { this.renderer.setAttribute(this.element.nativeElement, 'aria-pressed', `${this.selected}`); } } } ngAfterViewInit() { const chip = this.element.nativeElement; const stylingOptions = ['size', 'rounded', 'fillMode']; stylingOptions.forEach(input => { this.handleClasses(this[input], input); }); this.attachElementEventHandlers(chip); } /** * @hidden */ get kendoIconClass() { this.verifyIconSettings([this.iconClass]); return `k-i-${this.icon}`; } /** * @hidden */ get customIconClass() { this.verifyIconSettings([this.icon]); return this.iconClass; } /** * @hidden */ get removeIconClass() { return this.removeIcon ? this.removeIcon : 'k-i-x-circle'; } /** * Focuses the Chip component. */ focus() { if (isDocumentAvailable()) { this.element.nativeElement.focus(); } } /** * Blurs the Chip component. */ blur() { if (isDocumentAvailable()) { this.element.nativeElement.blur(); } } /** * @hidden */ onRemoveClick(e) { if (this.removable) { this.remove.emit({ sender: this, originalEvent: e }); } } /** * @hidden */ onMenuClick(e) { if (this.hasMenu) { this.menuToggle.emit({ sender: this, originalEvent: e }); } } attachElementEventHandlers(chip) { this.ngZone.runOutsideAngular(() => { this.subs.add(this.renderer.listen(chip, 'focus', () => { this.renderer.addClass(chip, 'k-focus'); })); this.subs.add(this.renderer.listen(chip, 'blur', () => { this.renderer.removeClass(chip, 'k-focus'); })); this.subs.add(this.renderer.listen(chip, 'click', (e) => { const isActionClicked = closest(e.target, '.k-chip-action'); if (!isActionClicked) { this.ngZone.run(() => { this.contentClick.emit({ sender: this, originalEvent: e }); }); } })); this.subs.add(this.renderer.listen(chip, 'keydown', this.keyDownHandler.bind(this))); }); } /** * @hidden */ verifyIconSettings(iconsToCheck) { if (isDevMode()) { if (iconsToCheck.filter(icon => icon !== null && icon !== undefined).length > 0) { this.renderer.removeClass(this.element.nativeElement, 'k-chip-has-icon'); throw new Error('Invalid configuration: Having multiple icons is not supported. Only a single icon on a chip can be displayed.'); } } } handleClasses(value, input) { const elem = this.element.nativeElement; const classes = getStylingClasses('chip', input, this[input], value); if (input === 'fillMode') { this.handleThemeColor(this.themeColor, this[input], value); } if (classes.toRemove) { this.renderer.removeClass(elem, classes.toRemove); } if (classes.toAdd) { this.renderer.addClass(elem, classes.toAdd); } } handleThemeColor(value, prevFillMode, fillMode) { const elem = this.element.nativeElement; const removeFillMode = prevFillMode ? prevFillMode : this.fillMode; const addFillMode = fillMode ? fillMode : this.fillMode; const themeColorClass = getThemeColorClasses('chip', removeFillMode, addFillMode, this.themeColor, value); this.renderer.removeClass(elem, themeColorClass.toRemove); if (addFillMode !== 'none' && fillMode !== 'none') { if (themeColorClass.toAdd) { this.renderer.addClass(elem, themeColorClass.toAdd); } } } keyDownHandler(e) { const isEnterOrSpace = e.keyCode === Keys.Enter || e.keyCode === Keys.Space; const isDeleteOrBackspace = e.keyCode === Keys.Delete || e.keyCode === Keys.Backspace; if (this.disabled) { return; } if (isEnterOrSpace) { this.ngZone.run(() => { this.contentClick.emit({ sender: this, originalEvent: e }); }); } else if (isDeleteOrBackspace && this.removable) { this.ngZone.run(() => { this.remove.emit({ sender: this, originalEvent: e }); }); } } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: ChipComponent, deps: [{ token: i0.ElementRef }, { token: i0.Renderer2 }, { token: i0.NgZone }, { token: i1.LocalizationService }], target: i0.ɵɵFactoryTarget.Component }); static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "16.2.12", type: ChipComponent, isStandalone: true, selector: "kendo-chip", inputs: { label: "label", icon: "icon", svgIcon: "svgIcon", iconClass: "iconClass", avatarSettings: "avatarSettings", selected: "selected", removable: "removable", removeIcon: "removeIcon", removeSvgIcon: "removeSvgIcon", hasMenu: "hasMenu", menuIcon: "menuIcon", menuSvgIcon: "menuSvgIcon", disabled: "disabled", size: "size", rounded: "rounded", fillMode: "fillMode", themeColor: "themeColor" }, outputs: { remove: "remove", menuToggle: "menuToggle", contentClick: "contentClick" }, host: { properties: { "attr.tabindex": "this.tabIndex", "class.k-chip": "this.hostClass", "class.k-chip-has-icon": "this.hasIconClass", "attr.aria-disabled": "this.disabledClass", "class.k-disabled": "this.disabledClass", "class.k-selected": "this.selectedClass", "class.k-focus": "this.focusedClass", "attr.dir": "this.direction" } }, providers: [ LocalizationService, { provide: L10N_PREFIX, useValue: 'kendo.chip' } ], usesOnChanges: true, ngImport: i0, template: ` <kendo-icon-wrapper *ngIf="icon || svgIcon" size="small" innerCssClass="k-chip-icon" [name]="icon" [svgIcon]="svgIcon"></kendo-icon-wrapper> <kendo-icon-wrapper *ngIf="iconClass" size="small" innerCssClass="k-chip-icon" [customFontClass]="customIconClass"></kendo-icon-wrapper> <span *ngIf="avatarSettings" class="k-chip-avatar k-avatar k-avatar-sm k-avatar-solid k-avatar-solid-primary k-rounded-full" [ngStyle]="avatarSettings.cssStyle"> <ng-container *ngIf="avatarSettings?.imageSrc"> <span class="k-avatar-image"> <img src="{{ avatarSettings.imageSrc }}" [ngStyle]="avatarSettings.imageCssStyl