sass-references/angular-material/material/slider/slider.ts

981 lines
29 KiB
TypeScript
Raw Normal View History

2024-12-06 10:42:08 +08:00
/**
* @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>
`);
}