@progress/kendo-angular-buttons
Version:
Buttons Package for Angular
1,408 lines (1,398 loc) • 221 kB
JavaScript
/**-----------------------------------------------------------------------------------------
* 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—or multiple classes separated by spaces—
* 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