license import { MDCComponent } from '@material/base/component' ; import { EventType , SpecificEventListener } from '@material/base/types' ; import { applyPassive } from '@material/dom/events' ; import { matches } from '@material/dom/ponyfill' ; import { MDCRippleAdapter } from '@material/ripple/adapter' ; import { MDCRipple } from '@material/ripple/component' ; import { MDCRippleFoundation } from '@material/ripple/foundation' ; import { MDCSliderAdapter } from './adapter' ; import { cssClasses , events } from './constants' ; import { MDCSliderFoundation } from './foundation' ; import { MDCSliderChangeEventDetail , Thumb , TickMark } from './types' ; /** Vanilla JS implementation of slider component. */ export class MDCSlider extends MDCComponent < MDCSliderFoundation > { static override attachTo ( root , options = { } ) { return new MDCSlider ( root , undefined , options ) ; } override root ! : HTMLElement ; private inputs ! : HTMLInputElement [ ] ; private thumbs ! : HTMLElement [ ] ; private trackActive ! : HTMLElement ; private ripples ! : MDCRipple [ ] ; private skipInitialUIUpdate = false ; private valueToAriaValueTextFn : ( ( value : number ) => string ) | null = null ; override getDefaultFoundation ( ) { const adapter = { hasClass : ( className ) => this . root . classList . contains ( className ) , addClass : ( className ) => { this . root . classList . add ( className ) ; } , removeClass : ( className ) => { this . root . classList . remove ( className ) ; } , addThumbClass : ( className , thumb ) => { this . getThumbEl ( thumb ) . classList . add ( className ) ; } , removeThumbClass : ( className , thumb ) => { this . getThumbEl ( thumb ) . classList . remove ( className ) ; } , getAttribute : ( attribute ) => this . root . getAttribute ( attribute ) , getInputValue : ( thumb ) => this . getInput ( thumb ) . value , setInputValue : ( value , thumb ) => { this . getInput ( thumb ) . value = value ; } , getInputAttribute : ( attribute , thumb ) => this . getInput ( thumb ) . getAttribute ( attribute ) , setInputAttribute : ( attribute , value , thumb ) => { this . getInput ( thumb ) . setAttribute ( attribute , value ) ; } , removeInputAttribute : ( attribute , thumb ) => { this . getInput ( thumb ) . removeAttribute ( attribute ) ; } , focusInput : ( thumb ) => { this . getInput ( thumb ) . focus ( ) ; } , isInputFocused : ( thumb ) => this . getInput ( thumb ) === document . activeElement , getThumbKnobWidth : ( thumb ) => { return this . getThumbEl ( thumb ) . querySelector < HTMLElement > ( `.${ cssClasses . THUMB_KNOB }` ) ! . getBoundingClientRect ( ) . width ; } , getThumbBoundingClientRect : ( thumb ) => this . getThumbEl ( thumb ) . getBoundingClientRect ( ) , getBoundingClientRect : ( ) => this . root . getBoundingClientRect ( ) , getValueIndicatorContainerWidth : ( thumb ) => { return this . getThumbEl ( thumb ) . querySelector < HTMLElement > ( `.${ cssClasses . VALUE_INDICATOR_CONTAINER }` ) ! . getBoundingClientRect ( ) . width ; } , isRTL : ( ) => getComputedStyle ( this . root ) . direction === 'rtl' , setThumbStyleProperty : ( propertyName , value , thumb ) => { this . getThumbEl ( thumb ) . style . setProperty ( propertyName , value ) ; } , removeThumbStyleProperty : ( propertyName , thumb ) => { this . getThumbEl ( thumb ) . style . removeProperty ( propertyName ) ; } , setTrackActiveStyleProperty : ( propertyName , value ) => { this . trackActive . style . setProperty ( propertyName , value ) ; } , removeTrackActiveStyleProperty : ( propertyName ) => { this . trackActive . style . removeProperty ( propertyName ) ; } , setValueIndicatorText : ( value , thumb ) => { const valueIndicatorEl = this . getThumbEl ( thumb ) . querySelector < HTMLElement > ( `.${ cssClasses . VALUE_INDICATOR_TEXT }` ) ; valueIndicatorEl ! . textContent = String ( value ) ; } , getValueToAriaValueTextFn : ( ) => this . valueToAriaValueTextFn , updateTickMarks : ( tickMarks ) => { let tickMarksContainer = this . root . querySelector < HTMLElement > ( `.${ cssClasses . TICK_MARKS_CONTAINER }` ) ; if ( ! tickMarksContainer ) { tickMarksContainer = document . createElement ( 'div' ) ; tickMarksContainer . classList . add ( cssClasses . TICK_MARKS_CONTAINER ) ; const track = this . root . querySelector < HTMLElement > ( `.${ cssClasses . TRACK }` ) ; track ! . appendChild ( tickMarksContainer ) ; } if ( tickMarks . length !== tickMarksContainer . children . length ) { while ( tickMarksContainer . firstChild ) { tickMarksContainer . removeChild ( tickMarksContainer . firstChild ) ; } this . addTickMarks ( tickMarksContainer , tickMarks ) ; } else { this . updateTickMarks ( tickMarksContainer , tickMarks ) ; } } , setPointerCapture : ( pointerId ) => { this . root . setPointerCapture ( pointerId ) ; } , emitChangeEvent : ( value , thumb ) => { this . emit < MDCSliderChangeEventDetail > ( events . CHANGE , { value , thumb } ) ; } , emitInputEvent : ( value , thumb ) => { this . emit < MDCSliderChangeEventDetail > ( events . INPUT , { value , thumb } ) ; } , emitDragStartEvent : ( _ , thumb ) => { this . getRipple ( thumb ) . activate ( ) ; } , emitDragEndEvent : ( _ , thumb ) => { this . getRipple ( thumb ) . deactivate ( ) ; } , registerEventHandler : ( evtType , handler ) => { this . listen ( evtType , handler ) ; } , deregisterEventHandler : ( evtType , handler ) => { this . unlisten ( evtType , handler ) ; } , registerThumbEventHandler : ( thumb , evtType , handler ) => { this . getThumbEl ( thumb ) . addEventListener ( evtType , handler ) ; } , deregisterThumbEventHandler : ( thumb , evtType , handler ) => { this . getThumbEl ( thumb ) . removeEventListener ( evtType , handler ) ; } , registerInputEventHandler : ( thumb , evtType , handler ) => { this . getInput ( thumb ) . addEventListener ( evtType , handler ) ; } , deregisterInputEventHandler : ( thumb , evtType , handler ) => { this . getInput ( thumb ) . removeEventListener ( evtType , handler ) ; } , registerBodyEventHandler : ( evtType , handler ) => { document . body . addEventListener ( evtType , handler ) ; } , deregisterBodyEventHandler : ( evtType , handler ) => { document . body . removeEventListener ( evtType , handler ) ; } , registerWindowEventHandler : ( evtType , handler ) => { window . addEventListener ( evtType , handler ) ; } , deregisterWindowEventHandler : ( evtType , handler ) => { window . removeEventListener ( evtType , handler ) ; } , } ; return new MDCSliderFoundation ( adapter ) ; } /**
     * Initializes component, with the following options:
     * - `skipInitialUIUpdate`: Whether to skip updating the UI when initially
     *   syncing with the DOM. This should be enabled when the slider position
     *   is set before component initialization.
     */ override initialize ( { skipInitialUIUpdate } = { } ) { this . inputs = [ ] . slice . call ( this . root . querySelectorAll ( `.${ cssClasses . INPUT }` ) ) as HTMLInputElement [ ] ; this . thumbs = [ ] . slice . call ( this . root . querySelectorAll ( `.${ cssClasses . THUMB }` ) ) as HTMLElement [ ] ; this . trackActive = this . root . querySelector ( `.${ cssClasses . TRACK_ACTIVE }` ) as HTMLElement ; this . ripples = this . createRipples ( ) ; if ( skipInitialUIUpdate ) { this . skipInitialUIUpdate = true ; } } override initialSyncWithDOM ( ) { this . foundation . layout ( { skipUpdateUI : this . skipInitialUIUpdate } ) ; } /** Redraws UI based on DOM (e.g. element dimensions, RTL). */ layout ( ) { this . foundation . layout ( ) ; } getValueStart ( ) { return this . foundation . getValueStart ( ) ; } setValueStart ( valueStart ) { this . foundation . setValueStart ( valueStart ) ; } getValue ( ) { return this . foundation . getValue ( ) ; } setValue ( value ) { this . foundation . setValue ( value ) ; } return getDisabled ( ) { return this . foundation . getDisabled ( ) ; } /** Sets slider disabled state. */ setDisabled ( disabled ) { this . foundation . setDisabled ( disabled ) ; } /**
     * Sets a function that maps the slider value to the value of the
     * `aria-valuetext` attribute on the thumb element.
     */ setValueToAriaValueTextFn ( mapFn ) { this . valueToAriaValueTextFn = mapFn ; } private getThumbEl ( thumb ) { return thumb === Thumb . END ? this . thumbs [ this . thumbs . length - 1 ] : this . thumbs [ 0 ] ; } private getInput ( thumb ) { return thumb === Thumb . END ? this . inputs [ this . inputs . length - 1 ] : this . inputs [ 0 ] ; } private getRipple ( thumb ) { return thumb === Thumb . END ? this . ripples [ this . ripples . length - 1 ] : this . ripples [ 0 ] ; } /** Adds tick mark elements to the given container. */ private addTickMarks ( tickMarkContainer , tickMarks ) { const fragment = document . createDocumentFragment ( ) ; for ( let i = 0 ; i < tickMarks . length ; i ++ ) { const div = document . createElement ( 'div' ) ; const tickMarkClass = tickMarks [ i ] === TickMark . ACTIVE ? cssClasses . TICK_MARK_ACTIVE : cssClasses . TICK_MARK_INACTIVE ; div . classList . add ( tickMarkClass ) ; fragment . appendChild ( div ) ; } tickMarkContainer . appendChild ( fragment ) ; } /** Updates tick mark elements' classes in the given container. */ private updateTickMarks ( tickMarkContainer , tickMarks ) { const tickMarkEls = Array . from ( tickMarkContainer . children ) ; for ( let i = 0 ; i < tickMarkEls . length ; i ++ ) { if ( tickMarks [ i ] === TickMark . ACTIVE ) { tickMarkEls [ i ] . classList . add ( cssClasses . TICK_MARK_ACTIVE ) ; tickMarkEls [ i ] . classList . remove ( cssClasses . TICK_MARK_INACTIVE ) ; } else { tickMarkEls [ i ] . classList . add ( cssClasses . TICK_MARK_INACTIVE ) ; tickMarkEls [ i ] . classList . remove ( cssClasses . TICK_MARK_ACTIVE ) ; } } } /** Initializes thumb ripples. */ private createRipples ( ) { const ripples = [ ] ; const rippleSurfaces = [ ] . slice . call ( this . root . querySelectorAll < HTMLElement > ( `.${ cssClasses . THUMB }` ) ) ; for ( let i = 0 ; i < rippleSurfaces . length ; i ++ ) { const rippleSurface = rippleSurfaces [ i ] as HTMLElement ; const input = this . inputs [ i ] ; const adapter = { ... MDCRipple . createAdapter ( this ) , addClass : ( className ) => { rippleSurface . classList . add ( className ) ; } , computeBoundingRect : ( ) => rippleSurface . getBoundingClientRect ( ) , deregisterInteractionHandler : < K extends EventType > ( evtType , handler ) => { input . removeEventListener ( evtType , handler ) ; } , isSurfaceActive : ( ) => matches ( input , ':active' ) , isUnbounded : ( ) => true , registerInteractionHandler : < K extends EventType > ( evtType , handler ) => { input . addEventListener ( evtType , handler , applyPassive ( ) ) ; } , removeClass : ( className ) => { rippleSurface . classList . remove ( className ) ; } , updateCssVariable : ( varName , value ) => { rippleSurface . style . setProperty ( varName , value ) ; } , } ; const ripple = new MDCRipple ( rippleSurface , new MDCRippleFoundation ( adapter ) ) ; ripple . unbounded = true ; ripples . push ( ripple ) ; } return ripples ; } }