826 lines
24 KiB
TypeScript
826 lines
24 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 {
|
|
booleanAttribute,
|
|
ChangeDetectorRef,
|
|
Directive,
|
|
ElementRef,
|
|
EventEmitter,
|
|
forwardRef,
|
|
inject,
|
|
Input,
|
|
NgZone,
|
|
numberAttribute,
|
|
OnDestroy,
|
|
Output,
|
|
signal,
|
|
} from '@angular/core';
|
|
import {ControlValueAccessor, FormControl, NG_VALUE_ACCESSOR} from '@angular/forms';
|
|
import {Subject} from 'rxjs';
|
|
import {
|
|
_MatThumb,
|
|
MatSliderDragEvent,
|
|
_MatSlider,
|
|
_MatSliderRangeThumb,
|
|
_MatSliderThumb,
|
|
MAT_SLIDER_RANGE_THUMB,
|
|
MAT_SLIDER_THUMB,
|
|
MAT_SLIDER,
|
|
} from './slider-interface';
|
|
import {Platform} from '@angular/cdk/platform';
|
|
|
|
/**
|
|
* Provider that allows the slider thumb to register as a ControlValueAccessor.
|
|
* @docs-private
|
|
*/
|
|
export const MAT_SLIDER_THUMB_VALUE_ACCESSOR: any = {
|
|
provide: NG_VALUE_ACCESSOR,
|
|
useExisting: forwardRef(() => MatSliderThumb),
|
|
multi: true,
|
|
};
|
|
|
|
/**
|
|
* Provider that allows the range slider thumb to register as a ControlValueAccessor.
|
|
* @docs-private
|
|
*/
|
|
export const MAT_SLIDER_RANGE_THUMB_VALUE_ACCESSOR: any = {
|
|
provide: NG_VALUE_ACCESSOR,
|
|
useExisting: forwardRef(() => MatSliderRangeThumb),
|
|
multi: true,
|
|
};
|
|
|
|
/**
|
|
* Directive that adds slider-specific behaviors to an input element inside `<mat-slider>`.
|
|
* Up to two may be placed inside of a `<mat-slider>`.
|
|
*
|
|
* If one is used, the selector `matSliderThumb` must be used, and the outcome will be a normal
|
|
* slider. If two are used, the selectors `matSliderStartThumb` and `matSliderEndThumb` must be
|
|
* used, and the outcome will be a range slider with two slider thumbs.
|
|
*/
|
|
@Directive({
|
|
selector: 'input[matSliderThumb]',
|
|
exportAs: 'matSliderThumb',
|
|
host: {
|
|
'class': 'mdc-slider__input',
|
|
'type': 'range',
|
|
'[attr.aria-valuetext]': '_valuetext()',
|
|
'(change)': '_onChange()',
|
|
'(input)': '_onInput()',
|
|
// TODO(wagnermaciel): Consider using a global event listener instead.
|
|
// Reason: I have found a semi-consistent way to mouse up without triggering this event.
|
|
'(blur)': '_onBlur()',
|
|
'(focus)': '_onFocus()',
|
|
},
|
|
providers: [
|
|
MAT_SLIDER_THUMB_VALUE_ACCESSOR,
|
|
{provide: MAT_SLIDER_THUMB, useExisting: MatSliderThumb},
|
|
],
|
|
})
|
|
export class MatSliderThumb implements _MatSliderThumb, OnDestroy, ControlValueAccessor {
|
|
readonly _ngZone = inject(NgZone);
|
|
readonly _elementRef = inject<ElementRef<HTMLInputElement>>(ElementRef);
|
|
readonly _cdr = inject(ChangeDetectorRef);
|
|
protected _slider = inject<_MatSlider>(MAT_SLIDER);
|
|
|
|
@Input({transform: numberAttribute})
|
|
get value(): number {
|
|
return numberAttribute(this._hostElement.value, 0);
|
|
}
|
|
set value(value: number) {
|
|
value = isNaN(value) ? 0 : value;
|
|
const stringValue = value + '';
|
|
if (!this._hasSetInitialValue) {
|
|
this._initialValue = stringValue;
|
|
return;
|
|
}
|
|
if (this._isActive) {
|
|
return;
|
|
}
|
|
this._setValue(stringValue);
|
|
}
|
|
|
|
/**
|
|
* Handles programmatic value setting. This has been split out to
|
|
* allow the range thumb to override it and add additional necessary logic.
|
|
*/
|
|
protected _setValue(value: string) {
|
|
this._hostElement.value = value;
|
|
this._updateThumbUIByValue();
|
|
this._slider._onValueChange(this);
|
|
this._cdr.detectChanges();
|
|
this._slider._cdr.markForCheck();
|
|
}
|
|
|
|
/** Event emitted when the `value` is changed. */
|
|
@Output() readonly valueChange: EventEmitter<number> = new EventEmitter<number>();
|
|
|
|
/** Event emitted when the slider thumb starts being dragged. */
|
|
@Output() readonly dragStart: EventEmitter<MatSliderDragEvent> =
|
|
new EventEmitter<MatSliderDragEvent>();
|
|
|
|
/** Event emitted when the slider thumb stops being dragged. */
|
|
@Output() readonly dragEnd: EventEmitter<MatSliderDragEvent> =
|
|
new EventEmitter<MatSliderDragEvent>();
|
|
|
|
/**
|
|
* The current translateX in px of the slider visual thumb.
|
|
* @docs-private
|
|
*/
|
|
get translateX(): number {
|
|
if (this._slider.min >= this._slider.max) {
|
|
this._translateX = this._tickMarkOffset;
|
|
return this._translateX;
|
|
}
|
|
if (this._translateX === undefined) {
|
|
this._translateX = this._calcTranslateXByValue();
|
|
}
|
|
return this._translateX;
|
|
}
|
|
set translateX(v: number) {
|
|
this._translateX = v;
|
|
}
|
|
private _translateX: number | undefined;
|
|
|
|
/**
|
|
* Indicates whether this thumb is the start or end thumb.
|
|
* @docs-private
|
|
*/
|
|
thumbPosition: _MatThumb = _MatThumb.END;
|
|
|
|
/** @docs-private */
|
|
get min(): number {
|
|
return numberAttribute(this._hostElement.min, 0);
|
|
}
|
|
set min(v: number) {
|
|
this._hostElement.min = v + '';
|
|
this._cdr.detectChanges();
|
|
}
|
|
|
|
/** @docs-private */
|
|
get max(): number {
|
|
return numberAttribute(this._hostElement.max, 0);
|
|
}
|
|
set max(v: number) {
|
|
this._hostElement.max = v + '';
|
|
this._cdr.detectChanges();
|
|
}
|
|
|
|
get step(): number {
|
|
return numberAttribute(this._hostElement.step, 0);
|
|
}
|
|
set step(v: number) {
|
|
this._hostElement.step = v + '';
|
|
this._cdr.detectChanges();
|
|
}
|
|
|
|
/** @docs-private */
|
|
get disabled(): boolean {
|
|
return booleanAttribute(this._hostElement.disabled);
|
|
}
|
|
set disabled(v: boolean) {
|
|
this._hostElement.disabled = v;
|
|
this._cdr.detectChanges();
|
|
|
|
if (this._slider.disabled !== this.disabled) {
|
|
this._slider.disabled = this.disabled;
|
|
}
|
|
}
|
|
|
|
/** The percentage of the slider that coincides with the value. */
|
|
get percentage(): number {
|
|
if (this._slider.min >= this._slider.max) {
|
|
return this._slider._isRtl ? 1 : 0;
|
|
}
|
|
return (this.value - this._slider.min) / (this._slider.max - this._slider.min);
|
|
}
|
|
|
|
/** @docs-private */
|
|
get fillPercentage(): number {
|
|
if (!this._slider._cachedWidth) {
|
|
return this._slider._isRtl ? 1 : 0;
|
|
}
|
|
if (this._translateX === 0) {
|
|
return 0;
|
|
}
|
|
return this.translateX / this._slider._cachedWidth;
|
|
}
|
|
|
|
/** The host native HTML input element. */
|
|
_hostElement = this._elementRef.nativeElement;
|
|
|
|
/** The aria-valuetext string representation of the input's value. */
|
|
_valuetext = signal('');
|
|
|
|
/** The radius of a native html slider's knob. */
|
|
_knobRadius: number = 8;
|
|
|
|
/** The distance in px from the start of the slider track to the first tick mark. */
|
|
_tickMarkOffset = 3;
|
|
|
|
/** Whether user's cursor is currently in a mouse down state on the input. */
|
|
_isActive: boolean = false;
|
|
|
|
/** Whether the input is currently focused (either by tab or after clicking). */
|
|
_isFocused: boolean = false;
|
|
|
|
/** Used to relay updates to _isFocused to the slider visual thumbs. */
|
|
private _setIsFocused(v: boolean): void {
|
|
this._isFocused = v;
|
|
}
|
|
|
|
/**
|
|
* Whether the initial value has been set.
|
|
* This exists because the initial value cannot be immediately set because the min and max
|
|
* must first be relayed from the parent MatSlider component, which can only happen later
|
|
* in the component lifecycle.
|
|
*/
|
|
private _hasSetInitialValue: boolean = false;
|
|
|
|
/** The stored initial value. */
|
|
_initialValue: string | undefined;
|
|
|
|
/** Defined when a user is using a form control to manage slider value & validation. */
|
|
private _formControl: FormControl | undefined;
|
|
|
|
/** Emits when the component is destroyed. */
|
|
protected readonly _destroyed = new Subject<void>();
|
|
|
|
/**
|
|
* Indicates whether UI updates should be skipped.
|
|
*
|
|
* This flag is used to avoid flickering
|
|
* when correcting values on pointer up/down.
|
|
*/
|
|
_skipUIUpdate: boolean = false;
|
|
|
|
/** Callback called when the slider input value changes. */
|
|
protected _onChangeFn: ((value: any) => void) | undefined;
|
|
|
|
/** Callback called when the slider input has been touched. */
|
|
private _onTouchedFn: () => void = () => {};
|
|
|
|
/**
|
|
* Whether the NgModel has been initialized.
|
|
*
|
|
* This flag is used to ignore ghost null calls to
|
|
* writeValue which can break slider initialization.
|
|
*
|
|
* See https://github.com/angular/angular/issues/14988.
|
|
*/
|
|
protected _isControlInitialized = false;
|
|
|
|
private _platform = inject(Platform);
|
|
|
|
constructor(...args: unknown[]);
|
|
|
|
constructor() {
|
|
this._ngZone.runOutsideAngular(() => {
|
|
this._hostElement.addEventListener('pointerdown', this._onPointerDown.bind(this));
|
|
this._hostElement.addEventListener('pointermove', this._onPointerMove.bind(this));
|
|
this._hostElement.addEventListener('pointerup', this._onPointerUp.bind(this));
|
|
});
|
|
}
|
|
|
|
ngOnDestroy(): void {
|
|
this._hostElement.removeEventListener('pointerdown', this._onPointerDown);
|
|
this._hostElement.removeEventListener('pointermove', this._onPointerMove);
|
|
this._hostElement.removeEventListener('pointerup', this._onPointerUp);
|
|
this._destroyed.next();
|
|
this._destroyed.complete();
|
|
this.dragStart.complete();
|
|
this.dragEnd.complete();
|
|
}
|
|
|
|
/** @docs-private */
|
|
initProps(): void {
|
|
this._updateWidthInactive();
|
|
|
|
// If this or the parent slider is disabled, just make everything disabled.
|
|
if (this.disabled !== this._slider.disabled) {
|
|
// The MatSlider setter for disabled will relay this and disable both inputs.
|
|
this._slider.disabled = true;
|
|
}
|
|
|
|
this.step = this._slider.step;
|
|
this.min = this._slider.min;
|
|
this.max = this._slider.max;
|
|
this._initValue();
|
|
}
|
|
|
|
/** @docs-private */
|
|
initUI(): void {
|
|
this._updateThumbUIByValue();
|
|
}
|
|
|
|
_initValue(): void {
|
|
this._hasSetInitialValue = true;
|
|
if (this._initialValue === undefined) {
|
|
this.value = this._getDefaultValue();
|
|
} else {
|
|
this._hostElement.value = this._initialValue;
|
|
this._updateThumbUIByValue();
|
|
this._slider._onValueChange(this);
|
|
this._cdr.detectChanges();
|
|
}
|
|
}
|
|
|
|
_getDefaultValue(): number {
|
|
return this.min;
|
|
}
|
|
|
|
_onBlur(): void {
|
|
this._setIsFocused(false);
|
|
this._onTouchedFn();
|
|
}
|
|
|
|
_onFocus(): void {
|
|
this._slider._setTransition(false);
|
|
this._slider._updateTrackUI(this);
|
|
this._setIsFocused(true);
|
|
}
|
|
|
|
_onChange(): void {
|
|
this.valueChange.emit(this.value);
|
|
// only used to handle the edge case where user
|
|
// mousedown on the slider then uses arrow keys.
|
|
if (this._isActive) {
|
|
this._updateThumbUIByValue({withAnimation: true});
|
|
}
|
|
}
|
|
|
|
_onInput(): void {
|
|
this._onChangeFn?.(this.value);
|
|
// handles arrowing and updating the value when
|
|
// a step is defined.
|
|
if (this._slider.step || !this._isActive) {
|
|
this._updateThumbUIByValue({withAnimation: true});
|
|
}
|
|
this._slider._onValueChange(this);
|
|
}
|
|
|
|
_onNgControlValueChange(): void {
|
|
// only used to handle when the value change
|
|
// originates outside of the slider.
|
|
if (!this._isActive || !this._isFocused) {
|
|
this._slider._onValueChange(this);
|
|
this._updateThumbUIByValue();
|
|
}
|
|
this._slider.disabled = this._formControl!.disabled;
|
|
}
|
|
|
|
_onPointerDown(event: PointerEvent): void {
|
|
if (this.disabled || event.button !== 0) {
|
|
return;
|
|
}
|
|
|
|
// On IOS, dragging only works if the pointer down happens on the
|
|
// slider thumb and the slider does not receive focus from pointer events.
|
|
if (this._platform.IOS) {
|
|
const isCursorOnSliderThumb = this._slider._isCursorOnSliderThumb(
|
|
event,
|
|
this._slider._getThumb(this.thumbPosition)._hostElement.getBoundingClientRect(),
|
|
);
|
|
|
|
this._isActive = isCursorOnSliderThumb;
|
|
this._updateWidthActive();
|
|
this._slider._updateDimensions();
|
|
return;
|
|
}
|
|
|
|
this._isActive = true;
|
|
this._setIsFocused(true);
|
|
this._updateWidthActive();
|
|
this._slider._updateDimensions();
|
|
|
|
// Does nothing if a step is defined because we
|
|
// want the value to snap to the values on input.
|
|
if (!this._slider.step) {
|
|
this._updateThumbUIByPointerEvent(event, {withAnimation: true});
|
|
}
|
|
|
|
if (!this.disabled) {
|
|
this._handleValueCorrection(event);
|
|
this.dragStart.emit({source: this, parent: this._slider, value: this.value});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Corrects the value of the slider on pointer up/down.
|
|
*
|
|
* Called on pointer down and up because the value is set based
|
|
* on the inactive width instead of the active width.
|
|
*/
|
|
private _handleValueCorrection(event: PointerEvent): void {
|
|
// Don't update the UI with the current value! The value on pointerdown
|
|
// and pointerup is calculated in the split second before the input(s)
|
|
// resize. See _updateWidthInactive() and _updateWidthActive() for more
|
|
// details.
|
|
this._skipUIUpdate = true;
|
|
|
|
// Note that this function gets triggered before the actual value of the
|
|
// slider is updated. This means if we were to set the value here, it
|
|
// would immediately be overwritten. Using setTimeout ensures the setting
|
|
// of the value happens after the value has been updated by the
|
|
// pointerdown event.
|
|
setTimeout(() => {
|
|
this._skipUIUpdate = false;
|
|
this._fixValue(event);
|
|
}, 0);
|
|
}
|
|
|
|
/** Corrects the value of the slider based on the pointer event's position. */
|
|
_fixValue(event: PointerEvent): void {
|
|
const xPos = event.clientX - this._slider._cachedLeft;
|
|
const width = this._slider._cachedWidth;
|
|
const step = this._slider.step === 0 ? 1 : this._slider.step;
|
|
const numSteps = Math.floor((this._slider.max - this._slider.min) / step);
|
|
const percentage = this._slider._isRtl ? 1 - xPos / width : xPos / width;
|
|
|
|
// To ensure the percentage is rounded to the necessary number of decimals.
|
|
const fixedPercentage = Math.round(percentage * numSteps) / numSteps;
|
|
|
|
const impreciseValue =
|
|
fixedPercentage * (this._slider.max - this._slider.min) + this._slider.min;
|
|
const value = Math.round(impreciseValue / step) * step;
|
|
const prevValue = this.value;
|
|
|
|
if (value === prevValue) {
|
|
// Because we prevented UI updates, if it turns out that the race
|
|
// condition didn't happen and the value is already correct, we
|
|
// have to apply the ui updates now.
|
|
this._slider._onValueChange(this);
|
|
this._slider.step > 0
|
|
? this._updateThumbUIByValue()
|
|
: this._updateThumbUIByPointerEvent(event, {withAnimation: this._slider._hasAnimation});
|
|
return;
|
|
}
|
|
|
|
this.value = value;
|
|
this.valueChange.emit(this.value);
|
|
this._onChangeFn?.(this.value);
|
|
this._slider._onValueChange(this);
|
|
this._slider.step > 0
|
|
? this._updateThumbUIByValue()
|
|
: this._updateThumbUIByPointerEvent(event, {withAnimation: this._slider._hasAnimation});
|
|
}
|
|
|
|
_onPointerMove(event: PointerEvent): void {
|
|
// Again, does nothing if a step is defined because
|
|
// we want the value to snap to the values on input.
|
|
if (!this._slider.step && this._isActive) {
|
|
this._updateThumbUIByPointerEvent(event);
|
|
}
|
|
}
|
|
|
|
_onPointerUp(): void {
|
|
if (this._isActive) {
|
|
this._isActive = false;
|
|
if (this._platform.SAFARI) {
|
|
this._setIsFocused(false);
|
|
}
|
|
this.dragEnd.emit({source: this, parent: this._slider, value: this.value});
|
|
|
|
// This setTimeout is to prevent the pointerup from triggering a value
|
|
// change on the input based on the inactive width. It's not clear why
|
|
// but for some reason on IOS this race condition is even more common so
|
|
// the timeout needs to be increased.
|
|
setTimeout(() => this._updateWidthInactive(), this._platform.IOS ? 10 : 0);
|
|
}
|
|
}
|
|
|
|
_clamp(v: number): number {
|
|
const min = this._tickMarkOffset;
|
|
const max = this._slider._cachedWidth - this._tickMarkOffset;
|
|
return Math.max(Math.min(v, max), min);
|
|
}
|
|
|
|
_calcTranslateXByValue(): number {
|
|
if (this._slider._isRtl) {
|
|
return (
|
|
(1 - this.percentage) * (this._slider._cachedWidth - this._tickMarkOffset * 2) +
|
|
this._tickMarkOffset
|
|
);
|
|
}
|
|
return (
|
|
this.percentage * (this._slider._cachedWidth - this._tickMarkOffset * 2) +
|
|
this._tickMarkOffset
|
|
);
|
|
}
|
|
|
|
_calcTranslateXByPointerEvent(event: PointerEvent): number {
|
|
return event.clientX - this._slider._cachedLeft;
|
|
}
|
|
|
|
/**
|
|
* Used to set the slider width to the correct
|
|
* dimensions while the user is dragging.
|
|
*/
|
|
_updateWidthActive(): void {}
|
|
|
|
/**
|
|
* Sets the slider input to disproportionate dimensions to allow for touch
|
|
* events to be captured on touch devices.
|
|
*/
|
|
_updateWidthInactive(): void {
|
|
this._hostElement.style.padding = `0 ${this._slider._inputPadding}px`;
|
|
this._hostElement.style.width = `calc(100% + ${
|
|
this._slider._inputPadding - this._tickMarkOffset * 2
|
|
}px)`;
|
|
this._hostElement.style.left = `-${this._slider._rippleRadius - this._tickMarkOffset}px`;
|
|
}
|
|
|
|
_updateThumbUIByValue(options?: {withAnimation: boolean}): void {
|
|
this.translateX = this._clamp(this._calcTranslateXByValue());
|
|
this._updateThumbUI(options);
|
|
}
|
|
|
|
_updateThumbUIByPointerEvent(event: PointerEvent, options?: {withAnimation: boolean}): void {
|
|
this.translateX = this._clamp(this._calcTranslateXByPointerEvent(event));
|
|
this._updateThumbUI(options);
|
|
}
|
|
|
|
_updateThumbUI(options?: {withAnimation: boolean}) {
|
|
this._slider._setTransition(!!options?.withAnimation);
|
|
this._slider._onTranslateXChange(this);
|
|
}
|
|
|
|
/**
|
|
* Sets the input's value.
|
|
* @param value The new value of the input
|
|
* @docs-private
|
|
*/
|
|
writeValue(value: any): void {
|
|
if (this._isControlInitialized || value !== null) {
|
|
this.value = value;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Registers a callback to be invoked when the input's value changes from user input.
|
|
* @param fn The callback to register
|
|
* @docs-private
|
|
*/
|
|
registerOnChange(fn: any): void {
|
|
this._onChangeFn = fn;
|
|
this._isControlInitialized = true;
|
|
}
|
|
|
|
/**
|
|
* Registers a callback to be invoked when the input is blurred by the user.
|
|
* @param fn The callback to register
|
|
* @docs-private
|
|
*/
|
|
registerOnTouched(fn: any): void {
|
|
this._onTouchedFn = fn;
|
|
}
|
|
|
|
/**
|
|
* Sets the disabled state of the slider.
|
|
* @param isDisabled The new disabled state
|
|
* @docs-private
|
|
*/
|
|
setDisabledState(isDisabled: boolean): void {
|
|
this.disabled = isDisabled;
|
|
}
|
|
|
|
focus(): void {
|
|
this._hostElement.focus();
|
|
}
|
|
|
|
blur(): void {
|
|
this._hostElement.blur();
|
|
}
|
|
}
|
|
|
|
@Directive({
|
|
selector: 'input[matSliderStartThumb], input[matSliderEndThumb]',
|
|
exportAs: 'matSliderRangeThumb',
|
|
providers: [
|
|
MAT_SLIDER_RANGE_THUMB_VALUE_ACCESSOR,
|
|
{provide: MAT_SLIDER_RANGE_THUMB, useExisting: MatSliderRangeThumb},
|
|
],
|
|
})
|
|
export class MatSliderRangeThumb extends MatSliderThumb implements _MatSliderRangeThumb {
|
|
override readonly _cdr = inject(ChangeDetectorRef);
|
|
|
|
/** @docs-private */
|
|
getSibling(): _MatSliderRangeThumb | undefined {
|
|
if (!this._sibling) {
|
|
this._sibling = this._slider._getInput(this._isEndThumb ? _MatThumb.START : _MatThumb.END) as
|
|
| MatSliderRangeThumb
|
|
| undefined;
|
|
}
|
|
return this._sibling;
|
|
}
|
|
private _sibling: MatSliderRangeThumb | undefined;
|
|
|
|
/**
|
|
* Returns the minimum translateX position allowed for this slider input's visual thumb.
|
|
* @docs-private
|
|
*/
|
|
getMinPos(): number {
|
|
const sibling = this.getSibling();
|
|
if (!this._isLeftThumb && sibling) {
|
|
return sibling.translateX;
|
|
}
|
|
return this._tickMarkOffset;
|
|
}
|
|
|
|
/**
|
|
* Returns the maximum translateX position allowed for this slider input's visual thumb.
|
|
* @docs-private
|
|
*/
|
|
getMaxPos(): number {
|
|
const sibling = this.getSibling();
|
|
if (this._isLeftThumb && sibling) {
|
|
return sibling.translateX;
|
|
}
|
|
return this._slider._cachedWidth - this._tickMarkOffset;
|
|
}
|
|
|
|
_setIsLeftThumb(): void {
|
|
this._isLeftThumb =
|
|
(this._isEndThumb && this._slider._isRtl) || (!this._isEndThumb && !this._slider._isRtl);
|
|
}
|
|
|
|
/** Whether this slider corresponds to the input on the left hand side. */
|
|
_isLeftThumb: boolean;
|
|
|
|
/** Whether this slider corresponds to the input with greater value. */
|
|
_isEndThumb: boolean;
|
|
|
|
constructor(...args: unknown[]);
|
|
|
|
constructor() {
|
|
super();
|
|
|
|
this._isEndThumb = this._hostElement.hasAttribute('matSliderEndThumb');
|
|
this._setIsLeftThumb();
|
|
this.thumbPosition = this._isEndThumb ? _MatThumb.END : _MatThumb.START;
|
|
}
|
|
|
|
override _getDefaultValue(): number {
|
|
return this._isEndThumb && this._slider._isRange ? this.max : this.min;
|
|
}
|
|
|
|
override _onInput(): void {
|
|
super._onInput();
|
|
this._updateSibling();
|
|
if (!this._isActive) {
|
|
this._updateWidthInactive();
|
|
}
|
|
}
|
|
|
|
override _onNgControlValueChange(): void {
|
|
super._onNgControlValueChange();
|
|
this.getSibling()?._updateMinMax();
|
|
}
|
|
|
|
override _onPointerDown(event: PointerEvent): void {
|
|
if (this.disabled || event.button !== 0) {
|
|
return;
|
|
}
|
|
if (this._sibling) {
|
|
this._sibling._updateWidthActive();
|
|
this._sibling._hostElement.classList.add('mat-mdc-slider-input-no-pointer-events');
|
|
}
|
|
super._onPointerDown(event);
|
|
}
|
|
|
|
override _onPointerUp(): void {
|
|
super._onPointerUp();
|
|
if (this._sibling) {
|
|
setTimeout(() => {
|
|
this._sibling!._updateWidthInactive();
|
|
this._sibling!._hostElement.classList.remove('mat-mdc-slider-input-no-pointer-events');
|
|
});
|
|
}
|
|
}
|
|
|
|
override _onPointerMove(event: PointerEvent): void {
|
|
super._onPointerMove(event);
|
|
if (!this._slider.step && this._isActive) {
|
|
this._updateSibling();
|
|
}
|
|
}
|
|
|
|
override _fixValue(event: PointerEvent): void {
|
|
super._fixValue(event);
|
|
this._sibling?._updateMinMax();
|
|
}
|
|
|
|
override _clamp(v: number): number {
|
|
return Math.max(Math.min(v, this.getMaxPos()), this.getMinPos());
|
|
}
|
|
|
|
_updateMinMax(): void {
|
|
const sibling = this.getSibling();
|
|
if (!sibling) {
|
|
return;
|
|
}
|
|
if (this._isEndThumb) {
|
|
this.min = Math.max(this._slider.min, sibling.value);
|
|
this.max = this._slider.max;
|
|
} else {
|
|
this.min = this._slider.min;
|
|
this.max = Math.min(this._slider.max, sibling.value);
|
|
}
|
|
}
|
|
|
|
override _updateWidthActive(): void {
|
|
const minWidth = this._slider._rippleRadius * 2 - this._slider._inputPadding * 2;
|
|
const maxWidth =
|
|
this._slider._cachedWidth + this._slider._inputPadding - minWidth - this._tickMarkOffset * 2;
|
|
const percentage =
|
|
this._slider.min < this._slider.max
|
|
? (this.max - this.min) / (this._slider.max - this._slider.min)
|
|
: 1;
|
|
const width = maxWidth * percentage + minWidth;
|
|
this._hostElement.style.width = `${width}px`;
|
|
this._hostElement.style.padding = `0 ${this._slider._inputPadding}px`;
|
|
}
|
|
|
|
override _updateWidthInactive(): void {
|
|
const sibling = this.getSibling();
|
|
if (!sibling) {
|
|
return;
|
|
}
|
|
const maxWidth = this._slider._cachedWidth - this._tickMarkOffset * 2;
|
|
const midValue = this._isEndThumb
|
|
? this.value - (this.value - sibling.value) / 2
|
|
: this.value + (sibling.value - this.value) / 2;
|
|
|
|
const _percentage = this._isEndThumb
|
|
? (this.max - midValue) / (this._slider.max - this._slider.min)
|
|
: (midValue - this.min) / (this._slider.max - this._slider.min);
|
|
|
|
const percentage = this._slider.min < this._slider.max ? _percentage : 1;
|
|
|
|
// Extend the native input width by the radius of the ripple
|
|
let ripplePadding = this._slider._rippleRadius;
|
|
|
|
// If one of the inputs is maximally sized (the value of both thumbs is
|
|
// equal to the min or max), make that input take up all of the width and
|
|
// make the other unselectable.
|
|
if (percentage === 1) {
|
|
ripplePadding = 48;
|
|
} else if (percentage === 0) {
|
|
ripplePadding = 0;
|
|
}
|
|
|
|
const width = maxWidth * percentage + ripplePadding;
|
|
this._hostElement.style.width = `${width}px`;
|
|
this._hostElement.style.padding = '0px';
|
|
|
|
if (this._isLeftThumb) {
|
|
this._hostElement.style.left = `-${this._slider._rippleRadius - this._tickMarkOffset}px`;
|
|
this._hostElement.style.right = 'auto';
|
|
} else {
|
|
this._hostElement.style.left = 'auto';
|
|
this._hostElement.style.right = `-${this._slider._rippleRadius - this._tickMarkOffset}px`;
|
|
}
|
|
}
|
|
|
|
_updateStaticStyles(): void {
|
|
this._hostElement.classList.toggle('mat-slider__right-input', !this._isLeftThumb);
|
|
}
|
|
|
|
private _updateSibling(): void {
|
|
const sibling = this.getSibling();
|
|
if (!sibling) {
|
|
return;
|
|
}
|
|
sibling._updateMinMax();
|
|
if (this._isActive) {
|
|
sibling._updateWidthActive();
|
|
} else {
|
|
sibling._updateWidthInactive();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sets the input's value.
|
|
* @param value The new value of the input
|
|
* @docs-private
|
|
*/
|
|
override writeValue(value: any): void {
|
|
if (this._isControlInitialized || value !== null) {
|
|
this.value = value;
|
|
this._updateWidthInactive();
|
|
this._updateSibling();
|
|
}
|
|
}
|
|
|
|
override _setValue(value: string) {
|
|
super._setValue(value);
|
|
this._updateWidthInactive();
|
|
this._updateSibling();
|
|
}
|
|
}
|