@rp3e11/ngx-slider
Version:
Self-contained, mobile friendly slider component for Angular 13 based on angularjs-slider
1,134 lines • 350 kB
JavaScript
import { Component, ViewChild, HostBinding, HostListener, Input, EventEmitter, Output, ContentChild, forwardRef, ChangeDetectionStrategy, } from "@angular/core";
import { NG_VALUE_ACCESSOR } from "@angular/forms";
import { Subject } from "rxjs";
import { distinctUntilChanged, filter, throttleTime, tap, } from "rxjs/operators";
import detectPassiveEvents from "detect-passive-events";
import { Options, LabelType, } from "./options";
import { PointerType } from "./pointer-type";
import { ChangeContext } from "./change-context";
import { ValueHelper } from "./value-helper";
import { CompatibilityHelper } from "./compatibility-helper";
import { MathHelper } from "./math-helper";
import { EventListenerHelper } from "./event-listener-helper";
import { SliderElementDirective } from "./slider-element.directive";
import { SliderHandleDirective } from "./slider-handle.directive";
import { SliderLabelDirective } from "./slider-label.directive";
import * as i0 from "@angular/core";
import * as i1 from "./tooltip-wrapper.component";
import * as i2 from "./slider-element.directive";
import * as i3 from "@angular/common";
import * as i4 from "./slider-handle.directive";
import * as i5 from "./slider-label.directive";
export class Tick {
constructor() {
this.selected = false;
this.style = {};
this.tooltip = null;
this.tooltipPlacement = null;
this.value = null;
this.valueTooltip = null;
this.valueTooltipPlacement = null;
this.legend = null;
}
}
class Dragging {
constructor() {
this.active = false;
this.value = 0;
this.difference = 0;
this.position = 0;
this.lowLimit = 0;
this.highLimit = 0;
}
}
class ModelValues {
static compare(x, y) {
if (ValueHelper.isNullOrUndefined(x) && ValueHelper.isNullOrUndefined(y)) {
return false;
}
if (ValueHelper.isNullOrUndefined(x) !== ValueHelper.isNullOrUndefined(y)) {
return false;
}
return x.value === y.value && x.highValue === y.highValue;
}
}
class ModelChange extends ModelValues {
static compare(x, y) {
if (ValueHelper.isNullOrUndefined(x) && ValueHelper.isNullOrUndefined(y)) {
return false;
}
if (ValueHelper.isNullOrUndefined(x) !== ValueHelper.isNullOrUndefined(y)) {
return false;
}
return (x.value === y.value &&
x.highValue === y.highValue &&
x.forceChange === y.forceChange);
}
}
class InputModelChange extends ModelChange {
}
class OutputModelChange extends ModelChange {
}
const NGX_SLIDER_CONTROL_VALUE_ACCESSOR = {
provide: NG_VALUE_ACCESSOR,
/* tslint:disable-next-line: no-use-before-declare */
useExisting: forwardRef(() => SliderComponent),
multi: true,
};
export class SliderComponent {
constructor(renderer, elementRef, changeDetectionRef, zone) {
this.renderer = renderer;
this.elementRef = elementRef;
this.changeDetectionRef = changeDetectionRef;
this.zone = zone;
// Model for low value of slider. For simple slider, this is the only input. For range slider, this is the low value.
this.value = null;
// Output for low value slider to support two-way bindings
this.valueChange = new EventEmitter();
// Model for high value of slider. Not used in simple slider. For range slider, this is the high value.
this.highValue = null;
// Output for high value slider to support two-way bindings
this.highValueChange = new EventEmitter();
// An object with all the other options of the slider.
// Each option can be updated at runtime and the slider will automatically be re-rendered.
this.options = new Options();
// Event emitted when user starts interaction with the slider
this.userChangeStart = new EventEmitter();
// Event emitted on each change coming from user interaction
this.userChange = new EventEmitter();
// Event emitted when user finishes interaction with the slider
this.userChangeEnd = new EventEmitter();
// Set to true if init method already executed
this.initHasRun = false;
// Changes in model inputs are passed through this subject
// These are all changes coming in from outside the component through input bindings or reactive form inputs
this.inputModelChangeSubject = new Subject();
this.inputModelChangeSubscription = null;
// Changes to model outputs are passed through this subject
// These are all changes that need to be communicated to output emitters and registered callbacks
this.outputModelChangeSubject = new Subject();
this.outputModelChangeSubscription = null;
// Low value synced to model low value
this.viewLowValue = null;
// High value synced to model high value
this.viewHighValue = null;
// Options synced to model options, based on defaults
this.viewOptions = new Options();
// Half of the width or height of the slider handles
this.handleHalfDimension = 0;
// Maximum position the slider handle can have
this.maxHandlePosition = 0;
// Which handle is currently tracked for move events
this.currentTrackingPointer = null;
// Internal variable to keep track of the focus element
this.currentFocusPointer = null;
// Used to call onStart on the first keydown event
this.firstKeyDown = false;
// Current touch id of touch event being handled
this.touchId = null;
// Values recorded when first dragging the bar
this.dragging = new Dragging();
// Host element class bindings
this.sliderElementVerticalClass = false;
this.sliderElementAnimateClass = false;
this.sliderElementWithLegendClass = false;
this.sliderElementDisabledAttr = null;
// CSS styles and class flags
this.barStyle = {};
this.minPointerStyle = {};
this.maxPointerStyle = {};
this.fullBarTransparentClass = false;
this.selectionBarDraggableClass = false;
this.ticksUnderValuesClass = false;
/* If tickStep is set or ticksArray is specified.
In this case, ticks values should be displayed below the slider. */
this.intermediateTicks = false;
// Ticks array as displayed in view
this.ticks = [];
// Event listeners
this.eventListenerHelper = null;
this.onMoveEventListener = null;
this.onEndEventListener = null;
// Whether currently moving the slider (between onStart() and onEnd())
this.moving = false;
// Observer for slider element resize events
this.resizeObserver = null;
// Callbacks for reactive forms support
this.onTouchedCallback = null;
this.onChangeCallback = null;
this.eventListenerHelper = new EventListenerHelper(this.renderer);
}
// Input event that triggers slider refresh (re-positioning of slider elements)
set manualRefresh(manualRefresh) {
this.unsubscribeManualRefresh();
this.manualRefreshSubscription = manualRefresh.subscribe(() => {
setTimeout(() => this.calculateViewDimensionsAndDetectChanges());
});
}
// Input event that triggers setting focus on given slider handle
set triggerFocus(triggerFocus) {
this.unsubscribeTriggerFocus();
this.triggerFocusSubscription = triggerFocus.subscribe((pointerType) => {
this.focusPointer(pointerType);
});
}
// Slider type, true means range slider
get range() {
return (!ValueHelper.isNullOrUndefined(this.value) &&
!ValueHelper.isNullOrUndefined(this.highValue));
}
// Whether to show/hide ticks
get showTicks() {
return this.viewOptions.showTicks;
}
// OnInit interface
ngOnInit() {
this.viewOptions = new Options();
Object.assign(this.viewOptions, this.options);
// We need to run these two things first, before the rest of the init in ngAfterViewInit(),
// because these two settings are set through @HostBinding and Angular change detection
// mechanism doesn't like them changing in ngAfterViewInit()
this.updateDisabledState();
this.updateVerticalState();
}
// AfterViewInit interface
ngAfterViewInit() {
this.applyOptions();
this.subscribeInputModelChangeSubject(this.viewOptions.inputEventsInterval);
this.subscribeOutputModelChangeSubject(this.viewOptions.outputEventsInterval);
// Once we apply options, we need to normalise model values for the first time
this.renormaliseModelValues();
this.viewLowValue = this.modelValueToViewValue(this.value);
if (this.range) {
this.viewHighValue = this.modelValueToViewValue(this.highValue);
}
else {
this.viewHighValue = null;
}
this.updateVerticalState(); // need to run this again to cover changes to slider elements
this.manageElementsStyle();
this.updateDisabledState();
this.calculateViewDimensions();
this.addAccessibility();
this.updateCeilLabel();
this.updateFloorLabel();
this.initHandles();
this.manageEventsBindings();
this.subscribeResizeObserver();
this.initHasRun = true;
// Run change detection manually to resolve some issues when init procedure changes values used in the view
if (!this.isRefDestroyed()) {
this.changeDetectionRef.detectChanges();
}
}
// OnChanges interface
ngOnChanges(changes) {
// Always apply options first
if (this.haveOptionsChanged(changes)) {
this.onChangeOptions();
}
// Then value changes
if (this.haveValuesChanged(changes)) {
this.inputModelChangeSubject.next({
value: this.value,
highValue: this.highValue,
forceChange: false,
internalChange: false,
});
}
}
haveOptionsChanged(changes) {
return (!ValueHelper.isNullOrUndefined(changes.options) &&
changes.options.currentValue?.floor !==
changes.options.previousValue?.floor &&
changes.options.currentValue?.ceil !== changes.options.previousValue?.ceil);
}
haveValuesChanged(changes) {
return ((!ValueHelper.isNullOrUndefined(changes.value) &&
changes.value.currentValue !== changes.value.previousValue) ||
(!ValueHelper.isNullOrUndefined(changes.highValue) &&
changes.highValue.currentValue !== changes.highValue.previousValue));
}
// OnDestroy interface
ngOnDestroy() {
this.unbindEvents();
this.unsubscribeResizeObserver();
this.unsubscribeInputModelChangeSubject();
this.unsubscribeOutputModelChangeSubject();
this.unsubscribeManualRefresh();
this.unsubscribeTriggerFocus();
}
// ControlValueAccessor interface
writeValue(obj) {
if (obj instanceof Array) {
this.value = obj[0];
this.highValue = obj[1];
}
else {
this.value = obj;
}
// ngOnChanges() is not called in this instance, so we need to communicate the change manually
this.inputModelChangeSubject.next({
value: this.value,
highValue: this.highValue,
forceChange: false,
internalChange: false,
});
}
// ControlValueAccessor interface
registerOnChange(onChangeCallback) {
this.onChangeCallback = onChangeCallback;
}
// ControlValueAccessor interface
registerOnTouched(onTouchedCallback) {
this.onTouchedCallback = onTouchedCallback;
}
// ControlValueAccessor interface
setDisabledState(isDisabled) {
this.viewOptions.disabled = isDisabled;
this.updateDisabledState();
}
onResize(event) {
this.calculateViewDimensionsAndDetectChanges();
}
subscribeInputModelChangeSubject(interval) {
this.inputModelChangeSubscription = this.inputModelChangeSubject
.pipe(distinctUntilChanged(ModelChange.compare),
// Hack to reset the status of the distinctUntilChanged() - if a "fake" event comes through with forceChange=true,
// we forcefully by-pass distinctUntilChanged(), but otherwise drop the event
filter((modelChange) => !modelChange.forceChange && !modelChange.internalChange), !ValueHelper.isNullOrUndefined(interval)
? throttleTime(interval, undefined, { leading: true, trailing: true })
: tap(() => { }) // no-op
)
.subscribe((modelChange) => this.applyInputModelChange(modelChange));
}
subscribeOutputModelChangeSubject(interval) {
this.outputModelChangeSubscription = this.outputModelChangeSubject
.pipe(distinctUntilChanged(ModelChange.compare), !ValueHelper.isNullOrUndefined(interval)
? throttleTime(interval, undefined, { leading: true, trailing: true })
: tap(() => { }) // no-op
)
.subscribe((modelChange) => this.publishOutputModelChange(modelChange));
}
subscribeResizeObserver() {
if (CompatibilityHelper.isResizeObserverAvailable()) {
this.resizeObserver = new ResizeObserver(() => this.calculateViewDimensionsAndDetectChanges());
this.resizeObserver.observe(this.elementRef.nativeElement);
}
}
unsubscribeResizeObserver() {
if (CompatibilityHelper.isResizeObserverAvailable() &&
this.resizeObserver !== null) {
this.resizeObserver.disconnect();
this.resizeObserver = null;
}
}
unsubscribeOnMove() {
if (!ValueHelper.isNullOrUndefined(this.onMoveEventListener)) {
this.eventListenerHelper.detachEventListener(this.onMoveEventListener);
this.onMoveEventListener = null;
}
}
unsubscribeOnEnd() {
if (!ValueHelper.isNullOrUndefined(this.onEndEventListener)) {
this.eventListenerHelper.detachEventListener(this.onEndEventListener);
this.onEndEventListener = null;
}
}
unsubscribeInputModelChangeSubject() {
if (!ValueHelper.isNullOrUndefined(this.inputModelChangeSubscription)) {
this.inputModelChangeSubscription.unsubscribe();
this.inputModelChangeSubscription = null;
}
}
unsubscribeOutputModelChangeSubject() {
if (!ValueHelper.isNullOrUndefined(this.outputModelChangeSubscription)) {
this.outputModelChangeSubscription.unsubscribe();
this.outputModelChangeSubscription = null;
}
}
unsubscribeManualRefresh() {
if (!ValueHelper.isNullOrUndefined(this.manualRefreshSubscription)) {
this.manualRefreshSubscription.unsubscribe();
this.manualRefreshSubscription = null;
}
}
unsubscribeTriggerFocus() {
if (!ValueHelper.isNullOrUndefined(this.triggerFocusSubscription)) {
this.triggerFocusSubscription.unsubscribe();
this.triggerFocusSubscription = null;
}
}
getPointerElement(pointerType) {
if (pointerType === PointerType.Min) {
return this.minHandleElement;
}
else if (pointerType === PointerType.Max) {
return this.maxHandleElement;
}
return null;
}
getCurrentTrackingValue() {
if (this.currentTrackingPointer === PointerType.Min) {
return this.viewLowValue;
}
else if (this.currentTrackingPointer === PointerType.Max) {
return this.viewHighValue;
}
return null;
}
modelValueToViewValue(modelValue) {
if (ValueHelper.isNullOrUndefined(modelValue)) {
return NaN;
}
if (!ValueHelper.isNullOrUndefined(this.viewOptions.stepsArray) &&
!this.viewOptions.bindIndexForStepsArray) {
return ValueHelper.findStepIndex(+modelValue, this.viewOptions.stepsArray);
}
return +modelValue;
}
viewValueToModelValue(viewValue) {
if (!ValueHelper.isNullOrUndefined(this.viewOptions.stepsArray) &&
!this.viewOptions.bindIndexForStepsArray) {
return this.getStepValue(viewValue);
}
return viewValue;
}
getStepValue(sliderValue) {
const step = this.viewOptions.stepsArray[sliderValue];
return !ValueHelper.isNullOrUndefined(step) ? step.value : NaN;
}
applyViewChange() {
this.value = this.viewValueToModelValue(this.viewLowValue);
if (this.range) {
this.highValue = this.viewValueToModelValue(this.viewHighValue);
}
this.outputModelChangeSubject.next({
value: this.value,
highValue: this.highValue,
userEventInitiated: true,
forceChange: false,
});
// At this point all changes are applied and outputs are emitted, so we should be done.
// However, input changes are communicated in different stream and we need to be ready to
// act on the next input change even if it is exactly the same as last input change.
// Therefore, we send a special event to reset the stream.
this.inputModelChangeSubject.next({
value: this.value,
highValue: this.highValue,
forceChange: false,
internalChange: true,
});
}
// Apply model change to the slider view
applyInputModelChange(modelChange) {
const normalisedModelChange = this.normaliseModelValues(modelChange);
// If normalised model change is different, apply the change to the model values
const normalisationChange = !ModelValues.compare(modelChange, normalisedModelChange);
if (normalisationChange) {
this.value = normalisedModelChange.value;
this.highValue = normalisedModelChange.highValue;
}
this.viewLowValue = this.modelValueToViewValue(normalisedModelChange.value);
if (this.range) {
this.viewHighValue = this.modelValueToViewValue(normalisedModelChange.highValue);
}
else {
this.viewHighValue = null;
}
this.updateLowHandle(this.valueToPosition(this.viewLowValue));
if (this.range) {
this.updateHighHandle(this.valueToPosition(this.viewHighValue));
}
this.updateSelectionBar();
this.updateTicksScale();
this.updateAriaAttributes();
if (this.range) {
this.updateCombinedLabel();
}
// At the end, we need to communicate the model change to the outputs as well
// Normalisation changes are also always forced out to ensure that subscribers always end up in correct state
this.outputModelChangeSubject.next({
value: normalisedModelChange.value,
highValue: normalisedModelChange.highValue,
forceChange: normalisationChange,
userEventInitiated: false,
});
}
// Publish model change to output event emitters and registered callbacks
publishOutputModelChange(modelChange) {
const emitOutputs = () => {
this.valueChange.emit(modelChange.value);
if (this.range) {
this.highValueChange.emit(modelChange.highValue);
}
if (!ValueHelper.isNullOrUndefined(this.onChangeCallback)) {
if (this.range) {
this.onChangeCallback([modelChange.value, modelChange.highValue]);
}
else {
this.onChangeCallback(modelChange.value);
}
}
if (!ValueHelper.isNullOrUndefined(this.onTouchedCallback)) {
if (this.range) {
this.onTouchedCallback([modelChange.value, modelChange.highValue]);
}
else {
this.onTouchedCallback(modelChange.value);
}
}
};
if (modelChange.userEventInitiated) {
// If this change was initiated by a user event, we can emit outputs in the same tick
emitOutputs();
this.userChange.emit(this.getChangeContext());
}
else {
// But, if the change was initated by something else like a change in input bindings,
// we need to wait until next tick to emit the outputs to keep Angular change detection happy
setTimeout(() => {
emitOutputs();
});
}
}
normaliseModelValues(input) {
const normalisedInput = new ModelValues();
normalisedInput.value = input.value;
normalisedInput.highValue = input.highValue;
if (!ValueHelper.isNullOrUndefined(this.viewOptions.stepsArray)) {
// When using steps array, only round to nearest step in the array
// No other enforcement can be done, as the step array may be out of order, and that is perfectly fine
if (this.viewOptions.enforceStepsArray) {
const valueIndex = ValueHelper.findStepIndex(normalisedInput.value, this.viewOptions.stepsArray);
normalisedInput.value = this.viewOptions.stepsArray[valueIndex].value;
if (this.range) {
const highValueIndex = ValueHelper.findStepIndex(normalisedInput.highValue, this.viewOptions.stepsArray);
normalisedInput.highValue =
this.viewOptions.stepsArray[highValueIndex].value;
}
}
return normalisedInput;
}
if (this.viewOptions.enforceStep) {
normalisedInput.value = this.roundStep(normalisedInput.value);
if (this.range) {
normalisedInput.highValue = this.roundStep(normalisedInput.highValue);
}
}
if (this.viewOptions.enforceRange) {
normalisedInput.value = MathHelper.clampToRange(normalisedInput.value, this.viewOptions.floor, this.viewOptions.ceil);
if (this.range) {
normalisedInput.highValue = MathHelper.clampToRange(normalisedInput.highValue, this.viewOptions.floor, this.viewOptions.ceil);
}
// Make sure that range slider invariant (value <= highValue) is always satisfied
if (this.range && input.value > input.highValue) {
// We know that both values are now clamped correctly, they may just be in the wrong order
// So the easy solution is to swap them... except swapping is sometimes disabled in options, so we make the two values the same
if (this.viewOptions.noSwitching) {
normalisedInput.value = normalisedInput.highValue;
}
else {
const tempValue = input.value;
normalisedInput.value = input.highValue;
normalisedInput.highValue = tempValue;
}
}
}
return normalisedInput;
}
renormaliseModelValues() {
const previousModelValues = {
value: this.value,
highValue: this.highValue,
};
const normalisedModelValues = this.normaliseModelValues(previousModelValues);
if (!ModelValues.compare(normalisedModelValues, previousModelValues)) {
this.value = normalisedModelValues.value;
this.highValue = normalisedModelValues.highValue;
this.outputModelChangeSubject.next({
value: this.value,
highValue: this.highValue,
forceChange: true,
userEventInitiated: false,
});
}
}
onChangeOptions() {
if (!this.initHasRun) {
return;
}
const previousInputEventsInterval = this.viewOptions.inputEventsInterval;
const previousOutputEventsInterval = this.viewOptions.outputEventsInterval;
const previousOptionsInfluencingEventBindings = this.getOptionsInfluencingEventBindings(this.viewOptions);
this.applyOptions();
const newOptionsInfluencingEventBindings = this.getOptionsInfluencingEventBindings(this.viewOptions);
// Avoid re-binding events in case nothing changes that can influence it
// It makes it possible to change options while dragging the slider
const rebindEvents = !ValueHelper.areArraysEqual(previousOptionsInfluencingEventBindings, newOptionsInfluencingEventBindings);
if (previousInputEventsInterval !== this.viewOptions.inputEventsInterval) {
this.unsubscribeInputModelChangeSubject();
this.subscribeInputModelChangeSubject(this.viewOptions.inputEventsInterval);
}
if (previousOutputEventsInterval !== this.viewOptions.outputEventsInterval) {
this.unsubscribeInputModelChangeSubject();
this.subscribeInputModelChangeSubject(this.viewOptions.outputEventsInterval);
}
// With new options, we need to re-normalise model values if necessary
this.renormaliseModelValues();
this.viewLowValue = this.modelValueToViewValue(this.value);
if (this.range) {
this.viewHighValue = this.modelValueToViewValue(this.highValue);
}
else {
this.viewHighValue = null;
}
this.resetSlider(rebindEvents);
}
// Read the user options and apply them to the slider model
applyOptions() {
this.viewOptions = new Options();
Object.assign(this.viewOptions, this.options);
this.viewOptions.draggableRange =
this.range && this.viewOptions.draggableRange;
this.viewOptions.draggableRangeOnly =
this.range && this.viewOptions.draggableRangeOnly;
if (this.viewOptions.draggableRangeOnly) {
this.viewOptions.draggableRange = true;
}
this.viewOptions.showTicks =
this.viewOptions.showTicks ||
this.viewOptions.showTicksValues ||
!ValueHelper.isNullOrUndefined(this.viewOptions.ticksArray);
if (this.viewOptions.showTicks &&
(!ValueHelper.isNullOrUndefined(this.viewOptions.tickStep) ||
!ValueHelper.isNullOrUndefined(this.viewOptions.ticksArray))) {
this.intermediateTicks = true;
}
this.viewOptions.showSelectionBar =
this.viewOptions.showSelectionBar ||
this.viewOptions.showSelectionBarEnd ||
!ValueHelper.isNullOrUndefined(this.viewOptions.showSelectionBarFromValue);
if (!ValueHelper.isNullOrUndefined(this.viewOptions.stepsArray)) {
this.applyStepsArrayOptions();
}
else {
this.applyFloorCeilOptions();
}
if (ValueHelper.isNullOrUndefined(this.viewOptions.combineLabels)) {
this.viewOptions.combineLabels = (minValue, maxValue) => {
return minValue + " - " + maxValue;
};
}
if (this.viewOptions.logScale && this.viewOptions.floor === 0) {
throw Error("Can't use floor=0 with logarithmic scale");
}
}
applyStepsArrayOptions() {
this.viewOptions.floor = 0;
this.viewOptions.ceil = this.viewOptions.stepsArray.length - 1;
this.viewOptions.step = 1;
if (ValueHelper.isNullOrUndefined(this.viewOptions.translate)) {
this.viewOptions.translate = (modelValue) => {
if (this.viewOptions.bindIndexForStepsArray) {
return String(this.getStepValue(modelValue));
}
return String(modelValue);
};
}
}
getViewOptionsStepOrEstimate() {
let step = this.viewOptions.step;
if (ValueHelper.isNullOrUndefined(this.viewOptions.step)) {
const range = this.viewOptions.ceil - this.viewOptions.floor + 1;
if (range < 50) {
step = 1;
}
else {
step = Math.floor(range / 20);
}
}
return step;
}
applyFloorCeilOptions() {
if (ValueHelper.isNullOrUndefined(this.viewOptions.step)) {
this.viewOptions.step = this.getViewOptionsStepOrEstimate();
}
else {
this.viewOptions.step = +this.viewOptions.step;
if (this.viewOptions.step <= 0) {
this.viewOptions.step = 1;
}
}
if (ValueHelper.isNullOrUndefined(this.viewOptions.ceil) ||
ValueHelper.isNullOrUndefined(this.viewOptions.floor)) {
throw Error("floor and ceil options must be supplied");
}
this.viewOptions.ceil = +this.viewOptions.ceil;
this.viewOptions.floor = +this.viewOptions.floor;
if (ValueHelper.isNullOrUndefined(this.viewOptions.translate)) {
this.viewOptions.translate = (value) => String(value);
}
}
// Resets slider
resetSlider(rebindEvents = true) {
this.manageElementsStyle();
this.addAccessibility();
this.updateCeilLabel();
this.updateFloorLabel();
if (rebindEvents) {
this.unbindEvents();
this.manageEventsBindings();
}
this.updateDisabledState();
this.calculateViewDimensions();
// this.refocusPointerIfNeeded();
}
// Sets focus on the specified pointer
focusPointer(pointerType) {
// If not supplied, use min pointer as default
if (pointerType !== PointerType.Min && pointerType !== PointerType.Max) {
pointerType = PointerType.Min;
}
if (pointerType === PointerType.Min) {
this.minHandleElement.focus();
}
else if (this.range && pointerType === PointerType.Max) {
this.maxHandleElement.focus();
}
}
refocusPointerIfNeeded() {
if (!ValueHelper.isNullOrUndefined(this.currentFocusPointer)) {
this.onPointerFocus(this.currentFocusPointer);
const element = this.getPointerElement(this.currentFocusPointer);
element.focus();
}
}
// Update each elements style based on options
manageElementsStyle() {
this.updateScale();
this.floorLabelElement.setAlwaysHide(this.viewOptions.showTicksValues || this.viewOptions.hideLimitLabels);
this.ceilLabelElement.setAlwaysHide(this.viewOptions.showTicksValues || this.viewOptions.hideLimitLabels);
const hideLabelsForTicks = this.viewOptions.showTicksValues && !this.intermediateTicks;
this.minHandleLabelElement.setAlwaysHide(hideLabelsForTicks || this.viewOptions.hidePointerLabels);
this.maxHandleLabelElement.setAlwaysHide(hideLabelsForTicks || !this.range || this.viewOptions.hidePointerLabels);
this.combinedLabelElement.setAlwaysHide(hideLabelsForTicks || !this.range || this.viewOptions.hidePointerLabels);
this.selectionBarElement.setAlwaysHide(!this.range && !this.viewOptions.showSelectionBar);
this.leftOuterSelectionBarElement.setAlwaysHide(!this.range || !this.viewOptions.showOuterSelectionBars);
this.rightOuterSelectionBarElement.setAlwaysHide(!this.range || !this.viewOptions.showOuterSelectionBars);
this.fullBarTransparentClass =
this.range && this.viewOptions.showOuterSelectionBars;
this.selectionBarDraggableClass =
this.viewOptions.draggableRange && !this.viewOptions.onlyBindHandles;
this.ticksUnderValuesClass =
this.intermediateTicks && this.options.showTicksValues;
if (this.sliderElementVerticalClass !== this.viewOptions.vertical) {
this.updateVerticalState();
// The above change in host component class will not be applied until the end of this cycle
// However, functions calculating the slider position expect the slider to be already styled as vertical
// So as a workaround, we need to reset the slider once again to compute the correct values
setTimeout(() => {
this.resetSlider();
});
}
// Changing animate class may interfere with slider reset/initialisation, so we should set it separately,
// after all is properly set up
if (this.sliderElementAnimateClass !== this.viewOptions.animate) {
setTimeout(() => {
this.sliderElementAnimateClass = this.viewOptions.animate;
});
}
}
// Manage the events bindings based on readOnly and disabled options
manageEventsBindings() {
if (this.viewOptions.disabled || this.viewOptions.readOnly) {
this.unbindEvents();
}
else {
this.bindEvents();
}
}
// Set the disabled state based on disabled option
updateDisabledState() {
this.sliderElementDisabledAttr = this.viewOptions.disabled
? "disabled"
: null;
}
// Set vertical state based on vertical option
updateVerticalState() {
this.sliderElementVerticalClass = this.viewOptions.vertical;
for (const element of this.getAllSliderElements()) {
// This is also called before ngAfterInit, so need to check that view child bindings work
if (!ValueHelper.isNullOrUndefined(element)) {
element.setVertical(this.viewOptions.vertical);
}
}
}
updateScale() {
for (const element of this.getAllSliderElements()) {
element.setScale(this.viewOptions.scale);
}
}
getAllSliderElements() {
return [
this.leftOuterSelectionBarElement,
this.rightOuterSelectionBarElement,
this.fullBarElement,
this.selectionBarElement,
this.minHandleElement,
this.maxHandleElement,
this.floorLabelElement,
this.ceilLabelElement,
this.minHandleLabelElement,
this.maxHandleLabelElement,
this.combinedLabelElement,
this.ticksElement,
];
}
// Initialize slider handles positions and labels
// Run only once during initialization and every time view port changes size
initHandles() {
this.updateLowHandle(this.valueToPosition(this.viewLowValue));
/*
the order here is important since the selection bar should be
updated after the high handle but before the combined label
*/
if (this.range) {
this.updateHighHandle(this.valueToPosition(this.viewHighValue));
}
this.updateSelectionBar();
if (this.range) {
this.updateCombinedLabel();
}
this.updateTicksScale();
}
// Adds accessibility attributes, run only once during initialization
addAccessibility() {
this.updateAriaAttributes();
this.minHandleElement.role = "slider";
if (this.viewOptions.keyboardSupport &&
!(this.viewOptions.readOnly || this.viewOptions.disabled)) {
this.minHandleElement.tabindex = "0";
}
else {
this.minHandleElement.tabindex = "";
}
this.minHandleElement.ariaOrientation = this.viewOptions.vertical
? "vertical"
: "horizontal";
if (!ValueHelper.isNullOrUndefined(this.viewOptions.ariaLabel)) {
this.minHandleElement.ariaLabel = this.viewOptions.ariaLabel;
}
else if (!ValueHelper.isNullOrUndefined(this.viewOptions.ariaLabelledBy)) {
this.minHandleElement.ariaLabelledBy = this.viewOptions.ariaLabelledBy;
}
if (this.range) {
this.maxHandleElement.role = "slider";
if (this.viewOptions.keyboardSupport &&
!(this.viewOptions.readOnly || this.viewOptions.disabled)) {
this.maxHandleElement.tabindex = "0";
}
else {
this.maxHandleElement.tabindex = "";
}
this.maxHandleElement.ariaOrientation = this.viewOptions.vertical
? "vertical"
: "horizontal";
if (!ValueHelper.isNullOrUndefined(this.viewOptions.ariaLabelHigh)) {
this.maxHandleElement.ariaLabel = this.viewOptions.ariaLabelHigh;
}
else if (!ValueHelper.isNullOrUndefined(this.viewOptions.ariaLabelledByHigh)) {
this.maxHandleElement.ariaLabelledBy =
this.viewOptions.ariaLabelledByHigh;
}
}
}
// Updates aria attributes according to current values
updateAriaAttributes() {
this.minHandleElement.ariaValueNow = (+this.value).toString();
this.minHandleElement.ariaValueText = this.viewOptions.translate(+this.value, LabelType.Low);
this.minHandleElement.ariaValueMin = this.viewOptions.floor.toString();
this.minHandleElement.ariaValueMax = this.viewOptions.ceil.toString();
if (this.range) {
this.maxHandleElement.ariaValueNow = (+this.highValue).toString();
this.maxHandleElement.ariaValueText = this.viewOptions.translate(+this.highValue, LabelType.High);
this.maxHandleElement.ariaValueMin = this.viewOptions.floor.toString();
this.maxHandleElement.ariaValueMax = this.viewOptions.ceil.toString();
}
}
// Calculate dimensions that are dependent on view port size
// Run once during initialization and every time view port changes size.
calculateViewDimensions() {
if (!ValueHelper.isNullOrUndefined(this.viewOptions.handleDimension)) {
this.minHandleElement.setDimension(this.viewOptions.handleDimension);
}
else {
this.minHandleElement.calculateDimension();
}
const handleWidth = this.minHandleElement.dimension;
this.handleHalfDimension = handleWidth / 2;
if (!ValueHelper.isNullOrUndefined(this.viewOptions.barDimension)) {
this.fullBarElement.setDimension(this.viewOptions.barDimension);
}
else {
this.fullBarElement.calculateDimension();
}
this.maxHandlePosition = this.fullBarElement.dimension - handleWidth;
if (this.initHasRun) {
this.updateFloorLabel();
this.updateCeilLabel();
this.initHandles();
}
}
calculateViewDimensionsAndDetectChanges() {
this.calculateViewDimensions();
if (!this.isRefDestroyed()) {
this.changeDetectionRef.detectChanges();
}
}
/**
* If the slider reference is already destroyed
* @returns boolean - true if ref is destroyed
*/
isRefDestroyed() {
return this.changeDetectionRef["destroyed"];
}
// Update the ticks position
updateTicksScale() {
if (!this.viewOptions.showTicks && this.sliderElementWithLegendClass) {
setTimeout(() => {
this.sliderElementWithLegendClass = false;
});
return;
}
const ticksArray = !ValueHelper.isNullOrUndefined(this.viewOptions.ticksArray)
? this.viewOptions.ticksArray
: this.getTicksArray();
const translate = this.viewOptions.vertical
? "translateY"
: "translateX";
if (this.viewOptions.rightToLeft) {
ticksArray.reverse();
}
const tickValueStep = !ValueHelper.isNullOrUndefined(this.viewOptions.tickValueStep)
? this.viewOptions.tickValueStep
: !ValueHelper.isNullOrUndefined(this.viewOptions.tickStep)
? this.viewOptions.tickStep
: this.getViewOptionsStepOrEstimate();
let hasAtLeastOneLegend = false;
const newTicks = ticksArray.map((value) => {
let position = this.valueToPosition(value);
if (this.viewOptions.vertical) {
position = this.maxHandlePosition - position;
}
const translation = translate + "(" + Math.round(position) + "px)";
const tick = new Tick();
tick.selected = this.isTickSelected(value);
tick.style = {
"-webkit-transform": translation,
"-moz-transform": translation,
"-o-transform": translation,
"-ms-transform": translation,
transform: translation,
};
if (tick.selected &&
!ValueHelper.isNullOrUndefined(this.viewOptions.getSelectionBarColor)) {
tick.style["background-color"] = this.getSelectionBarColor();
}
if (!tick.selected &&
!ValueHelper.isNullOrUndefined(this.viewOptions.getTickColor)) {
tick.style["background-color"] = this.getTickColor(value);
}
if (!ValueHelper.isNullOrUndefined(this.viewOptions.ticksTooltip)) {
tick.tooltip = this.viewOptions.ticksTooltip(value);
tick.tooltipPlacement = this.viewOptions.vertical ? "right" : "top";
}
if (this.viewOptions.showTicksValues &&
!ValueHelper.isNullOrUndefined(tickValueStep) &&
MathHelper.isModuloWithinPrecisionLimit(value, tickValueStep, this.viewOptions.precisionLimit)) {
tick.value = this.getDisplayValue(value, LabelType.TickValue);
if (!ValueHelper.isNullOrUndefined(this.viewOptions.ticksValuesTooltip)) {
tick.valueTooltip = this.viewOptions.ticksValuesTooltip(value);
tick.valueTooltipPlacement = this.viewOptions.vertical
? "right"
: "top";
}
}
let legend = null;
if (!ValueHelper.isNullOrUndefined(this.viewOptions.stepsArray)) {
const step = this.viewOptions.stepsArray[value];
if (!ValueHelper.isNullOrUndefined(step)) {
legend = step.legend;
}
}
else if (!ValueHelper.isNullOrUndefined(this.viewOptions.getLegend)) {
legend = this.viewOptions.getLegend(value);
}
if (!ValueHelper.isNullOrUndefined(legend)) {
tick.legend = legend;
hasAtLeastOneLegend = true;
}
return tick;
});
if (this.sliderElementWithLegendClass !== hasAtLeastOneLegend) {
setTimeout(() => {
this.sliderElementWithLegendClass = hasAtLeastOneLegend;
});
}
// We should avoid re-creating the ticks array if possible
// This both improves performance and makes CSS animations work correctly
if (!ValueHelper.isNullOrUndefined(this.ticks) &&
this.ticks.length === newTicks.length) {
for (let i = 0; i < newTicks.length; ++i) {
Object.assign(this.ticks[i], newTicks[i]);
}
}
else {
this.ticks = newTicks;
if (!this.isRefDestroyed()) {
this.changeDetectionRef.detectChanges();
}
}
}
getTicksArray() {
const step = !ValueHelper.isNullOrUndefined(this.viewOptions.tickStep)
? this.viewOptions.tickStep
: this.getViewOptionsStepOrEstimate();
const ticksArray = [];
const numberOfValues = 1 +
Math.floor(MathHelper.roundToPrecisionLimit(Math.abs(this.viewOptions.ceil - this.viewOptions.floor) / step, this.viewOptions.precisionLimit));
for (let index = 0; index < numberOfValues - 1; ++index) {
ticksArray.push(MathHelper.roundToPrecisionLimit(this.viewOptions.floor + step * index, this.viewOptions.precisionLimit));
}
ticksArray.push(this.viewOptions.ceil);
return ticksArray;
}
isTickSelected(value) {
if (!this.range) {
if (!ValueHelper.isNullOrUndefined(this.viewOptions.showSelectionBarFromValue)) {
const center = this.viewOptions.showSelectionBarFromValue;
if (this.viewLowValue > center &&
value >= center &&
value <= this.viewLowValue) {
return true;
}
else if (this.viewLowValue < center &&
value <= center &&
value >= this.viewLowValue) {
return true;
}
}
else if (this.viewOptions.showSelectionBarEnd) {
if (value >= this.viewLowValue) {
return true;
}
}
else if (this.viewOptions.showSelectionBar &&
value <= this.viewLowValue) {
return true;
}
}
if (this.range &&
value >= this.viewLowValue &&
value <= this.viewHighValue) {
return true;
}
return false;
}
// Update position of the floor label
updateFloorLabel() {
if (!this.floorLabelElement.alwaysHide) {
this.floorLabelElement.setValue(this.getDisplayValue(this.viewOptions.floor, LabelType.Floor));
this.floorLabelElement.calculateDimension();
const position = this.viewOptions.rightToLeft
? this.fullBarElement.dimension - this.floorLabelElement.dimension
: 0;
this.floorLabelElement.setPosition(position);
}
}
// Update position of the ceiling label
updateCeilLabel() {
if (!this.ceilLabelElement.alwaysHide) {
this.ceilLabelElement.setValue(this.getDisplayValue(this.viewOptions.ceil, LabelType.Ceil));
this.ceilLabelElement.calculateDimension();
const position = this.viewOptions.rightToLeft
? 0
: this.fullBarElement.dimension - this.ceilLabelElement.dimension;
this.ceilLabelElement.setPosition(position);
}
}
// Update slider handles and label positions
updateHandles(which, newPos) {
if (which === PointerType.Min) {
this.updateLowHandle(newPos);
}
else if (which === PointerType.Max) {
this.updateHighHandle(newPos);
}
this.updateSelectionBar();
this.updateTicksScale();
if (this.range) {
this.updateCombinedLabel();
}
}
// Helper function to work out the position for handle labels depending on RTL or not
getHandleLabelPos(labelType, newPos) {
const labelDimension = labelType === PointerType.Min
? this.minHandleLabelElement.dimension
: this.maxHandleLabelElement.dimension;
const nearHandlePos = newPos - labelDimension / 2 + this.handleHalfDimension;
const endOfBarPos = this.fullBarElement.dimension - labelDimension;
if (!this.viewOptions.boundPointerLabels) {
return nearHandlePos;
}
if ((this.viewOptions.rightToLeft && labelType === PointerType.Min) ||
(!this.viewOptions.rightToLeft && labelType === PointerType.Max)) {
return Math.min(nearHandlePos, endOfBarPos);
}
else {
return Math.min(Math.max(nearHandlePos, 0), endOfBarPos);
}
}
// Update low slider handle position and label
updateLowHandle(newPos) {
this.minHandleElement.setPosition(newPos);
this.minHandleLabelElement.setValue(this.getDisplayValue(this.viewLowValue, LabelType.Low));
this.minHandleLabelElement.setPosition(this.getHandleLabelPos(PointerType.Min, newPos));
if (!ValueHelper.isNullOrUndefined(this.viewOptions.getPointerColor)) {
this.minPointerStyle = {
backgroundColor: this.getPointerColor(PointerType.Min),
};
}
if (this.viewOptions.autoHideLimitLabels) {
this.updateFloorAndCeilLabelsVisibility();
}
}
// Update high slider handle position and label
updateHighHandle(newPos) {
this.maxHandleElement.setPosition(newPos);
this.maxHandleLabelElement.setValue(this.getDisplayValue(this.viewHighValue, LabelType.High));
this.maxHandleLabelElement.setPosition(this.getHandleLabelPos(PointerType.Max, newPos));
if (!ValueHelper.isNullOrUndefined(this.viewOptions.getPointerColor)) {
this.maxPointerStyle = {
backgroundColor: this.getPointerColor(PointerType.Max),
};
}
if (this.viewOptions.autoHideLimitLabels) {
this.updateFloorAndCeilLabelsVisibility();
}
}
// Show/hide floor/ceiling label
updateFloorAndCeilLabelsVisibility() {
// Show based only on hideLimitLabels if pointer labels are hidden
if (this.viewOptions.hidePointerLabels) {
return;
}
let floorLabelHidden = false;
let ceilLabelHidden = false;
const isMinLabelAtFloor = this.isLabelBelowFloorLabel(this.minHandleLabelElement);
const isMinLabelAtCeil = this.isLabelAboveCeilLabel(this.minHandleLabelElement);
const isMaxLabelAtCeil = this.isLabelAboveCeilLabel(this.maxHandleLabelElement);
const isCombinedLabelAtFloor = this.isLabelBelowFloorLabel(this.combinedLabelElement);
const isCombinedLabelAtCeil = this.isLabelAboveCeilLabel(this.combinedLabelElement);
if (isMinLabelAtFloor) {
f