981 lines
29 KiB
TypeScript
981 lines
29 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright Google LLC All Rights Reserved.
|
|
*
|
|
* Use of this source code is governed by an MIT-style license that can be
|
|
* found in the LICENSE file at https://angular.dev/license
|
|
*/
|
|
|
|
import {Directionality} from '@angular/cdk/bidi';
|
|
import {Platform} from '@angular/cdk/platform';
|
|
import {
|
|
AfterViewInit,
|
|
booleanAttribute,
|
|
ChangeDetectionStrategy,
|
|
ChangeDetectorRef,
|
|
Component,
|
|
ContentChild,
|
|
ContentChildren,
|
|
ElementRef,
|
|
inject,
|
|
Input,
|
|
NgZone,
|
|
numberAttribute,
|
|
OnDestroy,
|
|
QueryList,
|
|
ViewChild,
|
|
ViewChildren,
|
|
ViewEncapsulation,
|
|
ANIMATION_MODULE_TYPE,
|
|
} from '@angular/core';
|
|
import {
|
|
_StructuralStylesLoader,
|
|
MAT_RIPPLE_GLOBAL_OPTIONS,
|
|
RippleGlobalOptions,
|
|
ThemePalette,
|
|
} from '@angular/material/core';
|
|
import {Subscription} from 'rxjs';
|
|
import {
|
|
_MatThumb,
|
|
_MatTickMark,
|
|
_MatSlider,
|
|
_MatSliderRangeThumb,
|
|
_MatSliderThumb,
|
|
_MatSliderVisualThumb,
|
|
MAT_SLIDER_RANGE_THUMB,
|
|
MAT_SLIDER_THUMB,
|
|
MAT_SLIDER,
|
|
MAT_SLIDER_VISUAL_THUMB,
|
|
} from './slider-interface';
|
|
import {MatSliderVisualThumb} from './slider-thumb';
|
|
import {_CdkPrivateStyleLoader} from '@angular/cdk/private';
|
|
|
|
// TODO(wagnermaciel): maybe handle the following edge case:
|
|
// 1. start dragging discrete slider
|
|
// 2. tab to disable checkbox
|
|
// 3. without ending drag, disable the slider
|
|
|
|
/**
|
|
* Allows users to select from a range of values by moving the slider thumb. It is similar in
|
|
* behavior to the native `<input type="range">` element.
|
|
*/
|
|
@Component({
|
|
selector: 'mat-slider',
|
|
templateUrl: 'slider.html',
|
|
styleUrl: 'slider.css',
|
|
host: {
|
|
'class': 'mat-mdc-slider mdc-slider',
|
|
'[class]': '"mat-" + (color || "primary")',
|
|
'[class.mdc-slider--range]': '_isRange',
|
|
'[class.mdc-slider--disabled]': 'disabled',
|
|
'[class.mdc-slider--discrete]': 'discrete',
|
|
'[class.mdc-slider--tick-marks]': 'showTickMarks',
|
|
'[class._mat-animation-noopable]': '_noopAnimations',
|
|
},
|
|
exportAs: 'matSlider',
|
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
encapsulation: ViewEncapsulation.None,
|
|
providers: [{provide: MAT_SLIDER, useExisting: MatSlider}],
|
|
imports: [MatSliderVisualThumb],
|
|
})
|
|
export class MatSlider implements AfterViewInit, OnDestroy, _MatSlider {
|
|
readonly _ngZone = inject(NgZone);
|
|
readonly _cdr = inject(ChangeDetectorRef);
|
|
readonly _elementRef = inject<ElementRef<HTMLElement>>(ElementRef);
|
|
readonly _dir = inject(Directionality, {optional: true});
|
|
readonly _globalRippleOptions = inject<RippleGlobalOptions>(MAT_RIPPLE_GLOBAL_OPTIONS, {
|
|
optional: true,
|
|
});
|
|
|
|
/** The active portion of the slider track. */
|
|
@ViewChild('trackActive') _trackActive: ElementRef<HTMLElement>;
|
|
|
|
/** The slider thumb(s). */
|
|
@ViewChildren(MAT_SLIDER_VISUAL_THUMB) _thumbs: QueryList<_MatSliderVisualThumb>;
|
|
|
|
/** The sliders hidden range input(s). */
|
|
@ContentChild(MAT_SLIDER_THUMB) _input: _MatSliderThumb;
|
|
|
|
/** The sliders hidden range input(s). */
|
|
@ContentChildren(MAT_SLIDER_RANGE_THUMB, {descendants: false})
|
|
_inputs: QueryList<_MatSliderRangeThumb>;
|
|
|
|
/** Whether the slider is disabled. */
|
|
@Input({transform: booleanAttribute})
|
|
get disabled(): boolean {
|
|
return this._disabled;
|
|
}
|
|
set disabled(v: boolean) {
|
|
this._disabled = v;
|
|
const endInput = this._getInput(_MatThumb.END);
|
|
const startInput = this._getInput(_MatThumb.START);
|
|
|
|
if (endInput) {
|
|
endInput.disabled = this._disabled;
|
|
}
|
|
if (startInput) {
|
|
startInput.disabled = this._disabled;
|
|
}
|
|
}
|
|
private _disabled: boolean = false;
|
|
|
|
/** Whether the slider displays a numeric value label upon pressing the thumb. */
|
|
@Input({transform: booleanAttribute})
|
|
get discrete(): boolean {
|
|
return this._discrete;
|
|
}
|
|
set discrete(v: boolean) {
|
|
this._discrete = v;
|
|
this._updateValueIndicatorUIs();
|
|
}
|
|
private _discrete: boolean = false;
|
|
|
|
/** Whether the slider displays tick marks along the slider track. */
|
|
@Input({transform: booleanAttribute})
|
|
showTickMarks: boolean = false;
|
|
|
|
/** The minimum value that the slider can have. */
|
|
@Input({transform: numberAttribute})
|
|
get min(): number {
|
|
return this._min;
|
|
}
|
|
set min(v: number) {
|
|
const min = isNaN(v) ? this._min : v;
|
|
if (this._min !== min) {
|
|
this._updateMin(min);
|
|
}
|
|
}
|
|
private _min: number = 0;
|
|
|
|
/**
|
|
* Theme color of the slider. This API is supported in M2 themes only, it
|
|
* has no effect in M3 themes.
|
|
*
|
|
* For information on applying color variants in M3, see
|
|
* https://material.angular.io/guide/theming#using-component-color-variants.
|
|
*/
|
|
@Input()
|
|
color: ThemePalette;
|
|
|
|
/** Whether ripples are disabled in the slider. */
|
|
@Input({transform: booleanAttribute})
|
|
disableRipple: boolean = false;
|
|
|
|
private _updateMin(min: number): void {
|
|
const prevMin = this._min;
|
|
this._min = min;
|
|
this._isRange ? this._updateMinRange({old: prevMin, new: min}) : this._updateMinNonRange(min);
|
|
this._onMinMaxOrStepChange();
|
|
}
|
|
|
|
private _updateMinRange(min: {old: number; new: number}): void {
|
|
const endInput = this._getInput(_MatThumb.END) as _MatSliderRangeThumb;
|
|
const startInput = this._getInput(_MatThumb.START) as _MatSliderRangeThumb;
|
|
|
|
const oldEndValue = endInput.value;
|
|
const oldStartValue = startInput.value;
|
|
|
|
startInput.min = min.new;
|
|
endInput.min = Math.max(min.new, startInput.value);
|
|
startInput.max = Math.min(endInput.max, endInput.value);
|
|
|
|
startInput._updateWidthInactive();
|
|
endInput._updateWidthInactive();
|
|
|
|
min.new < min.old
|
|
? this._onTranslateXChangeBySideEffect(endInput, startInput)
|
|
: this._onTranslateXChangeBySideEffect(startInput, endInput);
|
|
|
|
if (oldEndValue !== endInput.value) {
|
|
this._onValueChange(endInput);
|
|
}
|
|
|
|
if (oldStartValue !== startInput.value) {
|
|
this._onValueChange(startInput);
|
|
}
|
|
}
|
|
|
|
private _updateMinNonRange(min: number): void {
|
|
const input = this._getInput(_MatThumb.END);
|
|
if (input) {
|
|
const oldValue = input.value;
|
|
|
|
input.min = min;
|
|
input._updateThumbUIByValue();
|
|
this._updateTrackUI(input);
|
|
|
|
if (oldValue !== input.value) {
|
|
this._onValueChange(input);
|
|
}
|
|
}
|
|
}
|
|
|
|
/** The maximum value that the slider can have. */
|
|
@Input({transform: numberAttribute})
|
|
get max(): number {
|
|
return this._max;
|
|
}
|
|
set max(v: number) {
|
|
const max = isNaN(v) ? this._max : v;
|
|
if (this._max !== max) {
|
|
this._updateMax(max);
|
|
}
|
|
}
|
|
private _max: number = 100;
|
|
|
|
private _updateMax(max: number): void {
|
|
const prevMax = this._max;
|
|
this._max = max;
|
|
this._isRange ? this._updateMaxRange({old: prevMax, new: max}) : this._updateMaxNonRange(max);
|
|
this._onMinMaxOrStepChange();
|
|
}
|
|
|
|
private _updateMaxRange(max: {old: number; new: number}): void {
|
|
const endInput = this._getInput(_MatThumb.END) as _MatSliderRangeThumb;
|
|
const startInput = this._getInput(_MatThumb.START) as _MatSliderRangeThumb;
|
|
|
|
const oldEndValue = endInput.value;
|
|
const oldStartValue = startInput.value;
|
|
|
|
endInput.max = max.new;
|
|
startInput.max = Math.min(max.new, endInput.value);
|
|
endInput.min = startInput.value;
|
|
|
|
endInput._updateWidthInactive();
|
|
startInput._updateWidthInactive();
|
|
|
|
max.new > max.old
|
|
? this._onTranslateXChangeBySideEffect(startInput, endInput)
|
|
: this._onTranslateXChangeBySideEffect(endInput, startInput);
|
|
|
|
if (oldEndValue !== endInput.value) {
|
|
this._onValueChange(endInput);
|
|
}
|
|
|
|
if (oldStartValue !== startInput.value) {
|
|
this._onValueChange(startInput);
|
|
}
|
|
}
|
|
|
|
private _updateMaxNonRange(max: number): void {
|
|
const input = this._getInput(_MatThumb.END);
|
|
if (input) {
|
|
const oldValue = input.value;
|
|
|
|
input.max = max;
|
|
input._updateThumbUIByValue();
|
|
this._updateTrackUI(input);
|
|
|
|
if (oldValue !== input.value) {
|
|
this._onValueChange(input);
|
|
}
|
|
}
|
|
}
|
|
|
|
/** The values at which the thumb will snap. */
|
|
@Input({transform: numberAttribute})
|
|
get step(): number {
|
|
return this._step;
|
|
}
|
|
set step(v: number) {
|
|
const step = isNaN(v) ? this._step : v;
|
|
if (this._step !== step) {
|
|
this._updateStep(step);
|
|
}
|
|
}
|
|
private _step: number = 1;
|
|
|
|
private _updateStep(step: number): void {
|
|
this._step = step;
|
|
this._isRange ? this._updateStepRange() : this._updateStepNonRange();
|
|
this._onMinMaxOrStepChange();
|
|
}
|
|
|
|
private _updateStepRange(): void {
|
|
const endInput = this._getInput(_MatThumb.END) as _MatSliderRangeThumb;
|
|
const startInput = this._getInput(_MatThumb.START) as _MatSliderRangeThumb;
|
|
|
|
const oldEndValue = endInput.value;
|
|
const oldStartValue = startInput.value;
|
|
|
|
const prevStartValue = startInput.value;
|
|
|
|
endInput.min = this._min;
|
|
startInput.max = this._max;
|
|
|
|
endInput.step = this._step;
|
|
startInput.step = this._step;
|
|
|
|
if (this._platform.SAFARI) {
|
|
endInput.value = endInput.value;
|
|
startInput.value = startInput.value;
|
|
}
|
|
|
|
endInput.min = Math.max(this._min, startInput.value);
|
|
startInput.max = Math.min(this._max, endInput.value);
|
|
|
|
startInput._updateWidthInactive();
|
|
endInput._updateWidthInactive();
|
|
|
|
endInput.value < prevStartValue
|
|
? this._onTranslateXChangeBySideEffect(startInput, endInput)
|
|
: this._onTranslateXChangeBySideEffect(endInput, startInput);
|
|
|
|
if (oldEndValue !== endInput.value) {
|
|
this._onValueChange(endInput);
|
|
}
|
|
|
|
if (oldStartValue !== startInput.value) {
|
|
this._onValueChange(startInput);
|
|
}
|
|
}
|
|
|
|
private _updateStepNonRange(): void {
|
|
const input = this._getInput(_MatThumb.END);
|
|
if (input) {
|
|
const oldValue = input.value;
|
|
|
|
input.step = this._step;
|
|
if (this._platform.SAFARI) {
|
|
input.value = input.value;
|
|
}
|
|
|
|
input._updateThumbUIByValue();
|
|
|
|
if (oldValue !== input.value) {
|
|
this._onValueChange(input);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Function that will be used to format the value before it is displayed
|
|
* in the thumb label. Can be used to format very large number in order
|
|
* for them to fit into the slider thumb.
|
|
*/
|
|
@Input() displayWith: (value: number) => string = (value: number) => `${value}`;
|
|
|
|
/** Used to keep track of & render the active & inactive tick marks on the slider track. */
|
|
_tickMarks: _MatTickMark[];
|
|
|
|
/** Whether animations have been disabled. */
|
|
_noopAnimations: boolean;
|
|
|
|
/** Subscription to changes to the directionality (LTR / RTL) context for the application. */
|
|
private _dirChangeSubscription: Subscription;
|
|
|
|
/** Observer used to monitor size changes in the slider. */
|
|
private _resizeObserver: ResizeObserver | null;
|
|
|
|
// Stored dimensions to avoid calling getBoundingClientRect redundantly.
|
|
|
|
_cachedWidth: number;
|
|
_cachedLeft: number;
|
|
|
|
_rippleRadius: number = 24;
|
|
|
|
// The value indicator tooltip text for the visual slider thumb(s).
|
|
|
|
/** @docs-private */
|
|
protected startValueIndicatorText: string = '';
|
|
|
|
/** @docs-private */
|
|
protected endValueIndicatorText: string = '';
|
|
|
|
// Used to control the translateX of the visual slider thumb(s).
|
|
|
|
_endThumbTransform: string;
|
|
_startThumbTransform: string;
|
|
|
|
_isRange: boolean = false;
|
|
|
|
/** Whether the slider is rtl. */
|
|
_isRtl: boolean = false;
|
|
|
|
private _hasViewInitialized: boolean = false;
|
|
|
|
/**
|
|
* The width of the tick mark track.
|
|
* The tick mark track width is different from full track width
|
|
*/
|
|
_tickMarkTrackWidth: number = 0;
|
|
|
|
_hasAnimation: boolean = false;
|
|
|
|
private _resizeTimer: null | ReturnType<typeof setTimeout> = null;
|
|
|
|
private _platform = inject(Platform);
|
|
|
|
constructor(...args: unknown[]);
|
|
|
|
constructor() {
|
|
inject(_CdkPrivateStyleLoader).load(_StructuralStylesLoader);
|
|
const animationMode = inject(ANIMATION_MODULE_TYPE, {optional: true});
|
|
this._noopAnimations = animationMode === 'NoopAnimations';
|
|
|
|
if (this._dir) {
|
|
this._dirChangeSubscription = this._dir.change.subscribe(() => this._onDirChange());
|
|
this._isRtl = this._dir.value === 'rtl';
|
|
}
|
|
}
|
|
|
|
/** The radius of the native slider's knob. AFAIK there is no way to avoid hardcoding this. */
|
|
_knobRadius: number = 8;
|
|
|
|
_inputPadding: number;
|
|
|
|
ngAfterViewInit(): void {
|
|
if (this._platform.isBrowser) {
|
|
this._updateDimensions();
|
|
}
|
|
|
|
const eInput = this._getInput(_MatThumb.END);
|
|
const sInput = this._getInput(_MatThumb.START);
|
|
this._isRange = !!eInput && !!sInput;
|
|
this._cdr.detectChanges();
|
|
|
|
if (typeof ngDevMode === 'undefined' || ngDevMode) {
|
|
_validateInputs(
|
|
this._isRange,
|
|
this._getInput(_MatThumb.END),
|
|
this._getInput(_MatThumb.START),
|
|
);
|
|
}
|
|
|
|
const thumb = this._getThumb(_MatThumb.END);
|
|
this._rippleRadius = thumb._ripple.radius;
|
|
this._inputPadding = this._rippleRadius - this._knobRadius;
|
|
|
|
this._isRange
|
|
? this._initUIRange(eInput as _MatSliderRangeThumb, sInput as _MatSliderRangeThumb)
|
|
: this._initUINonRange(eInput!);
|
|
|
|
this._updateTrackUI(eInput!);
|
|
this._updateTickMarkUI();
|
|
this._updateTickMarkTrackUI();
|
|
|
|
this._observeHostResize();
|
|
this._cdr.detectChanges();
|
|
}
|
|
|
|
private _initUINonRange(eInput: _MatSliderThumb): void {
|
|
eInput.initProps();
|
|
eInput.initUI();
|
|
|
|
this._updateValueIndicatorUI(eInput);
|
|
|
|
this._hasViewInitialized = true;
|
|
eInput._updateThumbUIByValue();
|
|
}
|
|
|
|
private _initUIRange(eInput: _MatSliderRangeThumb, sInput: _MatSliderRangeThumb): void {
|
|
eInput.initProps();
|
|
eInput.initUI();
|
|
|
|
sInput.initProps();
|
|
sInput.initUI();
|
|
|
|
eInput._updateMinMax();
|
|
sInput._updateMinMax();
|
|
|
|
eInput._updateStaticStyles();
|
|
sInput._updateStaticStyles();
|
|
|
|
this._updateValueIndicatorUIs();
|
|
|
|
this._hasViewInitialized = true;
|
|
|
|
eInput._updateThumbUIByValue();
|
|
sInput._updateThumbUIByValue();
|
|
}
|
|
|
|
ngOnDestroy(): void {
|
|
this._dirChangeSubscription.unsubscribe();
|
|
this._resizeObserver?.disconnect();
|
|
this._resizeObserver = null;
|
|
}
|
|
|
|
/** Handles updating the slider ui after a dir change. */
|
|
private _onDirChange(): void {
|
|
this._isRtl = this._dir?.value === 'rtl';
|
|
this._isRange ? this._onDirChangeRange() : this._onDirChangeNonRange();
|
|
this._updateTickMarkUI();
|
|
}
|
|
|
|
private _onDirChangeRange(): void {
|
|
const endInput = this._getInput(_MatThumb.END) as _MatSliderRangeThumb;
|
|
const startInput = this._getInput(_MatThumb.START) as _MatSliderRangeThumb;
|
|
|
|
endInput._setIsLeftThumb();
|
|
startInput._setIsLeftThumb();
|
|
|
|
endInput.translateX = endInput._calcTranslateXByValue();
|
|
startInput.translateX = startInput._calcTranslateXByValue();
|
|
|
|
endInput._updateStaticStyles();
|
|
startInput._updateStaticStyles();
|
|
|
|
endInput._updateWidthInactive();
|
|
startInput._updateWidthInactive();
|
|
|
|
endInput._updateThumbUIByValue();
|
|
startInput._updateThumbUIByValue();
|
|
}
|
|
|
|
private _onDirChangeNonRange(): void {
|
|
const input = this._getInput(_MatThumb.END)!;
|
|
input._updateThumbUIByValue();
|
|
}
|
|
|
|
/** Starts observing and updating the slider if the host changes its size. */
|
|
private _observeHostResize() {
|
|
if (typeof ResizeObserver === 'undefined' || !ResizeObserver) {
|
|
return;
|
|
}
|
|
|
|
this._ngZone.runOutsideAngular(() => {
|
|
this._resizeObserver = new ResizeObserver(() => {
|
|
if (this._isActive()) {
|
|
return;
|
|
}
|
|
if (this._resizeTimer) {
|
|
clearTimeout(this._resizeTimer);
|
|
}
|
|
this._onResize();
|
|
});
|
|
this._resizeObserver.observe(this._elementRef.nativeElement);
|
|
});
|
|
}
|
|
|
|
/** Whether any of the thumbs are currently active. */
|
|
private _isActive(): boolean {
|
|
return this._getThumb(_MatThumb.START)._isActive || this._getThumb(_MatThumb.END)._isActive;
|
|
}
|
|
|
|
private _getValue(thumbPosition: _MatThumb = _MatThumb.END): number {
|
|
const input = this._getInput(thumbPosition);
|
|
if (!input) {
|
|
return this.min;
|
|
}
|
|
return input.value;
|
|
}
|
|
|
|
private _skipUpdate(): boolean {
|
|
return !!(
|
|
this._getInput(_MatThumb.START)?._skipUIUpdate || this._getInput(_MatThumb.END)?._skipUIUpdate
|
|
);
|
|
}
|
|
|
|
/** Stores the slider dimensions. */
|
|
_updateDimensions(): void {
|
|
this._cachedWidth = this._elementRef.nativeElement.offsetWidth;
|
|
this._cachedLeft = this._elementRef.nativeElement.getBoundingClientRect().left;
|
|
}
|
|
|
|
/** Sets the styles for the active portion of the track. */
|
|
_setTrackActiveStyles(styles: {
|
|
left: string;
|
|
right: string;
|
|
transform: string;
|
|
transformOrigin: string;
|
|
}): void {
|
|
const trackStyle = this._trackActive.nativeElement.style;
|
|
|
|
trackStyle.left = styles.left;
|
|
trackStyle.right = styles.right;
|
|
trackStyle.transformOrigin = styles.transformOrigin;
|
|
trackStyle.transform = styles.transform;
|
|
}
|
|
|
|
/** Returns the translateX positioning for a tick mark based on it's index. */
|
|
_calcTickMarkTransform(index: number): string {
|
|
// TODO(wagnermaciel): See if we can avoid doing this and just using flex to position these.
|
|
const translateX = index * (this._tickMarkTrackWidth / (this._tickMarks.length - 1));
|
|
return `translateX(${translateX}px`;
|
|
}
|
|
|
|
// Handlers for updating the slider ui.
|
|
|
|
_onTranslateXChange(source: _MatSliderThumb): void {
|
|
if (!this._hasViewInitialized) {
|
|
return;
|
|
}
|
|
|
|
this._updateThumbUI(source);
|
|
this._updateTrackUI(source);
|
|
this._updateOverlappingThumbUI(source as _MatSliderRangeThumb);
|
|
}
|
|
|
|
_onTranslateXChangeBySideEffect(
|
|
input1: _MatSliderRangeThumb,
|
|
input2: _MatSliderRangeThumb,
|
|
): void {
|
|
if (!this._hasViewInitialized) {
|
|
return;
|
|
}
|
|
|
|
input1._updateThumbUIByValue();
|
|
input2._updateThumbUIByValue();
|
|
}
|
|
|
|
_onValueChange(source: _MatSliderThumb): void {
|
|
if (!this._hasViewInitialized) {
|
|
return;
|
|
}
|
|
|
|
this._updateValueIndicatorUI(source);
|
|
this._updateTickMarkUI();
|
|
this._cdr.detectChanges();
|
|
}
|
|
|
|
_onMinMaxOrStepChange(): void {
|
|
if (!this._hasViewInitialized) {
|
|
return;
|
|
}
|
|
|
|
this._updateTickMarkUI();
|
|
this._updateTickMarkTrackUI();
|
|
this._cdr.markForCheck();
|
|
}
|
|
|
|
_onResize(): void {
|
|
if (!this._hasViewInitialized) {
|
|
return;
|
|
}
|
|
|
|
this._updateDimensions();
|
|
if (this._isRange) {
|
|
const eInput = this._getInput(_MatThumb.END) as _MatSliderRangeThumb;
|
|
const sInput = this._getInput(_MatThumb.START) as _MatSliderRangeThumb;
|
|
|
|
eInput._updateThumbUIByValue();
|
|
sInput._updateThumbUIByValue();
|
|
|
|
eInput._updateStaticStyles();
|
|
sInput._updateStaticStyles();
|
|
|
|
eInput._updateMinMax();
|
|
sInput._updateMinMax();
|
|
|
|
eInput._updateWidthInactive();
|
|
sInput._updateWidthInactive();
|
|
} else {
|
|
const eInput = this._getInput(_MatThumb.END);
|
|
if (eInput) {
|
|
eInput._updateThumbUIByValue();
|
|
}
|
|
}
|
|
|
|
this._updateTickMarkUI();
|
|
this._updateTickMarkTrackUI();
|
|
this._cdr.detectChanges();
|
|
}
|
|
|
|
/** Whether or not the slider thumbs overlap. */
|
|
private _thumbsOverlap: boolean = false;
|
|
|
|
/** Returns true if the slider knobs are overlapping one another. */
|
|
private _areThumbsOverlapping(): boolean {
|
|
const startInput = this._getInput(_MatThumb.START);
|
|
const endInput = this._getInput(_MatThumb.END);
|
|
if (!startInput || !endInput) {
|
|
return false;
|
|
}
|
|
return endInput.translateX - startInput.translateX < 20;
|
|
}
|
|
|
|
/**
|
|
* Updates the class names of overlapping slider thumbs so
|
|
* that the current active thumb is styled to be on "top".
|
|
*/
|
|
private _updateOverlappingThumbClassNames(source: _MatSliderRangeThumb): void {
|
|
const sibling = source.getSibling()!;
|
|
const sourceThumb = this._getThumb(source.thumbPosition);
|
|
const siblingThumb = this._getThumb(sibling.thumbPosition);
|
|
siblingThumb._hostElement.classList.remove('mdc-slider__thumb--top');
|
|
sourceThumb._hostElement.classList.toggle('mdc-slider__thumb--top', this._thumbsOverlap);
|
|
}
|
|
|
|
/** Updates the UI of slider thumbs when they begin or stop overlapping. */
|
|
private _updateOverlappingThumbUI(source: _MatSliderRangeThumb): void {
|
|
if (!this._isRange || this._skipUpdate()) {
|
|
return;
|
|
}
|
|
if (this._thumbsOverlap !== this._areThumbsOverlapping()) {
|
|
this._thumbsOverlap = !this._thumbsOverlap;
|
|
this._updateOverlappingThumbClassNames(source);
|
|
}
|
|
}
|
|
|
|
// _MatThumb styles update conditions
|
|
//
|
|
// 1. TranslateX, resize, or dir change
|
|
// - Reason: The thumb styles need to be updated according to the new translateX.
|
|
// 2. Min, max, or step
|
|
// - Reason: The value may have silently changed.
|
|
|
|
/** Updates the translateX of the given thumb. */
|
|
_updateThumbUI(source: _MatSliderThumb) {
|
|
if (this._skipUpdate()) {
|
|
return;
|
|
}
|
|
const thumb = this._getThumb(
|
|
source.thumbPosition === _MatThumb.END ? _MatThumb.END : _MatThumb.START,
|
|
)!;
|
|
thumb._hostElement.style.transform = `translateX(${source.translateX}px)`;
|
|
}
|
|
|
|
// Value indicator text update conditions
|
|
//
|
|
// 1. Value
|
|
// - Reason: The value displayed needs to be updated.
|
|
// 2. Min, max, or step
|
|
// - Reason: The value may have silently changed.
|
|
|
|
/** Updates the value indicator tooltip ui for the given thumb. */
|
|
_updateValueIndicatorUI(source: _MatSliderThumb): void {
|
|
if (this._skipUpdate()) {
|
|
return;
|
|
}
|
|
|
|
const valuetext = this.displayWith(source.value);
|
|
|
|
this._hasViewInitialized
|
|
? source._valuetext.set(valuetext)
|
|
: source._hostElement.setAttribute('aria-valuetext', valuetext);
|
|
|
|
if (this.discrete) {
|
|
source.thumbPosition === _MatThumb.START
|
|
? (this.startValueIndicatorText = valuetext)
|
|
: (this.endValueIndicatorText = valuetext);
|
|
|
|
const visualThumb = this._getThumb(source.thumbPosition);
|
|
valuetext.length < 3
|
|
? visualThumb._hostElement.classList.add('mdc-slider__thumb--short-value')
|
|
: visualThumb._hostElement.classList.remove('mdc-slider__thumb--short-value');
|
|
}
|
|
}
|
|
|
|
/** Updates all value indicator UIs in the slider. */
|
|
private _updateValueIndicatorUIs(): void {
|
|
const eInput = this._getInput(_MatThumb.END);
|
|
const sInput = this._getInput(_MatThumb.START);
|
|
|
|
if (eInput) {
|
|
this._updateValueIndicatorUI(eInput);
|
|
}
|
|
if (sInput) {
|
|
this._updateValueIndicatorUI(sInput);
|
|
}
|
|
}
|
|
|
|
// Update Tick Mark Track Width
|
|
//
|
|
// 1. Min, max, or step
|
|
// - Reason: The maximum reachable value may have changed.
|
|
// - Side note: The maximum reachable value is different from the maximum value set by the
|
|
// user. For example, a slider with [min: 5, max: 100, step: 10] would have a maximum
|
|
// reachable value of 95.
|
|
// 2. Resize
|
|
// - Reason: The position for the maximum reachable value needs to be recalculated.
|
|
|
|
/** Updates the width of the tick mark track. */
|
|
private _updateTickMarkTrackUI(): void {
|
|
if (!this.showTickMarks || this._skipUpdate()) {
|
|
return;
|
|
}
|
|
|
|
const step = this._step && this._step > 0 ? this._step : 1;
|
|
const maxValue = Math.floor(this.max / step) * step;
|
|
const percentage = (maxValue - this.min) / (this.max - this.min);
|
|
this._tickMarkTrackWidth = this._cachedWidth * percentage - 6;
|
|
}
|
|
|
|
// Track active update conditions
|
|
//
|
|
// 1. TranslateX
|
|
// - Reason: The track active should line up with the new thumb position.
|
|
// 2. Min or max
|
|
// - Reason #1: The 'active' percentage needs to be recalculated.
|
|
// - Reason #2: The value may have silently changed.
|
|
// 3. Step
|
|
// - Reason: The value may have silently changed causing the thumb(s) to shift.
|
|
// 4. Dir change
|
|
// - Reason: The track active will need to be updated according to the new thumb position(s).
|
|
// 5. Resize
|
|
// - Reason: The total width the 'active' tracks translateX is based on has changed.
|
|
|
|
/** Updates the scale on the active portion of the track. */
|
|
_updateTrackUI(source: _MatSliderThumb): void {
|
|
if (this._skipUpdate()) {
|
|
return;
|
|
}
|
|
|
|
this._isRange
|
|
? this._updateTrackUIRange(source as _MatSliderRangeThumb)
|
|
: this._updateTrackUINonRange(source as _MatSliderThumb);
|
|
}
|
|
|
|
private _updateTrackUIRange(source: _MatSliderRangeThumb): void {
|
|
const sibling = source.getSibling();
|
|
if (!sibling || !this._cachedWidth) {
|
|
return;
|
|
}
|
|
|
|
const activePercentage = Math.abs(sibling.translateX - source.translateX) / this._cachedWidth;
|
|
|
|
if (source._isLeftThumb && this._cachedWidth) {
|
|
this._setTrackActiveStyles({
|
|
left: 'auto',
|
|
right: `${this._cachedWidth - sibling.translateX}px`,
|
|
transformOrigin: 'right',
|
|
transform: `scaleX(${activePercentage})`,
|
|
});
|
|
} else {
|
|
this._setTrackActiveStyles({
|
|
left: `${sibling.translateX}px`,
|
|
right: 'auto',
|
|
transformOrigin: 'left',
|
|
transform: `scaleX(${activePercentage})`,
|
|
});
|
|
}
|
|
}
|
|
|
|
private _updateTrackUINonRange(source: _MatSliderThumb): void {
|
|
this._isRtl
|
|
? this._setTrackActiveStyles({
|
|
left: 'auto',
|
|
right: '0px',
|
|
transformOrigin: 'right',
|
|
transform: `scaleX(${1 - source.fillPercentage})`,
|
|
})
|
|
: this._setTrackActiveStyles({
|
|
left: '0px',
|
|
right: 'auto',
|
|
transformOrigin: 'left',
|
|
transform: `scaleX(${source.fillPercentage})`,
|
|
});
|
|
}
|
|
|
|
// Tick mark update conditions
|
|
//
|
|
// 1. Value
|
|
// - Reason: a tick mark which was once active might now be inactive or vice versa.
|
|
// 2. Min, max, or step
|
|
// - Reason #1: the number of tick marks may have changed.
|
|
// - Reason #2: The value may have silently changed.
|
|
|
|
/** Updates the dots along the slider track. */
|
|
_updateTickMarkUI(): void {
|
|
if (
|
|
!this.showTickMarks ||
|
|
this.step === undefined ||
|
|
this.min === undefined ||
|
|
this.max === undefined
|
|
) {
|
|
return;
|
|
}
|
|
const step = this.step > 0 ? this.step : 1;
|
|
this._isRange ? this._updateTickMarkUIRange(step) : this._updateTickMarkUINonRange(step);
|
|
|
|
if (this._isRtl) {
|
|
this._tickMarks.reverse();
|
|
}
|
|
}
|
|
|
|
private _updateTickMarkUINonRange(step: number): void {
|
|
const value = this._getValue();
|
|
let numActive = Math.max(Math.round((value - this.min) / step), 0);
|
|
let numInactive = Math.max(Math.round((this.max - value) / step), 0);
|
|
this._isRtl ? numActive++ : numInactive++;
|
|
|
|
this._tickMarks = Array(numActive)
|
|
.fill(_MatTickMark.ACTIVE)
|
|
.concat(Array(numInactive).fill(_MatTickMark.INACTIVE));
|
|
}
|
|
|
|
private _updateTickMarkUIRange(step: number): void {
|
|
const endValue = this._getValue();
|
|
const startValue = this._getValue(_MatThumb.START);
|
|
|
|
const numInactiveBeforeStartThumb = Math.max(Math.round((startValue - this.min) / step), 0);
|
|
const numActive = Math.max(Math.round((endValue - startValue) / step) + 1, 0);
|
|
const numInactiveAfterEndThumb = Math.max(Math.round((this.max - endValue) / step), 0);
|
|
this._tickMarks = Array(numInactiveBeforeStartThumb)
|
|
.fill(_MatTickMark.INACTIVE)
|
|
.concat(
|
|
Array(numActive).fill(_MatTickMark.ACTIVE),
|
|
Array(numInactiveAfterEndThumb).fill(_MatTickMark.INACTIVE),
|
|
);
|
|
}
|
|
|
|
/** Gets the slider thumb input of the given thumb position. */
|
|
_getInput(thumbPosition: _MatThumb): _MatSliderThumb | _MatSliderRangeThumb | undefined {
|
|
if (thumbPosition === _MatThumb.END && this._input) {
|
|
return this._input;
|
|
}
|
|
if (this._inputs?.length) {
|
|
return thumbPosition === _MatThumb.START ? this._inputs.first : this._inputs.last;
|
|
}
|
|
return;
|
|
}
|
|
|
|
/** Gets the slider thumb HTML input element of the given thumb position. */
|
|
_getThumb(thumbPosition: _MatThumb): _MatSliderVisualThumb {
|
|
return thumbPosition === _MatThumb.END ? this._thumbs?.last! : this._thumbs?.first!;
|
|
}
|
|
|
|
_setTransition(withAnimation: boolean): void {
|
|
this._hasAnimation = !this._platform.IOS && withAnimation && !this._noopAnimations;
|
|
this._elementRef.nativeElement.classList.toggle(
|
|
'mat-mdc-slider-with-animation',
|
|
this._hasAnimation,
|
|
);
|
|
}
|
|
|
|
/** Whether the given pointer event occurred within the bounds of the slider pointer's DOM Rect. */
|
|
_isCursorOnSliderThumb(event: PointerEvent, rect: DOMRect) {
|
|
const radius = rect.width / 2;
|
|
const centerX = rect.x + radius;
|
|
const centerY = rect.y + radius;
|
|
const dx = event.clientX - centerX;
|
|
const dy = event.clientY - centerY;
|
|
return Math.pow(dx, 2) + Math.pow(dy, 2) < Math.pow(radius, 2);
|
|
}
|
|
}
|
|
|
|
/** Ensures that there is not an invalid configuration for the slider thumb inputs. */
|
|
function _validateInputs(
|
|
isRange: boolean,
|
|
endInputElement: _MatSliderThumb | _MatSliderRangeThumb | undefined,
|
|
startInputElement: _MatSliderThumb | undefined,
|
|
): void {
|
|
const startValid =
|
|
!isRange || startInputElement?._hostElement.hasAttribute('matSliderStartThumb');
|
|
const endValid = endInputElement?._hostElement.hasAttribute(
|
|
isRange ? 'matSliderEndThumb' : 'matSliderThumb',
|
|
);
|
|
|
|
if (!startValid || !endValid) {
|
|
_throwInvalidInputConfigurationError();
|
|
}
|
|
}
|
|
|
|
function _throwInvalidInputConfigurationError(): void {
|
|
throw Error(`Invalid slider thumb input configuration!
|
|
|
|
Valid configurations are as follows:
|
|
|
|
<mat-slider>
|
|
<input matSliderThumb>
|
|
</mat-slider>
|
|
|
|
or
|
|
|
|
<mat-slider>
|
|
<input matSliderStartThumb>
|
|
<input matSliderEndThumb>
|
|
</mat-slider>
|
|
`);
|
|
}
|