@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
623 lines (620 loc) • 27.3 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 { Renderer2, Component, ElementRef, Input, ViewChild, forwardRef, NgZone, Injector, ChangeDetectorRef } from '@angular/core';
import { NG_VALUE_ACCESSOR } from '@angular/forms';
import { take } from 'rxjs/operators';
import { trimValue, isSameRange, trimValueRange, validateValue } from '../sliders-common/sliders-util';
import { RangeSliderModel } from './rangeslider-model';
import { L10N_PREFIX, LocalizationService } from '@progress/kendo-angular-l10n';
import { eventValue, isStartHandle } from '../sliders-common/sliders-util';
import { invokeElementMethod } from '../common/dom-utils';
import { guid, isDocumentAvailable, Keys, KendoInput, anyChanged, hasObservers, EventsOutsideAngularDirective, DraggableDirective, ResizeSensorComponent } from '@progress/kendo-angular-common';
import { requiresZoneOnBlur } from '../common/utils';
import { SliderBase } from '../sliders-common/slider-base';
import { SliderTicksComponent } from '../sliders-common/slider-ticks.component';
import { NgIf } from '@angular/common';
import { LocalizedRangeSliderMessagesDirective } from './localization/localized-rangeslider-messages.directive';
import * as i0 from "@angular/core";
import * as i1 from "@progress/kendo-angular-l10n";
const PRESSED = 'k-pressed';
/**
* Represents the [Kendo UI RangeSlider component for Angular]({% slug overview_rangeslider %}).
*/
export class RangeSliderComponent extends SliderBase {
localization;
injector;
renderer;
ngZone;
changeDetector;
hostElement;
/**
* Sets the range value of the RangeSlider.
* The component can use either NgModel or the `value` binding but not both of them at the same time.
*/
value;
draghandleStart;
draghandleEnd;
/**
* @hidden
*/
startHandleId = `k-start-handle-${guid()}`;
/**
* @hidden
*/
endHandleId = `k-end-handle-${guid()}`;
/**
* @hidden
*/
focusableId = this.startHandleId;
draggedHandle;
lastHandlePosition;
activeHandle = 'startHandle';
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 RangeSlider.
*
* @example
* ```ts-no-run
* _@Component({
* selector: 'my-app',
* template: `
* <div>
* <button class="k-button" (click)="slider.focus()">Focus</button>
* </div>
* <kendo-rangeslider #slider></kendo-rangeslider>
* `
* })
* class AppComponent { }
* ```
*/
focus() {
this.focusChangedProgrammatically = true;
invokeElementMethod(this.draghandleStart, 'focus');
this.focusChangedProgrammatically = false;
}
/**
* Blurs the RangeSlider.
*/
blur() {
this.focusChangedProgrammatically = true;
const activeHandle = this.activeHandle === 'startHandle' ? this.draghandleStart : this.draghandleEnd;
invokeElementMethod(activeHandle, 'blur');
this.handleBlur();
this.focusChangedProgrammatically = false;
}
ngOnInit() {
if (!this.value) {
this.value = [this.min, this.max];
}
super.ngOnInit();
}
ngOnChanges(changes) {
if (anyChanged(['value', 'fixedTickWidth', 'tickPlacement'], changes, true)) {
if (changes['value'] && changes['value'].currentValue) {
validateValue(changes['value'].currentValue);
}
this.ngZone.onStable.asObservable().pipe(take(1)).subscribe(() => {
this.sizeComponent();
});
}
}
ngAfterViewInit() {
if (!isDocumentAvailable()) {
return;
}
this.sizeComponent();
if (this.ticks) {
this.ticks.tickElements
.changes
.subscribe(() => this.sizeComponent());
}
this.isRangeSliderInvalid();
this.attachElementEventHandlers();
}
ngOnDestroy() {
if (this.subscriptions) {
this.subscriptions.unsubscribe();
}
}
/**
* @hidden
*/
textFor(key) {
return this.localization.get(key);
}
/**
* @hidden
*/
get valueText() {
return this.value ? `${this.value[0]} - ${this.value[1]}` : '';
}
/**
* @hidden
*/
onWrapClick = (args) => {
if (!this.isDisabled) {
this.value = this.value || [this.min, this.min];
const trackValue = eventValue(args, this.track.nativeElement, this.getProps());
let newRangeValue;
const [startValue, endValue] = newRangeValue = this.value;
if (trackValue <= startValue) {
newRangeValue = [trackValue, endValue];
this.activeHandle = 'startHandle';
}
else if (startValue < trackValue && trackValue < endValue) {
if (trackValue < (startValue + endValue) / 2) {
newRangeValue = [trackValue, endValue];
this.activeHandle = 'startHandle';
}
else {
newRangeValue = [startValue, trackValue];
this.activeHandle = 'endHandle';
}
}
else if (trackValue >= endValue) {
newRangeValue = [startValue, trackValue];
this.activeHandle = 'endHandle';
}
const activeHandle = this.activeHandle === 'startHandle' ? this.draghandleStart : this.draghandleEnd;
invokeElementMethod(activeHandle, 'focus');
this.changeValue(newRangeValue);
}
};
/**
* @hidden
*/
handleDragPress(args) {
if (args.originalEvent) {
args.originalEvent.preventDefault();
}
const target = args.originalEvent.target;
this.draggedHandle = target;
const nonDraggedHandle = this.draghandleStart.nativeElement === this.draggedHandle ? this.draghandleEnd.nativeElement : this.draghandleStart.nativeElement;
this.renderer.removeStyle(nonDraggedHandle, 'zIndex');
this.renderer.setStyle(target, 'zIndex', 1);
}
/**
* @hidden
*/
onHandleDrag(args) {
this.value = this.value || [this.min, this.min];
const target = args.originalEvent.target;
const lastCoords = this.draggedHandle.getBoundingClientRect();
this.lastHandlePosition = { x: lastCoords.left, y: lastCoords.top };
this.dragging = { value: true, target };
const mousePos = {
x: (args.pageX - 0.5) - (lastCoords.width / 2),
y: (args.pageY - (lastCoords.width / 2))
};
const left = mousePos.x < this.lastHandlePosition.x;
const right = mousePos.x > this.lastHandlePosition.x;
const up = mousePos.y > this.lastHandlePosition.y;
const moveStartHandle = () => this.changeValue([eventValue(args, this.track.nativeElement, this.getProps()), this.value[1]]);
const moveEndHandle = () => this.changeValue([this.value[0], eventValue(args, this.track.nativeElement, this.getProps())]);
const moveBothHandles = () => this.changeValue([eventValue(args, this.track.nativeElement, this.getProps()), eventValue(args, this.track.nativeElement, this.getProps())]);
const activeStartHandle = isStartHandle(this.draggedHandle);
const vertical = this.vertical;
const horizontal = !vertical;
const forward = (vertical && up) || (this.reverse ? horizontal && right : horizontal && left);
const incorrectValueState = this.value[0] > this.value[1];
if (this.value[0] === this.value[1] || incorrectValueState) {
if (forward) {
// eslint-disable-next-line no-unused-expressions
activeStartHandle ? moveStartHandle() : moveBothHandles();
}
else {
// eslint-disable-next-line no-unused-expressions
activeStartHandle ? moveBothHandles() : moveEndHandle();
}
}
else {
// eslint-disable-next-line no-unused-expressions
activeStartHandle ? moveStartHandle() : moveEndHandle();
}
}
/**
* @hidden
*/
onKeyDown = (e) => {
this.value = this.value || [this.min, this.min];
const options = this.getProps();
const { max, min } = options;
const handler = this.keyBinding[e.keyCode];
if (this.isDisabled || !handler) {
return;
}
const startHandleIsActive = isStartHandle(e.target);
const nonDraggedHandle = startHandleIsActive ? this.draghandleEnd.nativeElement : this.draghandleStart.nativeElement;
this.renderer.removeStyle(nonDraggedHandle, 'zIndex');
this.renderer.setStyle(e.target, 'zIndex', 1);
const value = handler({ ...options, value: startHandleIsActive ? this.value[0] : this.value[1] });
if (startHandleIsActive) {
if (value > this.value[1]) {
this.value[1] = value;
}
}
else {
if (value < this.value[0]) {
this.value[0] = value;
}
}
const trimmedValue = trimValue(max, min, value);
const newValue = startHandleIsActive ? [trimmedValue, this.value[1]]
: [this.value[0], trimmedValue];
this.changeValue(newValue);
e.preventDefault();
};
/**
* @hidden
*/
onHandleRelease(args) {
this.dragging = { value: false, target: args.originalEvent.target }; //needed for animation
this.draggedHandle = undefined;
}
//ngModel binding
/**
* @hidden
*/
writeValue(value) {
validateValue(value);
this.value = value;
this.sizeComponent();
}
/**
* @hidden
*/
registerOnChange(fn) {
this.ngChange = fn;
}
/**
* @hidden
*/
registerOnTouched(fn) {
this.ngTouched = fn;
}
/**
* @hidden
*/
changeValue(value) {
if (!this.value || !isSameRange(this.value, value)) {
this.ngZone.run(() => {
this.value = value;
this.ngChange(value);
if (this.value) {
this.valueChange.emit(value);
}
this.sizeComponent();
});
}
this.isRangeSliderInvalid();
}
/**
* @hidden
*/
sizeComponent() {
if (!isDocumentAvailable()) {
return;
}
const wrapper = this.wrapper.nativeElement;
const track = this.track.nativeElement;
const selectionEl = this.sliderSelection.nativeElement;
const dragHandleStartEl = this.draghandleStart.nativeElement;
const dragHandleEndEl = this.draghandleEnd.nativeElement;
const ticks = this.ticks ? this.ticksContainer.nativeElement : null;
this.resetStyles([track, selectionEl, dragHandleStartEl, dragHandleEndEl, ticks, this.hostElement.nativeElement]);
const props = this.getProps();
const model = new RangeSliderModel(props, wrapper, track, this.renderer);
model.resizeTrack();
if (this.ticks) { //for case when tickPlacement: none
model.resizeTicks(this.ticksContainer.nativeElement, this.ticks.tickElements.map(element => element.nativeElement));
}
model.positionHandle(dragHandleStartEl);
model.positionHandle(dragHandleEndEl);
model.positionSelection(dragHandleStartEl, selectionEl);
if (this.fixedTickWidth) {
model.resizeWrapper();
}
}
/**
* @hidden
*/
get isDisabled() {
return this.disabled || this.readonly;
}
/**
* @hidden
* Used by the FloatingLabel to determine if the component is empty.
*/
isEmpty() {
return false;
}
set focused(value) {
if (this.isFocused !== value && this.hostElement) {
this.isFocused = value;
}
}
set dragging(data) {
if (this.isDragged !== data.value && this.sliderSelection && this.draghandleStart && this.draghandleEnd) {
const sliderSelection = this.sliderSelection.nativeElement;
const draghandle = data.target;
if (data.value) {
this.renderer.addClass(sliderSelection, PRESSED);
this.renderer.addClass(draghandle, PRESSED);
}
else {
this.renderer.removeClass(sliderSelection, PRESSED);
this.renderer.removeClass(draghandle, PRESSED);
}
this.isDragged = data.value;
}
}
ngChange = (_) => { };
ngTouched = () => { };
getProps() {
return {
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: trimValueRange(this.max, this.min, this.value),
vertical: this.vertical,
buttons: false
};
}
isRangeSliderInvalid() {
const rangeSliderClasses = this.hostElement.nativeElement.classList;
this.isInvalid = rangeSliderClasses.contains('ng-invalid') ? true : false;
this.renderer.setAttribute(this.draghandleStart.nativeElement, 'aria-invalid', `${this.isInvalid}`);
this.renderer.setAttribute(this.draghandleEnd.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.draghandleStart.nativeElement && args.relatedTarget !== this.draghandleEnd.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();
}
});
}
};
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: RangeSliderComponent, 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: RangeSliderComponent, isStandalone: true, selector: "kendo-rangeslider", inputs: { value: "value" }, providers: [
LocalizationService,
{ provide: L10N_PREFIX, useValue: 'kendo.rangeslider' },
{ multi: true, provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => RangeSliderComponent) },
{ provide: KendoInput, useExisting: forwardRef(() => RangeSliderComponent) }
], viewQueries: [{ propertyName: "draghandleStart", first: true, predicate: ["draghandleStart"], descendants: true, static: true }, { propertyName: "draghandleEnd", first: true, predicate: ["draghandleEnd"], descendants: true, static: true }], exportAs: ["kendoRangeSlider"], usesInheritance: true, usesOnChanges: true, ngImport: i0, template: `
<ng-container kendoSliderLocalizedMessages
i18n-dragHandleStart="kendo.rangeslider.dragHandleStart|The title of the **Start** drag handle of the Slider."
dragHandleStart="Drag"
i18n-dragHandleEnd="kendo.rangeslider.dragHandleEnd|The title of the **End** drag handle of the Slider."
dragHandleEnd="Drag"
>
<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"
[attr.aria-hidden]="true"
>
</ul>
<div #track class="k-slider-track">
<div #sliderSelection class="k-slider-selection">
</div>
<span #draghandleStart
role="slider"
[id]="startHandleId"
[attr.tabindex]="disabled ? undefined : tabindex"
[attr.aria-valuemin]="min"
[attr.aria-valuemax]="max"
[attr.aria-valuenow]="value ? value[0] : null"
[attr.aria-valuetext]="valueText"
[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"
[title]="textFor('dragHandleStart')"
kendoDraggable
(kendoPress)="ifEnabled(handleDragPress ,$event)"
(kendoDrag)="ifEnabled(onHandleDrag ,$event)"
(kendoRelease)="ifEnabled(onHandleRelease, $event)"
></span>
<span #draghandleEnd
role="slider"
[id]="endHandleId"
[attr.tabindex]="disabled ? undefined : tabindex"
[attr.aria-valuemin]="min"
[attr.aria-valuemax]="max"
[attr.aria-valuenow]="value ? value[1] : null"
[attr.aria-valuetext]="valueText"
[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"
[title]="textFor('dragHandleEnd')"
kendoDraggable
(kendoPress)="ifEnabled(handleDragPress ,$event)"
(kendoDrag)="ifEnabled(onHandleDrag ,$event)"
(kendoRelease)="ifEnabled(onHandleRelease, $event)"
></span>
</div>
</div>
<kendo-resize-sensor (resize)="sizeComponent()"></kendo-resize-sensor>
`, isInline: true, dependencies: [{ kind: "directive", type: LocalizedRangeSliderMessagesDirective, selector: "[kendoSliderLocalizedMessages]" }, { kind: "directive", type: EventsOutsideAngularDirective, selector: "[kendoEventsOutsideAngular]", inputs: ["kendoEventsOutsideAngular", "scope"] }, { kind: "directive", type: NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { 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: RangeSliderComponent, decorators: [{
type: Component,
args: [{
exportAs: 'kendoRangeSlider',
providers: [
LocalizationService,
{ provide: L10N_PREFIX, useValue: 'kendo.rangeslider' },
{ multi: true, provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => RangeSliderComponent) },
{ provide: KendoInput, useExisting: forwardRef(() => RangeSliderComponent) }
],
selector: 'kendo-rangeslider',
template: `
<ng-container kendoSliderLocalizedMessages
i18n-dragHandleStart="kendo.rangeslider.dragHandleStart|The title of the **Start** drag handle of the Slider."
dragHandleStart="Drag"
i18n-dragHandleEnd="kendo.rangeslider.dragHandleEnd|The title of the **End** drag handle of the Slider."
dragHandleEnd="Drag"
>
<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"
[attr.aria-hidden]="true"
>
</ul>
<div #track class="k-slider-track">
<div #sliderSelection class="k-slider-selection">
</div>
<span #draghandleStart
role="slider"
[id]="startHandleId"
[attr.tabindex]="disabled ? undefined : tabindex"
[attr.aria-valuemin]="min"
[attr.aria-valuemax]="max"
[attr.aria-valuenow]="value ? value[0] : null"
[attr.aria-valuetext]="valueText"
[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"
[title]="textFor('dragHandleStart')"
kendoDraggable
(kendoPress)="ifEnabled(handleDragPress ,$event)"
(kendoDrag)="ifEnabled(onHandleDrag ,$event)"
(kendoRelease)="ifEnabled(onHandleRelease, $event)"
></span>
<span #draghandleEnd
role="slider"
[id]="endHandleId"
[attr.tabindex]="disabled ? undefined : tabindex"
[attr.aria-valuemin]="min"
[attr.aria-valuemax]="max"
[attr.aria-valuenow]="value ? value[1] : null"
[attr.aria-valuetext]="valueText"
[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"
[title]="textFor('dragHandleEnd')"
kendoDraggable
(kendoPress)="ifEnabled(handleDragPress ,$event)"
(kendoDrag)="ifEnabled(onHandleDrag ,$event)"
(kendoRelease)="ifEnabled(onHandleRelease, $event)"
></span>
</div>
</div>
<kendo-resize-sensor (resize)="sizeComponent()"></kendo-resize-sensor>
`,
standalone: true,
imports: [LocalizedRangeSliderMessagesDirective, EventsOutsideAngularDirective, NgIf, 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: { value: [{
type: Input
}], draghandleStart: [{
type: ViewChild,
args: ['draghandleStart', { static: true }]
}], draghandleEnd: [{
type: ViewChild,
args: ['draghandleEnd', { static: true }]
}] } });