license import { FocusMonitor , FocusOrigin } from '@angular/cdk/a11y' ; import { Directionality } from '@angular/cdk/bidi' ; import { coerceBooleanProperty , coerceNumberProperty } from '@angular/cdk/coercion' ; import { DOWN_ARROW , END , HOME , LEFT_ARROW , PAGE_DOWN , PAGE_UP , RIGHT_ARROW , UP_ARROW , } from '@angular/cdk/keycodes' ; import { Attribute , ChangeDetectionStrategy , ChangeDetectorRef , Component , ElementRef , EventEmitter , forwardRef , Inject , Input , OnDestroy , OnInit , Optional , Output , ViewChild , ViewEncapsulation , } from '@angular/core' ; import { ControlValueAccessor , NG_VALUE_ACCESSOR } from '@angular/forms' ; import { CanColor , CanColorCtor , CanDisable , CanDisableCtor , HammerInput , HasTabIndex , HasTabIndexCtor , mixinColor , mixinDisabled , mixinTabIndex , } from '@angular/material/core' ; import { ANIMATION_MODULE_TYPE } from '@angular/platform-browser/animations' ; import { Subscription } from 'rxjs' ; /**
 * Visually, a 30px separation between tick marks looks best. This is very subjective but it is
 * the default separation we chose.
 */ const MIN_AUTO_TICK_SEPARATION = 30 ; /** The thumb gap size for a disabled slider. */ const DISABLED_THUMB_GAP = 7 ; /** The thumb gap size for a non-active slider at its minimum value. */ const MIN_VALUE_NONACTIVE_THUMB_GAP = 7 ; /** The thumb gap size for an active slider at its minimum value. */ const MIN_VALUE_ACTIVE_THUMB_GAP = 10 ; docs-private export const MAT_SLIDER_VALUE_ACCESSOR = { provide : NG_VALUE_ACCESSOR , useExisting : forwardRef ( ( ) => MatSlider ) , multi : true } ; /** A simple change event emitted by the MatSlider component. */ export class MatSliderChange { /** The MatSlider that changed. */ source : MatSlider ; /** The new value of the source slider. */ value : number | null ; } docs-private export class MatSliderBase { constructor ( public _elementRef ) { } } export const _MatSliderMixinBase = mixinTabIndex ( mixinColor ( mixinDisabled ( MatSliderBase ) , 'accent' ) ) ; /**
 * 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 ( { moduleId : module . id , selector : 'mat-slider' , exportAs : 'matSlider' , providers : [ MAT_SLIDER_VALUE_ACCESSOR ] , host : { '(focus)' : '_onFocus()' , '(blur)' : '_onBlur()' , '(mousedown)' : '_onMousedown($event)' , '(keydown)' : '_onKeydown($event)' , '(keyup)' : '_onKeyup()' , '(mouseenter)' : '_onMouseenter()' , '(slide)' : '_onSlide($event)' , '(slideend)' : '_onSlideEnd()' , '(slidestart)' : '_onSlideStart($event)' , 'class' : 'mat-slider' , 'role' : 'slider' , '[tabIndex]' : 'tabIndex' , '[attr.aria-disabled]' : 'disabled' , '[attr.aria-valuemax]' : 'max' , '[attr.aria-valuemin]' : 'min' , '[attr.aria-valuenow]' : 'value' , '[attr.aria-orientation]' : 'vertical ? "vertical" : "horizontal"' , '[class.mat-slider-disabled]' : 'disabled' , '[class.mat-slider-has-ticks]' : 'tickInterval' , '[class.mat-slider-horizontal]' : '!vertical' , '[class.mat-slider-axis-inverted]' : '_invertAxis' , '[class.mat-slider-sliding]' : '_isSliding' , '[class.mat-slider-thumb-label-showing]' : 'thumbLabel' , '[class.mat-slider-vertical]' : 'vertical' , '[class.mat-slider-min-value]' : '_isMinValue' , '[class.mat-slider-hide-last-tick]' : 'disabled || _isMinValue && _thumbGap && _invertAxis' , '[class._mat-animation-noopable]' : '_animationMode === "NoopAnimations"' , } , templateUrl : 'slider.html' , styleUrls : [ 'slider.css' ] , inputs : [ 'disabled' , 'color' , 'tabIndex' ] , encapsulation : ViewEncapsulation . None , changeDetection : ChangeDetectionStrategy . OnPush , } ) export class MatSlider extends _MatSliderMixinBase implements ControlValueAccessor , OnDestroy , CanDisable , CanColor , OnInit , HasTabIndex { /** Whether the slider is inverted. */ @ Input ( ) get invert ( ) : boolean { return this . _invert ; } set invert ( value ) { this . _invert = coerceBooleanProperty ( value ) ; } private _invert = false ; /** The maximum value that the slider can have. */ @ Input ( ) get max ( ) : number { return this . _max ; } set max ( v ) { this . _max = coerceNumberProperty ( v , this . _max ) ; this . _percent = this . _calculatePercentage ( this . _value ) ; this . _changeDetectorRef . markForCheck ( ) ; } private _max : number = 100 ; /** The minimum value that the slider can have. */ @ Input ( ) get min ( ) : number { return this . _min ; } set min ( v ) { this . _min = coerceNumberProperty ( v , this . _min ) ; if ( this . _value === null ) { this . value = this . _min ; } this . _percent = this . _calculatePercentage ( this . _value ) ; this . _changeDetectorRef . markForCheck ( ) ; } private _min : number = 0 ; /** The values at which the thumb will snap. */ @ Input ( ) get step ( ) : number { return this . _step ; } set step ( v ) { this . _step = coerceNumberProperty ( v , this . _step ) ; if ( this . _step % 1 !== 0 ) { this . _roundToDecimal = this . _step . toString ( ) . split ( '.' ) . pop ( ) ! . length ; } this . _changeDetectorRef . markForCheck ( ) ; } private _step : number = 1 ; /** Whether or not to show the thumb label. */ @ Input ( ) get thumbLabel ( ) : boolean { return this . _thumbLabel ; } set thumbLabel ( value ) { this . _thumbLabel = coerceBooleanProperty ( value ) ; } private _thumbLabel : boolean = false ; /**
     * How often to show ticks. Relative to the step so that a tick always appears on a step.
     * Ex: Tick interval of 4 with a step of 3 will draw a tick every 4 steps (every 12 values).
     */ @ Input ( ) get tickInterval ( ) { return this . _tickInterval ; } set tickInterval ( value ) { if ( value === 'auto' ) { this . _tickInterval = 'auto' ; } else if ( typeof value === 'number' || typeof value === 'string' ) { this . _tickInterval = coerceNumberProperty ( value , this . _tickInterval as number ) ; } else { this . _tickInterval = 0 ; } } private _tickInterval : 'auto' | number = 0 ; /** Value of the slider. */ @ Input ( ) get value ( ) : number | null { if ( this . _value === null ) { this . value = this . _min ; } return this . _value ; } set value ( v ) { if ( v !== this . _value ) { let value = coerceNumberProperty ( v ) ; if ( this . _roundToDecimal ) { value = parseFloat ( value . toFixed ( this . _roundToDecimal ) ) ; } this . _value = value ; this . _percent = this . _calculatePercentage ( this . _value ) ; this . _changeDetectorRef . markForCheck ( ) ; } } private _value : number | null = null ; /**
     * 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 | null ) => string | number ; /** Whether the slider is vertical. */ @ Input ( ) get vertical ( ) : boolean { return this . _vertical ; } set vertical ( value ) { this . _vertical = coerceBooleanProperty ( value ) ; } private _vertical = false ; /** Event emitted when the slider value has changed. */ @ Output ( ) readonly change : EventEmitter < MatSliderChange > = new EventEmitter < MatSliderChange > ( ) ; /** Event emitted when the slider thumb moves. */ @ Output ( ) readonly input : EventEmitter < MatSliderChange > = new EventEmitter < MatSliderChange > ( ) ; docs-private @ Output ( ) readonly valueChange : EventEmitter < number | null > = new EventEmitter < number | null > ( ) ; /** The value to be used for display purposes. */ get displayValue ( ) : string | number { if ( this . displayWith ) { return this . displayWith ( this . value ) ; } if ( this . _roundToDecimal && this . value && this . value % 1 !== 0 ) { return this . value . toFixed ( this . _roundToDecimal ) ; } return this . value || 0 ; } /** set focus to the host element */ focus ( ) { this . _focusHostElement ( ) ; } /** blur the host element */ blur ( ) { this . _blurHostElement ( ) ; } /** onTouch function registered via registerOnTouch (ControlValueAccessor). */ onTouched : ( ) => any = ( ) => { } ; /** The percentage of the slider that coincides with the value. */ get percent ( ) : number { return this . _clamp ( this . _percent ) ; } private _percent : number = 0 ; /**
     * Whether or not the thumb is sliding.
     * Used to determine if there should be a transition for the thumb and fill track.
     */ _isSliding : boolean = false ; /**
     * Whether or not the slider is active (clicked or sliding).
     * Used to shrink and grow the thumb as according to the Material Design spec.
     */ _isActive : boolean = false ; /**
     * Whether the axis of the slider is inverted.
     * (i.e. whether moving the thumb in the positive x or y direction decreases the slider's value).
     */ get _invertAxis ( ) { return this . vertical ? ! this . invert : this . invert ; } /** Whether the slider is at its minimum value. */ get _isMinValue ( ) { return this . percent === 0 ; } /**
     * The amount of space to leave between the slider thumb and the track fill & track background
     * elements.
     */ get _thumbGap ( ) { if ( this . disabled ) { return DISABLED_THUMB_GAP ; } if ( this . _isMinValue && ! this . thumbLabel ) { return this . _isActive ? MIN_VALUE_ACTIVE_THUMB_GAP : MIN_VALUE_NONACTIVE_THUMB_GAP ; } return 0 ; } /** CSS styles for the track background element. */ get _trackBackgroundStyles ( ) : { [ key ] : string ; } { const axis = this . vertical ? 'Y' : 'X' ; const scale = this . vertical ? `1, ${ 1 - this . percent }, 1` : `${ 1 - this . percent }, 1, 1` ; const sign = this . _shouldInvertMouseCoords ( ) ? '-' : '' ; return { transform : `translate${ axis }(${ sign }${ this . _thumbGap }px) scale3d(${ scale })` } ; } /** CSS styles for the track fill element. */ get _trackFillStyles ( ) : { [ key ] : string ; } { const axis = this . vertical ? 'Y' : 'X' ; const scale = this . vertical ? `1, ${ this . percent }, 1` : `${ this . percent }, 1, 1` ; const sign = this . _shouldInvertMouseCoords ( ) ? '' : '-' ; return { transform : `translate${ axis }(${ sign }${ this . _thumbGap }px) scale3d(${ scale })` } ; } /** CSS styles for the ticks container element. */ get _ticksContainerStyles ( ) : { [ key ] : string ; } { let axis = this . vertical ? 'Y' : 'X' ; let sign = ! this . vertical && this . _getDirection ( ) == 'rtl' ? '' : '-' ; let offset = this . _tickIntervalPercent / 2 * 100 ; return { 'transform' : `translate${ axis }(${ sign }${ offset }%)` } ; } /** CSS styles for the ticks element. */ get _ticksStyles ( ) : { [ key ] : string ; } { let tickSize = this . _tickIntervalPercent * 100 ; let backgroundSize = this . vertical ? `2px ${ tickSize }%` : `${ tickSize }% 2px` ; let axis = this . vertical ? 'Y' : 'X' ; let sign = ! this . vertical && this . _getDirection ( ) == 'rtl' ? '-' : '' ; let rotate = ! this . vertical && this . _getDirection ( ) == 'rtl' ? ' rotate(180deg)' : '' ; let styles = { 'backgroundSize' : backgroundSize , 'transform' : `translateZ(0) translate${ axis }(${ sign }${ tickSize / 2 }%)${ rotate }` } ; if ( this . _isMinValue && this . _thumbGap ) { let side = this . vertical ? ( this . _invertAxis ? 'Bottom' : 'Top' ) : ( this . _invertAxis ? 'Right' : 'Left' ) ; styles [ `padding${ side }` ] = `${ this . _thumbGap }px` ; } return styles ; } get _thumbContainerStyles ( ) : { [ key ] : string ; } { let axis = this . vertical ? 'Y' : 'X' ; let invertOffset = ( this . _getDirection ( ) == 'rtl' && ! this . vertical ) ? ! this . _invertAxis : this . _invertAxis ; let offset = ( invertOffset ? this . percent : 1 - this . percent ) * 100 ; return { 'transform' : `translate${ axis }(-${ offset }%)` } ; } /** The size of a tick interval as a percentage of the size of the track. */ private _tickIntervalPercent : number = 0 ; /** The dimensions of the slider. */ private _sliderDimensions : ClientRect | null = null ; private _controlValueAccessorChangeFn : ( value : any ) => void = ( ) => { } ; /** Decimal places to round to, based on the step amount. */ private _roundToDecimal : number ; /** Subscription to the Directionality change EventEmitter. */ private _dirChangeSubscription = Subscription . EMPTY ; /** The value of the slider when the slide start event fires. */ private _valueOnSlideStart : number | null ; /** Reference to the inner slider wrapper element. */ @ ViewChild ( 'sliderWrapper' ) private _sliderWrapper : ElementRef ; /**
     * Whether mouse events should be converted to a slider position by calculating their distance
     * from the right or bottom edge of the slider as opposed to the top or left.
     */ private _shouldInvertMouseCoords ( ) { return ( this . _getDirection ( ) == 'rtl' && ! this . vertical ) ? ! this . _invertAxis : this . _invertAxis ; } /** The language direction for this slider element. */ private _getDirection ( ) { return ( this . _dir && this . _dir . value == 'rtl' ) ? 'rtl' : 'ltr' ; } constructor ( elementRef , private _focusMonitor , private _changeDetectorRef , @ Optional ( ) private _dir , @ Attribute ( 'tabindex' ) tabIndex , @ Optional ( ) @ Inject ( ANIMATION_MODULE_TYPE ) public _animationMode ? ) { super ( elementRef ) ; this . tabIndex = parseInt ( tabIndex ) || 0 ; } ngOnInit ( ) { this . _focusMonitor . monitor ( this . _elementRef , true ) . subscribe ( ( origin ) => { this . _isActive = ! ! origin && origin !== 'keyboard' ; this . _changeDetectorRef . detectChanges ( ) ; } ) ; if ( this . _dir ) { this . _dirChangeSubscription = this . _dir . change . subscribe ( ( ) => { this . _changeDetectorRef . markForCheck ( ) ; } ) ; } } ngOnDestroy ( ) { this . _focusMonitor . stopMonitoring ( this . _elementRef ) ; this . _dirChangeSubscription . unsubscribe ( ) ; } _onMouseenter ( ) { if ( this . disabled ) { return ; } this . _sliderDimensions = this . _getSliderDimensions ( ) ; this . _updateTickIntervalPercent ( ) ; } _onMousedown ( event ) { if ( this . disabled || event . button !== 0 ) { return ; } const oldValue = this . value ; this . _isSliding = false ; this . _focusHostElement ( ) ; this . _updateValueFromPosition ( { x : event . clientX , y : event . clientY } ) ; if ( oldValue != this . value ) { this . _emitInputEvent ( ) ; this . _emitChangeEvent ( ) ; } } _onSlide ( event ) { if ( this . disabled ) { return ; } if ( ! this . _isSliding ) { this . _onSlideStart ( null ) ; } event . preventDefault ( ) ; let oldValue = this . value ; this . _updateValueFromPosition ( { x : event . center . x , y : event . center . y } ) ; if ( oldValue != this . value ) { this . _emitInputEvent ( ) ; } } _onSlideStart ( event ) { if ( this . disabled || this . _isSliding ) { return ; } this . _onMouseenter ( ) ; this . _isSliding = true ; this . _focusHostElement ( ) ; this . _valueOnSlideStart = this . value ; if ( event ) { this . _updateValueFromPosition ( { x : event . center . x , y : event . center . y } ) ; event . preventDefault ( ) ; } } _onSlideEnd ( ) { this . _isSliding = false ; if ( this . _valueOnSlideStart != this . value && ! this . disabled ) { this . _emitChangeEvent ( ) ; } this . _valueOnSlideStart = null ; } _onFocus ( ) { this . _sliderDimensions = this . _getSliderDimensions ( ) ; this . _updateTickIntervalPercent ( ) ; } _onBlur ( ) { this . onTouched ( ) ; } _onKeydown ( event ) { if ( this . disabled ) { return ; } let oldValue = this . value ; switch ( event . keyCode ) { case PAGE_UP : this . _increment ( 10 ) ; break ; case PAGE_DOWN : this . _increment ( - 10 ) ; break ; case END : this . value = this . max ; break ; case HOME : this . value = this . min ; break ; case LEFT_ARROW : this . _increment ( this . _getDirection ( ) == 'rtl' ? 1 : - 1 ) ; break ; case UP_ARROW : this . _increment ( 1 ) ; break ; case RIGHT_ARROW : this . _increment ( this . _getDirection ( ) == 'rtl' ? - 1 : 1 ) ; break ; case DOWN_ARROW : this . _increment ( - 1 ) ; break ; default : return ; } if ( oldValue != this . value ) { this . _emitInputEvent ( ) ; this . _emitChangeEvent ( ) ; } this . _isSliding = true ; event . preventDefault ( ) ; } _onKeyup ( ) { this . _isSliding = false ; } /** Increments the slider by the given number of steps (negative number decrements). */ private _increment ( numSteps ) { this . value = this . _clamp ( ( this . value || 0 ) + this . step * numSteps , this . min , this . max ) ; } /** Calculate the new value from the new physical location. The value will always be snapped. */ private _updateValueFromPosition ( pos ) { if ( ! this . _sliderDimensions ) { return ; } let offset = this . vertical ? this . _sliderDimensions . top : this . _sliderDimensions . left ; let size = this . vertical ? this . _sliderDimensions . height : this . _sliderDimensions . width ; let posComponent = this . vertical ? pos . y : pos . x ; let percent = this . _clamp ( ( posComponent - offset ) / size ) ; if ( this . _shouldInvertMouseCoords ( ) ) { percent = 1 - percent ; } if ( percent === 0 ) { this . value = this . min ; } else if ( percent === 1 ) { this . value = this . max ; } else { const exactValue = this . _calculateValue ( percent ) ; const closestValue = Math . round ( ( exactValue - this . min ) / this . step ) * this . step + this . min ; this . value = this . _clamp ( closestValue , this . min , this . max ) ; } } /** Emits a change event if the current value is different from the last emitted value. */ private _emitChangeEvent ( ) { this . _controlValueAccessorChangeFn ( this . value ) ; this . valueChange . emit ( this . value ) ; this . change . emit ( this . _createChangeEvent ( ) ) ; } /** Emits an input event when the current value is different from the last emitted value. */ private _emitInputEvent ( ) { this . input . emit ( this . _createChangeEvent ( ) ) ; } /** Updates the amount of space between ticks as a percentage of the width of the slider. */ private _updateTickIntervalPercent ( ) { if ( ! this . tickInterval || ! this . _sliderDimensions ) { return ; } if ( this . tickInterval == 'auto' ) { let trackSize = this . vertical ? this . _sliderDimensions . height : this . _sliderDimensions . width ; let pixelsPerStep = trackSize * this . step / ( this . max - this . min ) ; let stepsPerTick = Math . ceil ( MIN_AUTO_TICK_SEPARATION / pixelsPerStep ) ; let pixelsPerTick = stepsPerTick * this . step ; this . _tickIntervalPercent = pixelsPerTick / trackSize ; } else { this . _tickIntervalPercent = this . tickInterval * this . step / ( this . max - this . min ) ; } } /** Creates a slider change object from the specified value. */ private _createChangeEvent ( value = this . value ) { let event = new MatSliderChange ( ) ; event . source = this ; event . value = value ; return event ; } /** Calculates the percentage of the slider that a value is. */ private _calculatePercentage ( value ) { return ( ( value || 0 ) - this . min ) / ( this . max - this . min ) ; } /** Calculates the value a percentage of the slider corresponds to. */ private _calculateValue ( percentage ) { return this . min + percentage * ( this . max - this . min ) ; } /** Return a number between two numbers. */ private _clamp ( value , min = 0 , max = 1 ) { return Math . max ( min , Math . min ( value , max ) ) ; } /**
     * Get the bounding client rect of the slider track element.
     * The track is used rather than the native element to ignore the extra space that the thumb can
     * take up.
     */ private _getSliderDimensions ( ) { return this . _sliderWrapper ? this . _sliderWrapper . nativeElement . getBoundingClientRect ( ) : null ; } /**
     * Focuses the native element.
     * Currently only used to allow a blur event to fire but will be used with keyboard input later.
     */ private _focusHostElement ( ) { this . _elementRef . nativeElement . focus ( ) ; } /** Blurs the native element. */ private _blurHostElement ( ) { this . _elementRef . nativeElement . blur ( ) ; } param value writeValue ( value ) { this . value = value ; } param fn registerOnChange ( fn ) { this . _controlValueAccessorChangeFn = fn ; } param fn registerOnTouched ( fn ) { this . onTouched = fn ; } param isDisabled setDisabledState ( isDisabled ) { this . disabled = isDisabled ; } }