UNPKG

@progress/kendo-angular-inputs

Version:

Kendo UI for Angular Inputs Package - Everything you need to build professional form functionality (Checkbox, ColorGradient, ColorPalette, ColorPicker, FlatColorPicker, FormField, MaskedTextBox, NumericTextBox, RadioButton, RangeSlider, Slider, Switch, Te

670 lines (669 loc) 27.4 kB
/**----------------------------------------------------------------------------------------- * Copyright © 2025 Progress Software Corporation. All rights reserved. * Licensed under commercial license. See LICENSE.md in the project root for more information *-------------------------------------------------------------------------------------------*/ /* eslint-disable @typescript-eslint/no-explicit-any */ import { Renderer2, Component, ElementRef, Input, ViewChild, forwardRef, NgZone, Injector, ChangeDetectorRef } from '@angular/core'; import { NG_VALUE_ACCESSOR } from '@angular/forms'; import { fromEvent, interval, merge } from 'rxjs'; import { filter, concatMap, startWith, takeUntil, take, tap } from 'rxjs/operators'; import { trimValue } from '../sliders-common/sliders-util'; import { SliderModel } from './slider-model'; import { L10N_PREFIX, LocalizationService } from '@progress/kendo-angular-l10n'; import { areSame, isPresent, requiresZoneOnBlur } from '../common/utils'; import { eventValue, decreaseValueToStep, increaseValueToStep } from '../sliders-common/sliders-util'; import { invokeElementMethod } from '../common/dom-utils'; import { guid, isDocumentAvailable, hasObservers, KendoInput, anyChanged, Keys, EventsOutsideAngularDirective, DraggableDirective, ResizeSensorComponent } from '@progress/kendo-angular-common'; import { SliderBase } from '../sliders-common/slider-base'; import { caretAltDownIcon, caretAltLeftIcon, caretAltRightIcon, caretAltUpIcon } from '@progress/kendo-svg-icons'; import { SliderTicksComponent } from '../sliders-common/slider-ticks.component'; import { NgIf } from '@angular/common'; import { LocalizedSliderMessagesDirective } from './localization/localized-slider-messages.directive'; import { ButtonComponent } from '@progress/kendo-angular-buttons'; import * as i0 from "@angular/core"; import * as i1 from "@progress/kendo-angular-l10n"; const PRESSED = 'k-pressed'; /** * Represents the [Kendo UI Slider component for Angular]({% slug overview_slider %}). */ export class SliderComponent extends SliderBase { localization; injector; renderer; ngZone; changeDetector; hostElement; /** * @hidden */ focusableId = `k-${guid()}`; /** * Changes the `title` attribute of the drag handle so that it can be localized. */ dragHandleTitle; /** * Sets the title of the **Increase** button of the Slider ([see example]({% slug sidebuttons_slider %}#toc-titles)). */ incrementTitle; /** * Determines if the animation will be played on value change. * Regardless of this setting, no animation will be played during the initial rendering. */ animate = true; /** * Sets the title of the **Decrease** button of the Slider ([see example]({% slug sidebuttons_slider %}#toc-titles)). */ decrementTitle; /** * Renders the arrow side buttons of the Slider ([see example]({% slug sidebuttons_slider %}#toc-hidden-state)). * When `showButtons` is set to `false`, the buttons are not displayed. */ showButtons = true; /** * The current value of the Slider when it is initially displayed. * The component can use either NgModel or the `value` binding but not both of them at the same time. */ value = this.min; /** * @hidden */ set tabIndex(tabIndex) { this.tabindex = tabIndex; } get tabIndex() { return this.tabindex; } /** * @hidden */ get currentValue() { return isPresent(this.value) ? this.value.toString() : ''; } /** * @hidden */ arrowUpIcon = caretAltUpIcon; /** * @hidden */ arrowDownIcon = caretAltDownIcon; /** * @hidden */ arrowLeftIcon = caretAltLeftIcon; /** * @hidden */ arrowRightIcon = caretAltRightIcon; draghandle; decreaseButton; increaseButton; focusChangedProgrammatically = false; isInvalid; constructor(localization, injector, renderer, ngZone, changeDetector, hostElement) { super(localization, injector, renderer, ngZone, changeDetector, hostElement); this.localization = localization; this.injector = injector; this.renderer = renderer; this.ngZone = ngZone; this.changeDetector = changeDetector; this.hostElement = hostElement; } /** * Focuses the Slider. * * @example * ```ts-no-run * _@Component({ * selector: 'my-app', * template: ` * <button (click)="slider.focus()">Focus</button> * <kendo-slider #slider></kendo-slider> * ` * }) * class AppComponent { } * ``` */ focus() { if (!this.disabled) { this.focusChangedProgrammatically = true; invokeElementMethod(this.draghandle, 'focus'); this.focusChangedProgrammatically = false; } } /** * Blurs the Slider. */ blur() { this.focusChangedProgrammatically = true; invokeElementMethod(this.draghandle, 'blur'); this.handleBlur(); this.focusChangedProgrammatically = false; } ngOnChanges(changes) { if (anyChanged(['value', 'fixedTickWidth', 'tickPlacement'], changes, true)) { this.ngZone.onStable.asObservable().pipe(take(1)).subscribe(() => { this.sizeComponent(false); }); } } ngAfterViewInit() { if (!isDocumentAvailable()) { return; } if (this.showButtons) { this.setValueChangeInterval(this.increaseButton.nativeElement, () => this.increaseValue()); this.setValueChangeInterval(this.decreaseButton.nativeElement, () => this.decreaseValue()); } this.sizeComponent(false); if (this.ticks) { this.ticks.tickElements .changes .subscribe(() => this.sizeComponent(false)); } this.attachElementEventHandlers(); this.isSliderInvalid(); } ngOnDestroy() { if (this.subscriptions) { this.subscriptions.unsubscribe(); } } /** * @hidden */ get incrementMessage() { return this.incrementTitle || this.localizationService.get('increment'); } /** * @hidden */ get decrementMessage() { return this.decrementTitle || this.localizationService.get('decrement'); } /** * @hidden */ get dragHandleMessage() { return this.dragHandleTitle || this.localizationService.get('dragHandle'); } /** * @hidden */ onWrapClick = (args) => { const target = args.target; if (!this.isDisabled && !(target.closest('.k-button'))) { const value = eventValue(args, this.track.nativeElement, this.getProps()); this.changeValue(value); } invokeElementMethod(this.draghandle, 'focus'); }; /** * @hidden */ handleDragPress(args) { if (args.originalEvent) { args.originalEvent.preventDefault(); } this.renderer.removeClass(this.hostElement.nativeElement, 'k-slider-transitions'); } /** * @hidden */ onHandleDrag(args) { this.dragging = true; this.changeValue(eventValue(args, this.track.nativeElement, this.getProps())); } /** * @hidden */ onKeyDown = (e) => { const options = this.getProps(); const { max, min } = options; const handler = this.keyBinding[e.keyCode]; if (this.isDisabled || !handler) { return; } const value = handler(options); this.changeValue(trimValue(max, min, value)); e.preventDefault(); }; /** * @hidden */ onHandleRelease() { this.dragging = false; //needed for animation this.renderer.addClass(this.hostElement.nativeElement, 'k-slider-transitions'); } //ngModel binding /** * @hidden */ writeValue(value) { this.changeDetector.markForCheck(); this.value = value; this.sizeComponent(this.animate); } /** * @hidden */ registerOnChange(fn) { this.ngChange = fn; } /** * @hidden */ registerOnTouched(fn) { this.ngTouched = fn; } /** * @hidden */ changeValue(value) { if (!areSame(this.value, value)) { this.ngZone.run(() => { this.value = value; this.ngChange(value); this.valueChange.emit(value); this.sizeComponent(this.animate); this.changeDetector.markForCheck(); }); } this.isSliderInvalid(); } /** * @hidden */ sizeComponent(animate) { if (!isDocumentAvailable()) { return; } const wrapper = this.wrapper.nativeElement; const track = this.track.nativeElement; const selectionEl = this.sliderSelection.nativeElement; const dragHandleEl = this.draghandle.nativeElement; const ticks = this.ticks ? this.ticksContainer.nativeElement : null; if (!animate) { this.renderer.removeClass(this.hostElement.nativeElement, 'k-slider-transitions'); } this.resetStyles([track, selectionEl, dragHandleEl, ticks, this.hostElement.nativeElement]); const props = this.getProps(); const model = new SliderModel(props, wrapper, track, this.renderer, this.increaseButton); model.resizeTrack(); if (this.ticks) { //for case when tickPlacement: none model.resizeTicks(this.ticksContainer.nativeElement, this.ticks.tickElements.map(element => element.nativeElement)); } model.positionHandle(dragHandleEl); model.positionSelection(selectionEl); if (!animate) { this.hostElement.nativeElement.getBoundingClientRect(); this.renderer.addClass(this.hostElement.nativeElement, 'k-slider-transitions'); } if (this.fixedTickWidth) { model.resizeWrapper(); } } set focused(value) { if (this.isFocused !== value && this.hostElement) { this.isFocused = value; } } set dragging(value) { if (this.isDragged !== value && this.sliderSelection && this.draghandle) { const sliderSelection = this.sliderSelection.nativeElement; const draghandle = this.draghandle.nativeElement; if (value) { this.renderer.addClass(sliderSelection, PRESSED); this.renderer.addClass(draghandle, PRESSED); } else { this.renderer.removeClass(sliderSelection, PRESSED); this.renderer.removeClass(draghandle, PRESSED); } this.isDragged = value; } } setValueChangeInterval(element, callback) { this.ngZone.runOutsideAngular(() => { const pointerdown = fromEvent(element, 'pointerdown'); const pointerup = fromEvent(element, 'pointerup'); const pointerout = fromEvent(element, 'pointerout'); const subscription = pointerdown.pipe(tap((e) => e.preventDefault()), filter((e) => e.button === 0 && !this.isDisabled), concatMap(() => interval(150).pipe(startWith(-1), takeUntil(merge(pointerup, pointerout))))).subscribe(() => { if (!this.isFocused) { invokeElementMethod(this.draghandle, 'focus'); } callback(); }); this.subscriptions.add(subscription); }); } ngChange = (_) => { }; ngTouched = () => { }; decreaseValue = () => { this.changeValue(decreaseValueToStep(this.value, this.getProps())); }; increaseValue = () => { this.changeValue(increaseValueToStep(this.value, this.getProps())); }; getProps() { return { buttons: this.showButtons, disabled: this.disabled, fixedTickWidth: this.fixedTickWidth, largeStep: this.largeStep, max: this.max, min: this.min, readonly: this.readonly, reverse: this.reverse, rtl: this.localizationService.rtl, smallStep: this.smallStep, value: trimValue(this.max, this.min, this.value), vertical: this.vertical }; } isSliderInvalid() { const sliderClasses = this.hostElement.nativeElement.classList; this.isInvalid = sliderClasses.contains('ng-invalid') ? true : false; this.renderer.setAttribute(this.draghandle.nativeElement, 'aria-invalid', `${this.isInvalid}`); } attachElementEventHandlers() { const hostElement = this.hostElement.nativeElement; let tabbing = false; let cursorInsideWrapper = false; this.ngZone.runOutsideAngular(() => { // focusIn and focusOut are relative to the host element this.subscriptions.add(this.renderer.listen(hostElement, 'focusin', () => { if (!this.isFocused) { this.ngZone.run(() => { if (!this.focusChangedProgrammatically) { this.onFocus.emit(); } this.focused = true; }); } })); this.subscriptions.add(this.renderer.listen(hostElement, 'focusout', (args) => { if (!this.isFocused) { return; } if (tabbing) { if (args.relatedTarget !== this.draghandle.nativeElement) { this.handleBlur(); } tabbing = false; } else { if (!cursorInsideWrapper) { this.handleBlur(); } } })); this.subscriptions.add(this.renderer.listen(hostElement, 'mouseenter', () => { cursorInsideWrapper = true; })); this.subscriptions.add(this.renderer.listen(hostElement, 'mouseleave', () => { cursorInsideWrapper = false; })); this.subscriptions.add(this.renderer.listen(hostElement, 'keydown', (args) => { if (args.keyCode === Keys.Tab) { tabbing = true; } else { tabbing = false; } })); }); } handleBlur = () => { this.changeDetector.markForCheck(); this.focused = false; if (hasObservers(this.onBlur) || requiresZoneOnBlur(this.control)) { this.ngZone.run(() => { this.ngTouched(); if (!this.focusChangedProgrammatically) { this.onBlur.emit(); } }); } }; get decreaseButtonArrowIcon() { const icon = !this.vertical ? this.direction === 'ltr' ? 'caret-alt-left' : 'caret-alt-right' : 'caret-alt-down'; return icon; } get increaseButtonArrowIcon() { const icon = !this.vertical ? this.direction === 'ltr' ? 'caret-alt-right' : 'caret-alt-left' : 'caret-alt-up'; return icon; } get decreaseButtonArrowSVGIcon() { const icon = !this.vertical ? this.direction === 'ltr' ? this.arrowLeftIcon : this.arrowRightIcon : this.arrowDownIcon; return icon; } get increaseButtonArrowSVGIcon() { const icon = !this.vertical ? this.direction === 'ltr' ? this.arrowRightIcon : this.arrowLeftIcon : this.arrowUpIcon; return icon; } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: SliderComponent, deps: [{ token: i1.LocalizationService }, { token: i0.Injector }, { token: i0.Renderer2 }, { token: i0.NgZone }, { token: i0.ChangeDetectorRef }, { token: i0.ElementRef }], target: i0.ɵɵFactoryTarget.Component }); static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "16.2.12", type: SliderComponent, isStandalone: true, selector: "kendo-slider", inputs: { focusableId: "focusableId", dragHandleTitle: "dragHandleTitle", incrementTitle: "incrementTitle", animate: "animate", decrementTitle: "decrementTitle", showButtons: "showButtons", value: "value", tabIndex: "tabIndex" }, providers: [ LocalizationService, { provide: L10N_PREFIX, useValue: 'kendo.slider' }, { multi: true, provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => SliderComponent) }, { provide: KendoInput, useExisting: forwardRef(() => SliderComponent) } ], viewQueries: [{ propertyName: "draghandle", first: true, predicate: ["draghandle"], descendants: true, static: true }, { propertyName: "decreaseButton", first: true, predicate: ["decreaseButton"], descendants: true, read: ElementRef }, { propertyName: "increaseButton", first: true, predicate: ["increaseButton"], descendants: true, read: ElementRef }], exportAs: ["kendoSlider"], usesInheritance: true, usesOnChanges: true, ngImport: i0, template: ` <ng-container kendoSliderLocalizedMessages i18n-increment="kendo.slider.increment|The title of the **Increase** button of the Slider." increment="increment" i18n-decrement="kendo.slider.decrement|The title of the **Decrease** button of the Slider." decrement="decrement" i18n-dragHandle="kendo.slider.dragHandle|The title of the drag handle of the Slider." dragHandle="Drag" > <button kendoButton #decreaseButton *ngIf="showButtons" type="button" rounded="full" [icon]="decreaseButtonArrowIcon" [svgIcon]="decreaseButtonArrowSVGIcon" class="k-button-decrease" [title]="decrementMessage" aria-hidden="true" [attr.tabindex]="-1" ></button> <div #wrap class="k-slider-track-wrap" [class.k-slider-topleft]="tickPlacement === 'before'" [class.k-slider-bottomright]="tickPlacement === 'after'" [kendoEventsOutsideAngular]="{ click: onWrapClick, keydown: onKeyDown }" > <ul kendoSliderTicks #ticks *ngIf="tickPlacement !== 'none'" [tickTitle]="title" [vertical]="vertical" [step]="smallStep" [largeStep]="largeStep" [min]="min" [max]="max" [labelTemplate]="labelTemplate?.templateRef" aria-hidden="true" > </ul> <div #track class="k-slider-track"> <div #sliderSelection class="k-slider-selection"> </div> <span #draghandle role="slider" [attr.aria-valuemin]="min" [attr.aria-valuemax]="max" [attr.aria-valuenow]="currentValue" [attr.aria-valuetext]="currentValue" [attr.aria-disabled]="disabled ? true : undefined" [attr.aria-readonly]="readonly ? true : undefined" [attr.aria-orientation]="vertical ? 'vertical' : 'horizontal'" [style.touch-action]="isDisabled ? '' : 'none'" class="k-draghandle k-draghandle-end" [title]="dragHandleMessage" [attr.tabindex]="disabled ? '-1' : tabIndex" [id]="focusableId" kendoDraggable (kendoPress)="ifEnabled(handleDragPress, $event)" (kendoDrag)="ifEnabled(onHandleDrag, $event)" (kendoRelease)="ifEnabled(onHandleRelease, $event)" ></span> </div> </div> <button kendoButton #increaseButton *ngIf="showButtons" type="button" rounded="full" [icon]="increaseButtonArrowIcon" [svgIcon]="increaseButtonArrowSVGIcon" class="k-button-increase" [title]="incrementMessage" [attr.tabindex]="-1" aria-hidden="true" ></button> <kendo-resize-sensor (resize)="sizeComponent(false)"></kendo-resize-sensor> `, isInline: true, dependencies: [{ kind: "directive", type: LocalizedSliderMessagesDirective, selector: "[kendoSliderLocalizedMessages]" }, { kind: "directive", type: NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: ButtonComponent, selector: "button[kendoButton]", inputs: ["arrowIcon", "toggleable", "togglable", "selected", "tabIndex", "imageUrl", "iconClass", "icon", "disabled", "size", "rounded", "fillMode", "themeColor", "svgIcon", "primary", "look"], outputs: ["selectedChange", "click"], exportAs: ["kendoButton"] }, { kind: "directive", type: EventsOutsideAngularDirective, selector: "[kendoEventsOutsideAngular]", inputs: ["kendoEventsOutsideAngular", "scope"] }, { kind: "component", type: SliderTicksComponent, selector: "[kendoSliderTicks]", inputs: ["tickTitle", "vertical", "step", "largeStep", "min", "max", "labelTemplate"] }, { kind: "directive", type: DraggableDirective, selector: "[kendoDraggable]", inputs: ["enableDrag"], outputs: ["kendoPress", "kendoDrag", "kendoRelease"] }, { kind: "component", type: ResizeSensorComponent, selector: "kendo-resize-sensor", inputs: ["rateLimit"], outputs: ["resize"] }] }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: SliderComponent, decorators: [{ type: Component, args: [{ exportAs: 'kendoSlider', providers: [ LocalizationService, { provide: L10N_PREFIX, useValue: 'kendo.slider' }, { multi: true, provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => SliderComponent) }, { provide: KendoInput, useExisting: forwardRef(() => SliderComponent) } ], selector: 'kendo-slider', template: ` <ng-container kendoSliderLocalizedMessages i18n-increment="kendo.slider.increment|The title of the **Increase** button of the Slider." increment="increment" i18n-decrement="kendo.slider.decrement|The title of the **Decrease** button of the Slider." decrement="decrement" i18n-dragHandle="kendo.slider.dragHandle|The title of the drag handle of the Slider." dragHandle="Drag" > <button kendoButton #decreaseButton *ngIf="showButtons" type="button" rounded="full" [icon]="decreaseButtonArrowIcon" [svgIcon]="decreaseButtonArrowSVGIcon" class="k-button-decrease" [title]="decrementMessage" aria-hidden="true" [attr.tabindex]="-1" ></button> <div #wrap class="k-slider-track-wrap" [class.k-slider-topleft]="tickPlacement === 'before'" [class.k-slider-bottomright]="tickPlacement === 'after'" [kendoEventsOutsideAngular]="{ click: onWrapClick, keydown: onKeyDown }" > <ul kendoSliderTicks #ticks *ngIf="tickPlacement !== 'none'" [tickTitle]="title" [vertical]="vertical" [step]="smallStep" [largeStep]="largeStep" [min]="min" [max]="max" [labelTemplate]="labelTemplate?.templateRef" aria-hidden="true" > </ul> <div #track class="k-slider-track"> <div #sliderSelection class="k-slider-selection"> </div> <span #draghandle role="slider" [attr.aria-valuemin]="min" [attr.aria-valuemax]="max" [attr.aria-valuenow]="currentValue" [attr.aria-valuetext]="currentValue" [attr.aria-disabled]="disabled ? true : undefined" [attr.aria-readonly]="readonly ? true : undefined" [attr.aria-orientation]="vertical ? 'vertical' : 'horizontal'" [style.touch-action]="isDisabled ? '' : 'none'" class="k-draghandle k-draghandle-end" [title]="dragHandleMessage" [attr.tabindex]="disabled ? '-1' : tabIndex" [id]="focusableId" kendoDraggable (kendoPress)="ifEnabled(handleDragPress, $event)" (kendoDrag)="ifEnabled(onHandleDrag, $event)" (kendoRelease)="ifEnabled(onHandleRelease, $event)" ></span> </div> </div> <button kendoButton #increaseButton *ngIf="showButtons" type="button" rounded="full" [icon]="increaseButtonArrowIcon" [svgIcon]="increaseButtonArrowSVGIcon" class="k-button-increase" [title]="incrementMessage" [attr.tabindex]="-1" aria-hidden="true" ></button> <kendo-resize-sensor (resize)="sizeComponent(false)"></kendo-resize-sensor> `, standalone: true, imports: [LocalizedSliderMessagesDirective, NgIf, ButtonComponent, EventsOutsideAngularDirective, SliderTicksComponent, DraggableDirective, ResizeSensorComponent] }] }], ctorParameters: function () { return [{ type: i1.LocalizationService }, { type: i0.Injector }, { type: i0.Renderer2 }, { type: i0.NgZone }, { type: i0.ChangeDetectorRef }, { type: i0.ElementRef }]; }, propDecorators: { focusableId: [{ type: Input }], dragHandleTitle: [{ type: Input }], incrementTitle: [{ type: Input }], animate: [{ type: Input }], decrementTitle: [{ type: Input }], showButtons: [{ type: Input }], value: [{ type: Input }], tabIndex: [{ type: Input }], draghandle: [{ type: ViewChild, args: ['draghandle', { static: true }] }], decreaseButton: [{ type: ViewChild, args: ['decreaseButton', { read: ElementRef }] }], increaseButton: [{ type: ViewChild, args: ['increaseButton', { read: ElementRef }] }] } });