@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
JavaScript
/**-----------------------------------------------------------------------------------------
* 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 }]
}] } });