Polymer({ | |
is: 'paper-slider', | |
behaviors: [ | |
Polymer.IronA11yKeysBehavior, | |
Polymer.IronFormElementBehavior, | |
Polymer.PaperInkyFocusBehavior, | |
Polymer.IronRangeBehavior | |
], | |
properties: { | |
/** | |
* If true, the slider thumb snaps to tick marks evenly spaced based | |
* on the `step` property value. | |
*/ | |
snaps: { | |
type: Boolean, | |
value: false, | |
notify: true | |
}, | |
/** | |
* If true, a pin with numeric value label is shown when the slider thumb | |
* is pressed. Use for settings for which users need to know the exact | |
* value of the setting. | |
*/ | |
pin: { | |
type: Boolean, | |
value: false, | |
notify: true | |
}, | |
/** | |
* The number that represents the current secondary progress. | |
*/ | |
secondaryProgress: { | |
type: Number, | |
value: 0, | |
notify: true, | |
observer: '_secondaryProgressChanged' | |
}, | |
/** | |
* If true, an input is shown and user can use it to set the slider value. | |
*/ | |
editable: { | |
type: Boolean, | |
value: false | |
}, | |
/** | |
* The immediate value of the slider. This value is updated while the user | |
* is dragging the slider. | |
*/ | |
immediateValue: { | |
type: Number, | |
value: 0, | |
readOnly: true, | |
notify: true | |
}, | |
/** | |
* The maximum number of markers | |
*/ | |
maxMarkers: { | |
type: Number, | |
value: 0, | |
notify: true | |
}, | |
/** | |
* If true, the knob is expanded | |
*/ | |
expand: { | |
type: Boolean, | |
value: false, | |
readOnly: true | |
}, | |
/** | |
* True when the user is dragging the slider. | |
*/ | |
dragging: { | |
type: Boolean, | |
value: false, | |
readOnly: true | |
}, | |
transiting: { | |
type: Boolean, | |
value: false, | |
readOnly: true | |
}, | |
markers: { | |
type: Array, | |
readOnly: true, | |
value: function() { | |
return []; | |
} | |
}, | |
}, | |
observers: [ | |
'_updateKnob(value, min, max, snaps, step)', | |
'_valueChanged(value)', | |
'_immediateValueChanged(immediateValue)', | |
'_updateMarkers(maxMarkers, min, max, snaps)' | |
], | |
hostAttributes: { | |
role: 'slider', | |
tabindex: 0 | |
}, | |
keyBindings: { | |
'left': '_leftKey', | |
'right': '_rightKey', | |
'down pagedown home': '_decrementKey', | |
'up pageup end': '_incrementKey' | |
}, | |
/** | |
* Increases value by `step` but not above `max`. | |
* @method increment | |
*/ | |
increment: function() { | |
this.value = this._clampValue(this.value + this.step); | |
}, | |
/** | |
* Decreases value by `step` but not below `min`. | |
* @method decrement | |
*/ | |
decrement: function() { | |
this.value = this._clampValue(this.value - this.step); | |
}, | |
_updateKnob: function(value, min, max, snaps, step) { | |
this.setAttribute('aria-valuemin', min); | |
this.setAttribute('aria-valuemax', max); | |
this.setAttribute('aria-valuenow', value); | |
this._positionKnob(this._calcRatio(value) * 100); | |
}, | |
_valueChanged: function() { | |
this.fire('value-change', {composed: true}); | |
}, | |
_immediateValueChanged: function() { | |
if (this.dragging) { | |
this.fire('immediate-value-change', {composed: true}); | |
} else { | |
this.value = this.immediateValue; | |
} | |
}, | |
_secondaryProgressChanged: function() { | |
this.secondaryProgress = this._clampValue(this.secondaryProgress); | |
}, | |
_expandKnob: function() { | |
this._setExpand(true); | |
}, | |
_resetKnob: function() { | |
this.cancelDebouncer('expandKnob'); | |
this._setExpand(false); | |
}, | |
_positionKnob: function(ratio) { | |
this._setImmediateValue(this._calcStep(this._calcKnobPosition(ratio))); | |
this._setRatio(this._calcRatio(this.immediateValue) * 100); | |
this.$.sliderKnob.style.left = this.ratio + '%'; | |
if (this.dragging) { | |
this._knobstartx = (this.ratio * this._w) / 100; | |
this.translate3d(0, 0, 0, this.$.sliderKnob); | |
} | |
}, | |
_calcKnobPosition: function(ratio) { | |
return (this.max - this.min) * ratio / 100 + this.min; | |
}, | |
_onTrack: function(event) { | |
event.stopPropagation(); | |
switch (event.detail.state) { | |
case 'start': | |
this._trackStart(event); | |
break; | |
case 'track': | |
this._trackX(event); | |
break; | |
case 'end': | |
this._trackEnd(); | |
break; | |
} | |
}, | |
_trackStart: function(event) { | |
this._setTransiting(false); | |
this._w = this.$.sliderBar.offsetWidth; | |
this._x = this.ratio * this._w / 100; | |
this._startx = this._x; | |
this._knobstartx = this._startx; | |
this._minx = - this._startx; | |
this._maxx = this._w - this._startx; | |
this.$.sliderKnob.classList.add('dragging'); | |
this._setDragging(true); | |
}, | |
_trackX: function(event) { | |
if (!this.dragging) { | |
this._trackStart(event); | |
} | |
var direction = this._isRTL ? -1 : 1; | |
var dx = Math.min( | |
this._maxx, Math.max(this._minx, event.detail.dx * direction)); | |
this._x = this._startx + dx; | |
var immediateValue = this._calcStep(this._calcKnobPosition(this._x / this._w * 100)); | |
this._setImmediateValue(immediateValue); | |
// update knob's position | |
var translateX = ((this._calcRatio(this.immediateValue) * this._w) - this._knobstartx); | |
this.translate3d(translateX + 'px', 0, 0, this.$.sliderKnob); | |
}, | |
_trackEnd: function() { | |
var s = this.$.sliderKnob.style; | |
this.$.sliderKnob.classList.remove('dragging'); | |
this._setDragging(false); | |
this._resetKnob(); | |
this.value = this.immediateValue; | |
s.transform = s.webkitTransform = ''; | |
this.fire('change', {composed: true}); | |
}, | |
_knobdown: function(event) { | |
this._expandKnob(); | |
// cancel selection | |
event.preventDefault(); | |
// set the focus manually because we will called prevent default | |
this.focus(); | |
}, | |
_bardown: function(event) { | |
this._w = this.$.sliderBar.offsetWidth; | |
var rect = this.$.sliderBar.getBoundingClientRect(); | |
var ratio = (event.detail.x - rect.left) / this._w * 100; | |
if (this._isRTL) { | |
ratio = 100 - ratio; | |
} | |
var prevRatio = this.ratio; | |
this._setTransiting(true); | |
this._positionKnob(ratio); | |
this.debounce('expandKnob', this._expandKnob, 60); | |
// if the ratio doesn't change, sliderKnob's animation won't start | |
// and `_knobTransitionEnd` won't be called | |
// Therefore, we need to manually update the `transiting` state | |
if (prevRatio === this.ratio) { | |
this._setTransiting(false); | |
} | |
this.async(function() { | |
this.fire('change', {composed: true}); | |
}); | |
// cancel selection | |
event.preventDefault(); | |
// set the focus manually because we will called prevent default | |
this.focus(); | |
}, | |
_knobTransitionEnd: function(event) { | |
if (event.target === this.$.sliderKnob) { | |
this._setTransiting(false); | |
} | |
}, | |
_updateMarkers: function(maxMarkers, min, max, snaps) { | |
if (!snaps) { | |
this._setMarkers([]); | |
} | |
var steps = Math.round((max - min) / this.step); | |
if (steps > maxMarkers) { | |
steps = maxMarkers; | |
} | |
if (steps < 0 || !isFinite(steps)) { | |
steps = 0; | |
} | |
this._setMarkers(new Array(steps)); | |
}, | |
_mergeClasses: function(classes) { | |
return Object.keys(classes).filter( | |
function(className) { | |
return classes[className]; | |
}).join(' '); | |
}, | |
_getClassNames: function() { | |
return this._mergeClasses({ | |
disabled: this.disabled, | |
pin: this.pin, | |
snaps: this.snaps, | |
ring: this.immediateValue <= this.min, | |
expand: this.expand, | |
dragging: this.dragging, | |
transiting: this.transiting, | |
editable: this.editable | |
}); | |
}, | |
get _isRTL() { | |
if (this.__isRTL === undefined) { | |
this.__isRTL = window.getComputedStyle(this)['direction'] === 'rtl'; | |
} | |
return this.__isRTL; | |
}, | |
_leftKey: function(event) { | |
if (this._isRTL) | |
this._incrementKey(event); | |
else | |
this._decrementKey(event); | |
}, | |
_rightKey: function(event) { | |
if (this._isRTL) | |
this._decrementKey(event); | |
else | |
this._incrementKey(event); | |
}, | |
_incrementKey: function(event) { | |
if (!this.disabled) { | |
if (event.detail.key === 'end') { | |
this.value = this.max; | |
} else { | |
this.increment(); | |
} | |
this.fire('change'); | |
event.preventDefault(); | |
} | |
}, | |
_decrementKey: function(event) { | |
if (!this.disabled) { | |
if (event.detail.key === 'home') { | |
this.value = this.min; | |
} else { | |
this.decrement(); | |
} | |
this.fire('change'); | |
event.preventDefault(); | |
} | |
}, | |
_changeValue: function(event) { | |
this.value = event.target.value; | |
this.fire('change', {composed: true}); | |
}, | |
_inputKeyDown: function(event) { | |
event.stopPropagation(); | |
}, | |
// create the element ripple inside the `sliderKnob` | |
_createRipple: function() { | |
this._rippleContainer = this.$.sliderKnob; | |
return Polymer.PaperInkyFocusBehaviorImpl._createRipple.call(this); | |
}, | |
}); | |
/** | |
* Fired when the slider's value changes. | |
* | |
* @event value-change | |
*/ | |
/** | |
* Fired when the slider's immediateValue changes. Only occurs while the | |
* user is dragging. | |
* | |
* To detect changes to immediateValue that happen for any input (i.e. | |
* dragging, tapping, clicking, etc.) listen for immediate-value-changed | |
* instead. | |
* | |
* @event immediate-value-change | |
*/ | |
/** | |
* Fired when the slider's value changes due to user interaction. | |
* | |
* Changes to the slider's value due to changes in an underlying | |
* bound variable will not trigger this event. | |
* | |
* @event change | |
*/ |