license import { AnimationFrame } from '@material/animation/animationframe' ; import { getCorrectPropertyName } from '@material/animation/util' ; import { MDCFoundation } from '@material/base/foundation' ; import { SpecificEventListener } from '@material/base/types' ; import { MDCSliderAdapter } from './adapter' ; import { attributes , cssClasses , numbers , strings } from './constants' ; import { Thumb , TickMark } from './types' ; enum AnimationKeys { SLIDER_UPDATE = 'slider_update' } const HAS_WINDOW = typeof window !== 'undefined' ; /**
 * Foundation class for slider. Responsibilities include:
 * - Updating slider values (internal state and DOM updates) based on client
 *   'x' position.
 * - Updating DOM after slider property updates (e.g. min, max).
 */ export class MDCSliderFoundation extends MDCFoundation < MDCSliderAdapter > { static SUPPORTS_POINTER_EVENTS = HAS_WINDOW && Boolean ( window . PointerEvent ) && ! isIOS ( ) ; private initialStylesRemoved = false ; private min ! : number ; private max ! : number ; private valueStart ! : number ; private value ! : number ; private rect ! : ClientRect ; private isDisabled = false ; private isDiscrete = false ; private step = numbers . STEP_SIZE ; private minRange = numbers . MIN_RANGE ; private numDecimalPlaces ! : number ; private hasTickMarks = false ; private isRange = false ; private thumb : Thumb | null = null ; private downEventClientX : number | null = null ; private valueStartBeforeDownEvent ! : number ; private valueBeforeDownEvent ! : number ; private startThumbKnobWidth = 0 ; private endThumbKnobWidth = 0 ; private readonly animFrame : AnimationFrame ; private mousedownOrTouchstartListener ! : SpecificEventListener < 'mousedown' | 'touchstart' > ; private moveListener ! : SpecificEventListener < 'pointermove' | 'mousemove' | 'touchmove' > ; private pointerdownListener ! : SpecificEventListener < 'pointerdown' > ; private pointerupListener ! : SpecificEventListener < 'pointerup' > ; private thumbMouseenterListener ! : SpecificEventListener < 'mouseenter' > ; private thumbMouseleaveListener ! : SpecificEventListener < 'mouseleave' > ; private inputStartChangeListener ! : SpecificEventListener < 'change' > ; private inputEndChangeListener ! : SpecificEventListener < 'change' > ; private inputStartFocusListener ! : SpecificEventListener < 'focus' > ; private inputEndFocusListener ! : SpecificEventListener < 'focus' > ; private inputStartBlurListener ! : SpecificEventListener < 'blur' > ; private inputEndBlurListener ! : SpecificEventListener < 'blur' > ; private resizeListener ! : SpecificEventListener < 'resize' > ; constructor ( adapter ? ) { super ( { ... MDCSliderFoundation . defaultAdapter , ... adapter } ) ; this . animFrame = new AnimationFrame ( ) ; } static override get defaultAdapter ( ) : MDCSliderAdapter { return { hasClass : ( ) => false , addClass : ( ) => undefined , removeClass : ( ) => undefined , addThumbClass : ( ) => undefined , removeThumbClass : ( ) => undefined , getAttribute : ( ) => null , getInputValue : ( ) => '' , setInputValue : ( ) => undefined , getInputAttribute : ( ) => null , setInputAttribute : ( ) => null , removeInputAttribute : ( ) => null , focusInput : ( ) => undefined , isInputFocused : ( ) => false , getThumbKnobWidth : ( ) => 0 , getValueIndicatorContainerWidth : ( ) => 0 , getThumbBoundingClientRect : ( ) => ( { top : 0 , right : 0 , bottom : 0 , left : 0 , width : 0 , height : 0 } as any ) , getBoundingClientRect : ( ) => ( { top : 0 , right : 0 , bottom : 0 , left : 0 , width : 0 , height : 0 } as any ) , isRTL : ( ) => false , setThumbStyleProperty : ( ) => undefined , removeThumbStyleProperty : ( ) => undefined , setTrackActiveStyleProperty : ( ) => undefined , removeTrackActiveStyleProperty : ( ) => undefined , setValueIndicatorText : ( ) => undefined , getValueToAriaValueTextFn : ( ) => null , updateTickMarks : ( ) => undefined , setPointerCapture : ( ) => undefined , emitChangeEvent : ( ) => undefined , emitInputEvent : ( ) => undefined , emitDragStartEvent : ( ) => undefined , emitDragEndEvent : ( ) => undefined , registerEventHandler : ( ) => undefined , deregisterEventHandler : ( ) => undefined , registerThumbEventHandler : ( ) => undefined , deregisterThumbEventHandler : ( ) => undefined , registerInputEventHandler : ( ) => undefined , deregisterInputEventHandler : ( ) => undefined , registerBodyEventHandler : ( ) => undefined , deregisterBodyEventHandler : ( ) => undefined , registerWindowEventHandler : ( ) => undefined , deregisterWindowEventHandler : ( ) => undefined , } ; } override init ( ) { this . isDisabled = this . adapter . hasClass ( cssClasses . DISABLED ) ; this . isDiscrete = this . adapter . hasClass ( cssClasses . DISCRETE ) ; this . hasTickMarks = this . adapter . hasClass ( cssClasses . TICK_MARKS ) ; this . isRange = this . adapter . hasClass ( cssClasses . RANGE ) ; const min = this . convertAttributeValueToNumber ( this . adapter . getInputAttribute ( attributes . INPUT_MIN , this . isRange ? Thumb . START : Thumb . END ) , attributes . INPUT_MIN ) ; const max = this . convertAttributeValueToNumber ( this . adapter . getInputAttribute ( attributes . INPUT_MAX , Thumb . END ) , attributes . INPUT_MAX ) ; const value = this . convertAttributeValueToNumber ( this . adapter . getInputAttribute ( attributes . INPUT_VALUE , Thumb . END ) , attributes . INPUT_VALUE ) ; const valueStart = this . isRange ? this . convertAttributeValueToNumber ( this . adapter . getInputAttribute ( attributes . INPUT_VALUE , Thumb . START ) , attributes . INPUT_VALUE ) : min ; const stepAttr = this . adapter . getInputAttribute ( attributes . INPUT_STEP , Thumb . END ) ; const step = stepAttr ? this . convertAttributeValueToNumber ( stepAttr , attributes . INPUT_STEP ) : this . step ; const minRangeAttr = this . adapter . getAttribute ( attributes . DATA_MIN_RANGE ) ; const minRange = minRangeAttr ? this . convertAttributeValueToNumber ( minRangeAttr , attributes . DATA_MIN_RANGE ) : this . minRange ; this . validateProperties ( { min , max , value , valueStart , step , minRange } ) ; this . min = min ; this . max = max ; this . value = value ; this . valueStart = valueStart ; this . step = step ; this . minRange = minRange ; this . numDecimalPlaces = getNumDecimalPlaces ( this . step ) ; this . valueBeforeDownEvent = value ; this . valueStartBeforeDownEvent = valueStart ; this . mousedownOrTouchstartListener = this . handleMousedownOrTouchstart . bind ( this ) ; this . moveListener = this . handleMove . bind ( this ) ; this . pointerdownListener = this . handlePointerdown . bind ( this ) ; this . pointerupListener = this . handlePointerup . bind ( this ) ; this . thumbMouseenterListener = this . handleThumbMouseenter . bind ( this ) ; this . thumbMouseleaveListener = this . handleThumbMouseleave . bind ( this ) ; this . inputStartChangeListener = ( ) => { this . handleInputChange ( Thumb . START ) ; } ; this . inputEndChangeListener = ( ) => { this . handleInputChange ( Thumb . END ) ; } ; this . inputStartFocusListener = ( ) => { this . handleInputFocus ( Thumb . START ) ; } ; this . inputEndFocusListener = ( ) => { this . handleInputFocus ( Thumb . END ) ; } ; this . inputStartBlurListener = ( ) => { this . handleInputBlur ( Thumb . START ) ; } ; this . inputEndBlurListener = ( ) => { this . handleInputBlur ( Thumb . END ) ; } ; this . resizeListener = this . handleResize . bind ( this ) ; this . registerEventHandlers ( ) ; } override destroy ( ) { this . deregisterEventHandlers ( ) ; } setMin ( value ) { this . min = value ; if ( ! this . isRange ) { this . valueStart = value ; } this . updateUI ( ) ; } setMax ( value ) { this . max = value ; this . updateUI ( ) ; } getMin ( ) { return this . min ; } getMax ( ) { return this . max ; } /**
     * - For single point sliders, returns the thumb value.
     * - For range (two-thumb) sliders, returns the end thumb's value.
     */ getValue ( ) { return this . value ; } /**
     * - For single point sliders, sets the thumb value.
     * - For range (two-thumb) sliders, sets the end thumb's value.
     */ setValue ( value ) { if ( this . isRange && value < this . valueStart + this . minRange ) { throw new Error ( `end thumb value (${ value }) must be >= start thumb ` + `value (${ this . valueStart }) + min range (${ this . minRange })` ) ; } this . updateValue ( value , Thumb . END ) ; } return getValueStart ( ) { if ( ! this . isRange ) { throw new Error ( '`valueStart` is only applicable for range sliders.' ) ; } return this . valueStart ; } /**
     * Only applicable for range sliders. Sets the start thumb's value.
     */ setValueStart ( valueStart ) { if ( ! this . isRange ) { throw new Error ( '`valueStart` is only applicable for range sliders.' ) ; } if ( this . isRange && valueStart > this . value - this . minRange ) { throw new Error ( `start thumb value (${ valueStart }) must be <= end thumb ` + `value (${ this . value }) - min range (${ this . minRange })` ) ; } this . updateValue ( valueStart , Thumb . START ) ; } setStep ( value ) { this . step = value ; this . numDecimalPlaces = getNumDecimalPlaces ( value ) ; this . updateUI ( ) ; } /**
     * Only applicable for range sliders. Sets the minimum difference between the
     * start and end values.
     */ setMinRange ( value ) { if ( ! this . isRange ) { throw new Error ( '`minRange` is only applicable for range sliders.' ) ; } if ( value < 0 ) { throw new Error ( '`minRange` must be non-negative.' ) ; } if ( this . value - this . valueStart < value ) { throw new Error ( `start thumb value (${ this . valueStart }) and end thumb value ` + `(${ this . value }) must differ by at least ${ value }.` ) ; } this . minRange = value ; } setIsDiscrete ( value ) { this . isDiscrete = value ; this . updateValueIndicatorUI ( ) ; this . updateTickMarksUI ( ) ; } getStep ( ) { return this . step ; } getMinRange ( ) { if ( ! this . isRange ) { throw new Error ( '`minRange` is only applicable for range sliders.' ) ; } return this . minRange ; } setHasTickMarks ( value ) { this . hasTickMarks = value ; this . updateTickMarksUI ( ) ; } getDisabled ( ) { return this . isDisabled ; } /**
     * Sets disabled state, including updating styles and thumb tabindex.
     */ setDisabled ( disabled ) { this . isDisabled = disabled ; if ( disabled ) { this . adapter . addClass ( cssClasses . DISABLED ) ; if ( this . isRange ) { this . adapter . setInputAttribute ( attributes . INPUT_DISABLED , '' , Thumb . START ) ; } this . adapter . setInputAttribute ( attributes . INPUT_DISABLED , '' , Thumb . END ) ; } else { this . adapter . removeClass ( cssClasses . DISABLED ) ; if ( this . isRange ) { this . adapter . removeInputAttribute ( attributes . INPUT_DISABLED , Thumb . START ) ; } this . adapter . removeInputAttribute ( attributes . INPUT_DISABLED , Thumb . END ) ; } } return getIsRange ( ) { return this . isRange ; } /**
     * - Syncs slider boundingClientRect with the current DOM.
     * - Updates UI based on internal state.
     */ layout ( { skipUpdateUI } = { } ) { this . rect = this . adapter . getBoundingClientRect ( ) ; if ( this . isRange ) { this . startThumbKnobWidth = this . adapter . getThumbKnobWidth ( Thumb . START ) ; this . endThumbKnobWidth = this . adapter . getThumbKnobWidth ( Thumb . END ) ; } if ( ! skipUpdateUI ) { this . updateUI ( ) ; } } /** Handles resize events on the window. */ handleResize ( ) { this . layout ( ) ; } /**
     * Handles pointer down events on the slider root element.
     */ handleDown ( event ) { if ( this . isDisabled ) return ; this . valueStartBeforeDownEvent = this . valueStart ; this . valueBeforeDownEvent = this . value ; const clientX = ( event as MouseEvent ) . clientX != null ? ( event as MouseEvent ) . clientX : ( event as TouchEvent ) . targetTouches [ 0 ] . clientX ; this . downEventClientX = clientX ; const value = this . mapClientXOnSliderScale ( clientX ) ; this . thumb = this . getThumbFromDownEvent ( clientX , value ) ; if ( this . thumb === null ) return ; this . handleDragStart ( event , value , this . thumb ) ; this . updateValue ( value , this . thumb , { emitInputEvent : true } ) ; } /**
     * Handles pointer move events on the slider root element.
     */ handleMove ( event ) { if ( this . isDisabled ) return ; event . preventDefault ( ) ; const clientX = ( event as MouseEvent ) . clientX != null ? ( event as MouseEvent ) . clientX : ( event as TouchEvent ) . targetTouches [ 0 ] . clientX ; const dragAlreadyStarted = this . thumb != null ; this . thumb = this . getThumbFromMoveEvent ( clientX ) ; if ( this . thumb === null ) return ; const value = this . mapClientXOnSliderScale ( clientX ) ; if ( ! dragAlreadyStarted ) { this . handleDragStart ( event , value , this . thumb ) ; this . adapter . emitDragStartEvent ( value , this . thumb ) ; } this . updateValue ( value , this . thumb , { emitInputEvent : true } ) ; } /**
     * Handles pointer up events on the slider root element.
     */ handleUp ( ) { if ( this . isDisabled || this . thumb === null ) return ; const oldValue = this . thumb === Thumb . START ? this . valueStartBeforeDownEvent : this . valueBeforeDownEvent ; const newValue = this . thumb === Thumb . START ? this . valueStart : this . value ; if ( oldValue !== newValue ) { this . adapter . emitChangeEvent ( newValue , this . thumb ) ; } this . adapter . emitDragEndEvent ( newValue , this . thumb ) ; this . thumb = null ; } /**
     * For range, discrete slider, shows the value indicator on both thumbs.
     */ handleThumbMouseenter ( ) { if ( ! this . isDiscrete || ! this . isRange ) return ; this . adapter . addThumbClass ( cssClasses . THUMB_WITH_INDICATOR , Thumb . START ) ; this . adapter . addThumbClass ( cssClasses . THUMB_WITH_INDICATOR , Thumb . END ) ; } /**
     * For range, discrete slider, hides the value indicator on both thumbs.
     */ handleThumbMouseleave ( ) { if ( ! this . isDiscrete || ! this . isRange ) return ; if ( this . adapter . isInputFocused ( Thumb . START ) || this . adapter . isInputFocused ( Thumb . END ) ) { return ; } this . adapter . removeThumbClass ( cssClasses . THUMB_WITH_INDICATOR , Thumb . START ) ; this . adapter . removeThumbClass ( cssClasses . THUMB_WITH_INDICATOR , Thumb . END ) ; } handleMousedownOrTouchstart ( event ) { const moveEventType = event . type === 'mousedown' ? 'mousemove' : 'touchmove' ; this . adapter . registerBodyEventHandler ( moveEventType , this . moveListener ) ; const upHandler = ( ) => { this . handleUp ( ) ; this . adapter . deregisterBodyEventHandler ( moveEventType , this . moveListener ) ; this . adapter . deregisterEventHandler ( 'mouseup' , upHandler ) ; this . adapter . deregisterEventHandler ( 'touchend' , upHandler ) ; } ; this . adapter . registerBodyEventHandler ( 'mouseup' , upHandler ) ; this . adapter . registerBodyEventHandler ( 'touchend' , upHandler ) ; this . handleDown ( event ) ; } handlePointerdown ( event ) { if ( event . pointerId != null ) { this . adapter . setPointerCapture ( event . pointerId ) ; } this . adapter . registerEventHandler ( 'pointermove' , this . moveListener ) ; this . handleDown ( event ) ; } /**
     * Handles input `change` event by setting internal slider value to match
     * input's new value.
     */ handleInputChange ( thumb ) { const value = Number ( this . adapter . getInputValue ( thumb ) ) ; if ( thumb === Thumb . START ) { this . setValueStart ( value ) ; } else { this . setValue ( value ) ; } this . adapter . emitChangeEvent ( thumb === Thumb . START ? this . valueStart : this . value , thumb ) ; this . adapter . emitInputEvent ( thumb === Thumb . START ? this . valueStart : this . value , thumb ) ; } /** Shows activated state and value indicator on thumb(s). */ handleInputFocus ( thumb ) { this . adapter . addThumbClass ( cssClasses . THUMB_FOCUSED , thumb ) ; if ( ! this . isDiscrete ) return ; this . adapter . addThumbClass ( cssClasses . THUMB_WITH_INDICATOR , thumb ) ; if ( this . isRange ) { const otherThumb = thumb === Thumb . START ? Thumb . END : Thumb . START ; this . adapter . addThumbClass ( cssClasses . THUMB_WITH_INDICATOR , otherThumb ) ; } } /** Removes activated state and value indicator from thumb(s). */ handleInputBlur ( thumb ) { this . adapter . removeThumbClass ( cssClasses . THUMB_FOCUSED , thumb ) ; if ( ! this . isDiscrete ) return ; this . adapter . removeThumbClass ( cssClasses . THUMB_WITH_INDICATOR , thumb ) ; if ( this . isRange ) { const otherThumb = thumb === Thumb . START ? Thumb . END : Thumb . START ; this . adapter . removeThumbClass ( cssClasses . THUMB_WITH_INDICATOR , otherThumb ) ; } } /**
     * Emits custom dragStart event, along with focusing the underlying input.
     */ private handleDragStart ( event , value , thumb ) { this . adapter . emitDragStartEvent ( value , thumb ) ; this . adapter . focusInput ( thumb ) ; event . preventDefault ( ) ; } return private getThumbFromDownEvent ( clientX , value ) { if ( ! this . isRange ) return Thumb . END ; const thumbStartRect = this . adapter . getThumbBoundingClientRect ( Thumb . START ) ; const thumbEndRect = this . adapter . getThumbBoundingClientRect ( Thumb . END ) ; const inThumbStartBounds = clientX >= thumbStartRect . left && clientX <= thumbStartRect . right ; const inThumbEndBounds = clientX >= thumbEndRect . left && clientX <= thumbEndRect . right ; if ( inThumbStartBounds && inThumbEndBounds ) { return null ; } if ( inThumbStartBounds ) { return Thumb . START ; } if ( inThumbEndBounds ) { return Thumb . END ; } if ( value < this . valueStart ) { return Thumb . START ; } if ( value > this . value ) { return Thumb . END ; } return ( value - this . valueStart <= this . value - value ) ? Thumb . START : Thumb . END ; } return private getThumbFromMoveEvent ( clientX ) { if ( this . thumb !== null ) return this . thumb ; if ( this . downEventClientX === null ) { throw new Error ( '`downEventClientX` is null after move event.' ) ; } const moveDistanceUnderThreshold = Math . abs ( this . downEventClientX - clientX ) < numbers . THUMB_UPDATE_MIN_PX ; if ( moveDistanceUnderThreshold ) return this . thumb ; const draggedThumbToLeft = clientX < this . downEventClientX ; if ( draggedThumbToLeft ) { return this . adapter . isRTL ( ) ? Thumb . END : Thumb . START ; } else { return this . adapter . isRTL ( ) ? Thumb . START : Thumb . END ; } } param thumb private updateUI ( thumb ? ) { this . updateThumbAndInputAttributes ( thumb ) ; this . updateThumbAndTrackUI ( thumb ) ; this . updateValueIndicatorUI ( thumb ) ; this . updateTickMarksUI ( ) ; } param thumb private updateThumbAndInputAttributes ( thumb ? ) { if ( ! thumb ) return ; const value = this . isRange && thumb === Thumb . START ? this . valueStart : this . value ; const valueStr = String ( value ) ; this . adapter . setInputAttribute ( attributes . INPUT_VALUE , valueStr , thumb ) ; if ( this . isRange && thumb === Thumb . START ) { this . adapter . setInputAttribute ( attributes . INPUT_MIN , String ( value + this . minRange ) , Thumb . END ) ; } else if ( this . isRange && thumb === Thumb . END ) { this . adapter . setInputAttribute ( attributes . INPUT_MAX , String ( value - this . minRange ) , Thumb . START ) ; } if ( this . adapter . getInputValue ( thumb ) !== valueStr ) { this . adapter . setInputValue ( valueStr , thumb ) ; } const valueToAriaValueTextFn = this . adapter . getValueToAriaValueTextFn ( ) ; if ( valueToAriaValueTextFn ) { this . adapter . setInputAttribute ( attributes . ARIA_VALUETEXT , valueToAriaValueTextFn ( value , thumb ) , thumb ) ; } } param thumb private updateValueIndicatorUI ( thumb ? ) { if ( ! this . isDiscrete ) return ; const value = this . isRange && thumb === Thumb . START ? this . valueStart : this . value ; this . adapter . setValueIndicatorText ( value , thumb === Thumb . START ? Thumb . START : Thumb . END ) ; if ( ! thumb && this . isRange ) { this . adapter . setValueIndicatorText ( this . valueStart , Thumb . START ) ; } } /**
     * Updates tick marks UI within slider, based on current min, max, and step.
     */ private updateTickMarksUI ( ) { if ( ! this . isDiscrete || ! this . hasTickMarks ) return ; const numTickMarksInactiveStart = ( this . valueStart - this . min ) / this . step ; const numTickMarksActive = ( this . value - this . valueStart ) / this . step + 1 ; const numTickMarksInactiveEnd = ( this . max - this . value ) / this . step ; const tickMarksInactiveStart = Array . from < TickMark > ( { length : numTickMarksInactiveStart } ) . fill ( TickMark . INACTIVE ) ; const tickMarksActive = Array . from < TickMark > ( { length : numTickMarksActive } ) . fill ( TickMark . ACTIVE ) ; const tickMarksInactiveEnd = Array . from < TickMark > ( { length : numTickMarksInactiveEnd } ) . fill ( TickMark . INACTIVE ) ; this . adapter . updateTickMarks ( tickMarksInactiveStart . concat ( tickMarksActive ) . concat ( tickMarksInactiveEnd ) ) ; } /** Maps clientX to a value on the slider scale. */ private mapClientXOnSliderScale ( clientX ) { const xPos = clientX - this . rect . left ; let pctComplete = xPos / this . rect . width ; if ( this . adapter . isRTL ( ) ) { pctComplete = 1 - pctComplete ; } const value = this . min + pctComplete * ( this . max - this . min ) ; if ( value === this . max || value === this . min ) { return value ; } return Number ( this . quantize ( value ) . toFixed ( this . numDecimalPlaces ) ) ; } /** Calculates the quantized value based on step value. */ private quantize ( value ) { const numSteps = Math . round ( ( value - this . min ) / this . step ) ; return this . min + numSteps * this . step ; } /**
     * Updates slider value (internal state and UI) based on the given value.
     */ private updateValue ( value , thumb , { emitInputEvent , } = { } ) { value = this . clampValue ( value , thumb ) ; if ( this . isRange && thumb === Thumb . START ) { if ( this . valueStart === value ) return ; this . valueStart = value ; } else { if ( this . value === value ) return ; this . value = value ; } this . updateUI ( thumb ) ; if ( emitInputEvent ) { this . adapter . emitInputEvent ( thumb === Thumb . START ? this . valueStart : this . value , thumb ) ; } } /**
     * Clamps the given value for the given thumb based on slider properties:
     * - Restricts value within [min, max].
     * - If range slider, clamp start value <= end value - min range, and
     *   end value >= start value + min range.
     */ private clampValue ( value , thumb ) { value = Math . min ( Math . max ( value , this . min ) , this . max ) ; const thumbStartMovedPastThumbEnd = this . isRange && thumb === Thumb . START && value > this . value - this . minRange ; if ( thumbStartMovedPastThumbEnd ) { return this . value - this . minRange ; } const thumbEndMovedPastThumbStart = this . isRange && thumb === Thumb . END && value < this . valueStart + this . minRange ; if ( thumbEndMovedPastThumbStart ) { return this . valueStart + this . minRange ; } return value ; } /**
     * Updates the active track and thumb style properties to reflect current
     * value.
     */ private updateThumbAndTrackUI ( thumb ? ) { const { max , min } = this ; const pctComplete = ( this . value - this . valueStart ) / ( max - min ) ; const rangePx = pctComplete * this . rect . width ; const isRtl = this . adapter . isRTL ( ) ; const transformProp = HAS_WINDOW ? getCorrectPropertyName ( window , 'transform' ) : 'transform' ; if ( this . isRange ) { const thumbLeftPos = this . adapter . isRTL ( ) ? ( max - this . value ) / ( max - min ) * this . rect . width : ( this . valueStart - min ) / ( max - min ) * this . rect . width ; const thumbRightPos = thumbLeftPos + rangePx ; this . animFrame . request ( AnimationKeys . SLIDER_UPDATE , ( ) => { const trackAnimatesFromRight = ( ! isRtl && thumb === Thumb . START ) || ( isRtl && thumb !== Thumb . START ) ; if ( trackAnimatesFromRight ) { this . adapter . setTrackActiveStyleProperty ( 'transform-origin' , 'right' ) ; this . adapter . setTrackActiveStyleProperty ( 'left' , 'auto' ) ; this . adapter . setTrackActiveStyleProperty ( 'right' , `${ this . rect . width - thumbRightPos }px` ) ; } else { this . adapter . setTrackActiveStyleProperty ( 'transform-origin' , 'left' ) ; this . adapter . setTrackActiveStyleProperty ( 'right' , 'auto' ) ; this . adapter . setTrackActiveStyleProperty ( 'left' , `${ thumbLeftPos }px` ) ; } this . adapter . setTrackActiveStyleProperty ( transformProp , `scaleX(${ pctComplete })` ) ; const thumbStartPos = isRtl ? thumbRightPos : thumbLeftPos ; const thumbEndPos = this . adapter . isRTL ( ) ? thumbLeftPos : thumbRightPos ; if ( thumb === Thumb . START || ! thumb || ! this . initialStylesRemoved ) { this . adapter . setThumbStyleProperty ( transformProp , `translateX(${ thumbStartPos }px)` , Thumb . START ) ; this . alignValueIndicator ( Thumb . START , thumbStartPos ) ; } if ( thumb === Thumb . END || ! thumb || ! this . initialStylesRemoved ) { this . adapter . setThumbStyleProperty ( transformProp , `translateX(${ thumbEndPos }px)` , Thumb . END ) ; this . alignValueIndicator ( Thumb . END , thumbEndPos ) ; } this . removeInitialStyles ( isRtl ) ; this . updateOverlappingThumbsUI ( thumbStartPos , thumbEndPos , thumb ) ; } ) ; } else { this . animFrame . request ( AnimationKeys . SLIDER_UPDATE , ( ) => { const thumbStartPos = isRtl ? this . rect . width - rangePx : rangePx ; this . adapter . setThumbStyleProperty ( transformProp , `translateX(${ thumbStartPos }px)` , Thumb . END ) ; this . alignValueIndicator ( Thumb . END , thumbStartPos ) ; this . adapter . setTrackActiveStyleProperty ( transformProp , `scaleX(${ pctComplete })` ) ; this . removeInitialStyles ( isRtl ) ; } ) ; } } /**
     * Shifts the value indicator to either side if it would otherwise stick
     * beyond the slider's length while keeping the caret above the knob.
     */ private alignValueIndicator ( thumb , thumbPos ) { if ( ! this . isDiscrete ) return ; const thumbHalfWidth = this . adapter . getThumbBoundingClientRect ( thumb ) . width / 2 ; const containerWidth = this . adapter . getValueIndicatorContainerWidth ( thumb ) ; const sliderWidth = this . adapter . getBoundingClientRect ( ) . width ; if ( containerWidth / 2 > thumbPos + thumbHalfWidth ) { this . adapter . setThumbStyleProperty ( strings . VAR_VALUE_INDICATOR_CARET_LEFT , `${ thumbHalfWidth }px` , thumb ) ; this . adapter . setThumbStyleProperty ( strings . VAR_VALUE_INDICATOR_CARET_RIGHT , 'auto' , thumb ) ; this . adapter . setThumbStyleProperty ( strings . VAR_VALUE_INDICATOR_CARET_TRANSFORM , 'translateX(-50%)' , thumb ) ; this . adapter . setThumbStyleProperty ( strings . VAR_VALUE_INDICATOR_CONTAINER_LEFT , '0' , thumb ) ; this . adapter . setThumbStyleProperty ( strings . VAR_VALUE_INDICATOR_CONTAINER_RIGHT , 'auto' , thumb ) ; this . adapter . setThumbStyleProperty ( strings . VAR_VALUE_INDICATOR_CONTAINER_TRANSFORM , 'none' , thumb ) ; } else if ( containerWidth / 2 > sliderWidth - thumbPos + thumbHalfWidth ) { this . adapter . setThumbStyleProperty ( strings . VAR_VALUE_INDICATOR_CARET_LEFT , 'auto' , thumb ) ; this . adapter . setThumbStyleProperty ( strings . VAR_VALUE_INDICATOR_CARET_RIGHT , `${ thumbHalfWidth }px` , thumb ) ; this . adapter . setThumbStyleProperty ( strings . VAR_VALUE_INDICATOR_CARET_TRANSFORM , 'translateX(50%)' , thumb ) ; this . adapter . setThumbStyleProperty ( strings . VAR_VALUE_INDICATOR_CONTAINER_LEFT , 'auto' , thumb ) ; this . adapter . setThumbStyleProperty ( strings . VAR_VALUE_INDICATOR_CONTAINER_RIGHT , '0' , thumb ) ; this . adapter . setThumbStyleProperty ( strings . VAR_VALUE_INDICATOR_CONTAINER_TRANSFORM , 'none' , thumb ) ; } else { this . adapter . setThumbStyleProperty ( strings . VAR_VALUE_INDICATOR_CARET_LEFT , '50%' , thumb ) ; this . adapter . setThumbStyleProperty ( strings . VAR_VALUE_INDICATOR_CARET_RIGHT , 'auto' , thumb ) ; this . adapter . setThumbStyleProperty ( strings . VAR_VALUE_INDICATOR_CARET_TRANSFORM , 'translateX(-50%)' , thumb ) ; this . adapter . setThumbStyleProperty ( strings . VAR_VALUE_INDICATOR_CONTAINER_LEFT , '50%' , thumb ) ; this . adapter . setThumbStyleProperty ( strings . VAR_VALUE_INDICATOR_CONTAINER_RIGHT , 'auto' , thumb ) ; this . adapter . setThumbStyleProperty ( strings . VAR_VALUE_INDICATOR_CONTAINER_TRANSFORM , 'translateX(-50%)' , thumb ) ; } } /**
     * Removes initial inline styles if not already removed. `left:<...>%`
     * inline styles can be added to position the thumb correctly before JS
     * initialization. However, they need to be removed before the JS starts
     * positioning the thumb. This is because the JS uses
     * `transform:translateX(<...>)px` (for performance reasons) to position
     * the thumb (which is not possible for initial styles since we need the
     * bounding rect measurements).
     */ private removeInitialStyles ( isRtl ) { if ( this . initialStylesRemoved ) return ; const position = isRtl ? 'right' : 'left' ; this . adapter . removeThumbStyleProperty ( position , Thumb . END ) ; if ( this . isRange ) { this . adapter . removeThumbStyleProperty ( position , Thumb . START ) ; } this . initialStylesRemoved = true ; this . resetTrackAndThumbAnimation ( ) ; } /**
     * Resets track/thumb animation to prevent animation when adding
     * `transform` styles to thumb initially.
     */ private resetTrackAndThumbAnimation ( ) { if ( ! this . isDiscrete ) return ; const transitionProp = HAS_WINDOW ? getCorrectPropertyName ( window , 'transition' ) : 'transition' ; const transitionDefault = 'none 0s ease 0s' ; this . adapter . setThumbStyleProperty ( transitionProp , transitionDefault , Thumb . END ) ; if ( this . isRange ) { this . adapter . setThumbStyleProperty ( transitionProp , transitionDefault , Thumb . START ) ; } this . adapter . setTrackActiveStyleProperty ( transitionProp , transitionDefault ) ; requestAnimationFrame ( ( ) => { this . adapter . removeThumbStyleProperty ( transitionProp , Thumb . END ) ; this . adapter . removeTrackActiveStyleProperty ( transitionProp ) ; if ( this . isRange ) { this . adapter . removeThumbStyleProperty ( transitionProp , Thumb . START ) ; } } ) ; } param thumb private updateOverlappingThumbsUI ( thumbStartPos , thumbEndPos , thumb ? ) { let thumbsOverlap = false ; if ( this . adapter . isRTL ( ) ) { const startThumbLeftEdge = thumbStartPos - this . startThumbKnobWidth / 2 ; const endThumbRightEdge = thumbEndPos + this . endThumbKnobWidth / 2 ; thumbsOverlap = endThumbRightEdge >= startThumbLeftEdge ; } else { const startThumbRightEdge = thumbStartPos + this . startThumbKnobWidth / 2 ; const endThumbLeftEdge = thumbEndPos - this . endThumbKnobWidth / 2 ; thumbsOverlap = startThumbRightEdge >= endThumbLeftEdge ; } if ( thumbsOverlap ) { this . adapter . addThumbClass ( cssClasses . THUMB_TOP , thumb || Thumb . END ) ; this . adapter . removeThumbClass ( cssClasses . THUMB_TOP , thumb === Thumb . START ? Thumb . END : Thumb . START ) ; } else { this . adapter . removeThumbClass ( cssClasses . THUMB_TOP , Thumb . START ) ; this . adapter . removeThumbClass ( cssClasses . THUMB_TOP , Thumb . END ) ; } } param attributeValue param attributeName private convertAttributeValueToNumber ( attributeValue , attributeName ) { if ( attributeValue === null ) { throw new Error ( `MDCSliderFoundation: \`${ attributeName }\` must be non-null.` ) ; } const value = Number ( attributeValue ) ; if ( isNaN ( value ) ) { throw new Error ( `MDCSliderFoundation: \`${ attributeName }\` value is ` + `\`${ attributeValue }\`, but must be a number.` ) ; } return value ; } /** Checks that the given properties are valid slider values. */ private validateProperties ( { min , max , value , valueStart , step , minRange } ) { if ( min >= max ) { throw new Error ( `MDCSliderFoundation: min must be strictly less than max. ` + `Current: [min: ${ min }, max: ${ max }]` ) ; } if ( step <= 0 ) { throw new Error ( `MDCSliderFoundation: step must be a positive number. ` + `Current step: ${ this . step }` ) ; } if ( this . isRange ) { if ( value < min || value > max || valueStart < min || valueStart > max ) { throw new Error ( `MDCSliderFoundation: values must be in [min, max] range. ` + `Current values: [start value: ${ valueStart }, end value: ${ value }]` ) ; } if ( valueStart > value ) { throw new Error ( `MDCSliderFoundation: start value must be <= end value. ` + `Current values: [start value: ${ valueStart }, end value: ${ value }]` ) ; } if ( minRange < 0 ) { throw new Error ( `MDCSliderFoundation: minimum range must be non-negative. ` + `Current min range: ${ minRange }` ) ; } if ( value - valueStart < minRange ) { throw new Error ( `MDCSliderFoundation: start value and end value must differ by at least ` + `${ minRange }. Current values: [start value: ${ valueStart }, ` + `end value: ${ value }]` ) ; } const numStepsValueStartFromMin = ( valueStart - min ) / step ; const numStepsValueFromMin = ( value - min ) / step ; if ( ! Number . isInteger ( parseFloat ( numStepsValueStartFromMin . toFixed ( 6 ) ) ) || ! Number . isInteger ( parseFloat ( numStepsValueFromMin . toFixed ( 6 ) ) ) ) { throw new Error ( `MDCSliderFoundation: Slider values must be valid based on the ` + `step value. Current values: [start value: ${ valueStart }, ` + `end value: ${ value }]` ) ; } } else { if ( value < min || value > max ) { throw new Error ( `MDCSliderFoundation: value must be in [min, max] range. ` + `Current value: ${ value }` ) ; } const numStepsValueFromMin = ( value - min ) / step ; if ( ! Number . isInteger ( parseFloat ( numStepsValueFromMin . toFixed ( 6 ) ) ) ) { throw new Error ( `MDCSliderFoundation: Slider value must be valid based on the ` + `step value. Current value: ${ value }` ) ; } } } private registerEventHandlers ( ) { this . adapter . registerWindowEventHandler ( 'resize' , this . resizeListener ) ; if ( MDCSliderFoundation . SUPPORTS_POINTER_EVENTS ) { this . adapter . registerEventHandler ( 'pointerdown' , this . pointerdownListener ) ; this . adapter . registerEventHandler ( 'pointerup' , this . pointerupListener ) ; } else { this . adapter . registerEventHandler ( 'mousedown' , this . mousedownOrTouchstartListener ) ; this . adapter . registerEventHandler ( 'touchstart' , this . mousedownOrTouchstartListener ) ; } if ( this . isRange ) { this . adapter . registerThumbEventHandler ( Thumb . START , 'mouseenter' , this . thumbMouseenterListener ) ; this . adapter . registerThumbEventHandler ( Thumb . START , 'mouseleave' , this . thumbMouseleaveListener ) ; this . adapter . registerInputEventHandler ( Thumb . START , 'change' , this . inputStartChangeListener ) ; this . adapter . registerInputEventHandler ( Thumb . START , 'focus' , this . inputStartFocusListener ) ; this . adapter . registerInputEventHandler ( Thumb . START , 'blur' , this . inputStartBlurListener ) ; } this . adapter . registerThumbEventHandler ( Thumb . END , 'mouseenter' , this . thumbMouseenterListener ) ; this . adapter . registerThumbEventHandler ( Thumb . END , 'mouseleave' , this . thumbMouseleaveListener ) ; this . adapter . registerInputEventHandler ( Thumb . END , 'change' , this . inputEndChangeListener ) ; this . adapter . registerInputEventHandler ( Thumb . END , 'focus' , this . inputEndFocusListener ) ; this . adapter . registerInputEventHandler ( Thumb . END , 'blur' , this . inputEndBlurListener ) ; } private deregisterEventHandlers ( ) { this . adapter . deregisterWindowEventHandler ( 'resize' , this . resizeListener ) ; if ( MDCSliderFoundation . SUPPORTS_POINTER_EVENTS ) { this . adapter . deregisterEventHandler ( 'pointerdown' , this . pointerdownListener ) ; this . adapter . deregisterEventHandler ( 'pointerup' , this . pointerupListener ) ; } else { this . adapter . deregisterEventHandler ( 'mousedown' , this . mousedownOrTouchstartListener ) ; this . adapter . deregisterEventHandler ( 'touchstart' , this . mousedownOrTouchstartListener ) ; } if ( this . isRange ) { this . adapter . deregisterThumbEventHandler ( Thumb . START , 'mouseenter' , this . thumbMouseenterListener ) ; this . adapter . deregisterThumbEventHandler ( Thumb . START , 'mouseleave' , this . thumbMouseleaveListener ) ; this . adapter . deregisterInputEventHandler ( Thumb . START , 'change' , this . inputStartChangeListener ) ; this . adapter . deregisterInputEventHandler ( Thumb . START , 'focus' , this . inputStartFocusListener ) ; this . adapter . deregisterInputEventHandler ( Thumb . START , 'blur' , this . inputStartBlurListener ) ; } this . adapter . deregisterThumbEventHandler ( Thumb . END , 'mouseenter' , this . thumbMouseenterListener ) ; this . adapter . deregisterThumbEventHandler ( Thumb . END , 'mouseleave' , this . thumbMouseleaveListener ) ; this . adapter . deregisterInputEventHandler ( Thumb . END , 'change' , this . inputEndChangeListener ) ; this . adapter . deregisterInputEventHandler ( Thumb . END , 'focus' , this . inputEndFocusListener ) ; this . adapter . deregisterInputEventHandler ( Thumb . END , 'blur' , this . inputEndBlurListener ) ; } private handlePointerup ( ) { this . handleUp ( ) ; this . adapter . deregisterEventHandler ( 'pointermove' , this . moveListener ) ; } } function isIOS ( ) { return [ 'iPad Simulator' , 'iPhone Simulator' , 'iPod Simulator' , 'iPad' , 'iPhone' , 'iPod' ] . includes ( navigator . platform ) || ( navigator . userAgent . includes ( 'Mac' ) && 'ontouchend' in document ) ; } /**
 * Given a number, returns the number of digits that appear after the
 * decimal point.
 * See
 * https://stackoverflow.com/questions/9539513/is-there-a-reliable-way-in-javascript-to-obtain-the-number-of-decimal-places-of
 */ function getNumDecimalPlaces ( n ) { const match = /(?:\.(\d+))?(?:[eE]([+\-]?\d+))?$/ . exec ( String ( n ) ) ; if ( ! match ) return 0 ; const fraction = match [ 1 ] || '' ; const exponent = match [ 2 ] || 0 ; return Math . max ( 0 , ( fraction === '0' ? 0 : fraction . length ) - Number ( exponent ) ) ; }