UNPKG

@rp3e11/ngx-slider

Version:

Self-contained, mobile friendly slider component for Angular 13 based on angularjs-slider

1,134 lines 350 kB
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