<!-- @license Copyright (c) 2015 The Polymer Project Authors. All rights reserved. This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as part of the polymer project is also subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt --> <link rel="import" href="../polymer/polymer.html"> <link rel="import" href="../iron-flex-layout/iron-flex-layout.html"> <link rel="import" href="../paper-styles/color.html"> <link rel="import" href="../paper-progress/paper-progress.html"> <link rel="import" href="../paper-input/paper-input.html"> <link rel="import" href="../paper-behaviors/paper-inky-focus-behavior.html"> <link rel="import" href="../paper-ripple/paper-ripple.html"> <link rel="import" href="../iron-a11y-keys-behavior/iron-a11y-keys-behavior.html"> <link rel="import" href="../iron-range-behavior/iron-range-behavior.html"> <link rel="import" href="../iron-form-element-behavior/iron-form-element-behavior.html"> <!-- Material design: [Sliders](https://www.google.com/design/spec/components/sliders.html) `paper-slider` allows user to select a value from a range of values by moving the slider thumb. The interactive nature of the slider makes it a great choice for settings that reflect intensity levels, such as volume, brightness, or color saturation. Example: <paper-slider></paper-slider> Use `min` and `max` to specify the slider range. Default is 0 to 100. Example: <paper-slider min="10" max="200" value="110"></paper-slider> ### Styling The following custom properties and mixins are available for styling: Custom property | Description | Default ----------------|-------------|---------- `--paper-slider-bar-color` | The background color of the slider | `transparent` `--paper-slider-active-color` | The progress bar color | `--google-blue-700` `--paper-slider-secondary-color` | The secondary progress bar color | `--google-blue-300` `--paper-slider-knob-color` | The knob color | `--google-blue-700` `--paper-slider-disabled-knob-color` | The disabled knob color | `--google-grey-500` `--paper-slider-pin-color` | The pin color | `--google-blue-700` `--paper-slider-font-color` | The pin's text color | `#fff` `--paper-slider-disabled-active-color` | The disabled progress bar color | `--google-grey-500` `--paper-slider-disabled-secondary-color` | The disabled secondary progress bar color | `--google-grey-300` `--paper-slider-knob-start-color` | The fill color of the knob at the far left | `transparent` `--paper-slider-knob-start-border-color` | The border color of the knob at the far left | `#c8c8c8` `--paper-slider-pin-start-color` | The color of the pin at the far left | `#c8c8c8` `--paper-slider-height` | Height of the progress bar | `2px` @group Paper Elements @element paper-slider @demo demo/index.html @hero hero.svg --> <dom-module id="paper-slider"> <template strip-whitespace> <style> :host { display: inline-block; width: 200px; cursor: default; -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; -webkit-tap-highlight-color: rgba(0, 0, 0, 0); --paper-progress-active-color: var(--paper-slider-active-color, --google-blue-700); --paper-progress-secondary-color: var(--paper-slider-secondary-color, --google-blue-300); --paper-progress-disabled-active-color: var(--paper-slider-disabled-active-color, --google-grey-500); --paper-progress-disabled-secondary-color: var(--paper-slider-disabled-secondary-color, --google-grey-300); } /* focus shows the ripple */ :host(:focus) { outline: none; } #sliderContainer { position: relative; width: calc(100% - 32px); height: 32px; } #sliderContainer:focus { outline: 0; } #sliderContainer.editable { float: left; width: calc(100% - 72px); margin: 12px 0; } .bar-container { position: absolute; top: 0; left: 16px; height: 100%; width: 100%; overflow: hidden; } .ring > .bar-container { left: 20px; width: calc(100% - 4px); transition: left 0.18s ease, width 0.18s ease; } .ring.expand:not(.pin) > .bar-container { left: 30px; width: calc(100% - 14px); } .ring.expand.dragging > .bar-container { transition: none; } #sliderBar { position: absolute; top: 15px; left: 0; width: 100%; padding: 8px 0; margin: -8px 0; background-color: var(--paper-slider-bar-color, transparent); --paper-progress-height: var(--paper-slider-height, 2px); } .ring #sliderBar { left: -4px; width: calc(100% + 4px); } .ring.expand:not(.pin) #sliderBar { left: -14px; width: calc(100% + 14px); } .slider-markers { position: absolute; top: 15px; left: 15px; height: 2px; width: calc(100% + 2px); box-sizing: border-box; pointer-events: none; } .slider-markers::after, .slider-marker::after { content: ""; display: block; width: 2px; height: 2px; border-radius: 50%; background-color: black; } #sliderKnob { @apply(--layout-center-justified); @apply(--layout-center); @apply(--layout-horizontal); position: absolute; left: 0; top: 0; width: 32px; height: 32px; } .transiting > #sliderKnob { transition: left 0.08s ease; } #sliderKnob:focus { outline: none; } #sliderKnob.dragging { transition: none; } .snaps > #sliderKnob.dragging { transition: -webkit-transform 0.08s ease; transition: transform 0.08s ease; } #sliderKnobInner { width: 12px; height: 12px; border-radius: 50%; background-color: var(--paper-slider-knob-color, --google-blue-700); -moz-box-sizing: border-box; box-sizing: border-box; transition-property: height, width, background-color, border; transition-duration: 0.1s; transition-timing-function: ease; } .expand:not(.pin) > #sliderKnob > #sliderKnobInner { width: 100%; height: 100%; -webkit-transform: translateZ(0); transform: translateZ(0); } .ring > #sliderKnob > #sliderKnobInner { background-color: var(--paper-slider-knob-start-color, transparent); border: 2px solid var(--paper-slider-knob-start-border-color, #c8c8c8); } #sliderKnobInner::before { background-color: var(--paper-slider-pin-color, --google-blue-700); } .pin > #sliderKnob > #sliderKnobInner::before { content: ""; position: absolute; top: 0; left: 0; width: 26px; height: 26px; margin-left: 3px; border-radius: 50% 50% 50% 0; -webkit-transform: rotate(-45deg) scale(0) translate(0); transform: rotate(-45deg) scale(0) translate(0); } #sliderKnobInner::before, #sliderKnobInner::after { transition: -webkit-transform .2s ease, background-color .18s ease; transition: transform .2s ease, background-color .18s ease; } .pin.ring > #sliderKnob > #sliderKnobInner::before { background-color: var(--paper-slider-pin-start-color, #c8c8c8); } .pin.expand > #sliderKnob > #sliderKnobInner::before { -webkit-transform: rotate(-45deg) scale(1) translate(17px, -17px); transform: rotate(-45deg) scale(1) translate(17px, -17px); } .pin > #sliderKnob > #sliderKnobInner::after { content: attr(value); position: absolute; top: 0; left: 0; width: 32px; height: 26px; text-align: center; color: var(--paper-slider-font-color, #fff); font-size: 10px; -webkit-transform: scale(0) translate(0); transform: scale(0) translate(0); } .pin.expand > #sliderKnob > #sliderKnobInner::after { -webkit-transform: scale(1) translate(0, -17px); transform: scale(1) translate(0, -17px); } /* paper-input */ .slider-input { width: 50px; float: right; overflow: hidden; --paper-input-container-input: { text-align: center; }; } /* disabled state */ #sliderContainer.disabled { pointer-events: none; } .disabled > #sliderKnob > #sliderKnobInner { width: 8px; height: 8px; background-color: var(--paper-slider-disabled-knob-color, --google-grey-500); } .disabled.ring > #sliderKnob > #sliderKnobInner { background-color: transparent; } paper-ripple { color: var(--paper-slider-knob-color, --google-blue-700); } </style> <div id="sliderContainer" class$="[[_getClassNames(disabled, pin, snaps, immediateValue, min, expand, dragging, transiting, editable)]]"> <div class="bar-container"> <paper-progress disabled$="[[disabled]]" id="sliderBar" aria-hidden="true" min="[[min]]" max="[[max]]" step="[[step]]" value="[[immediateValue]]" secondary-progress="[[secondaryProgress]]" on-down="_bardown" on-up="_resetKnob" on-track="_onTrack"> </paper-progress> </div> <template is="dom-if" if="[[snaps]]"> <div class="slider-markers horizontal layout"> <template is="dom-repeat" items="[[markers]]"> <div class="slider-marker flex"></div> </template> </div> </template> <div id="sliderKnob" class="center-justified center horizontal layout" on-down="_knobdown" on-up="_resetKnob" on-track="_onTrack" on-transitionend="_knobTransitionEnd"> <div id="sliderKnobInner" value$="[[immediateValue]]"></div> </div> </div> <template is="dom-if" if="[[editable]]"> <paper-input id="input" type="number" step="[[step]]" min="[[min]]" max="[[max]]" class="slider-input" disabled$="[[disabled]]" value="[[immediateValue]]" on-change="_changeValue" on-keydown="_inputKeyDown" no-label-float> </paper-input> </template> </template> </dom-module> <script> Polymer({ is: 'paper-slider', behaviors: [ 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, observer: '_maxMarkersChanged' }, /** * 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: [] }, }, observers: [ '_updateKnob(value, min, max, snaps, step)', '_valueChanged(value)', '_immediateValueChanged(immediateValue)' ], hostAttributes: { role: 'slider', tabindex: 0 }, keyBindings: { 'left down pagedown home': '_decrementKey', 'right up pageup end': '_incrementKey' }, ready: function() { // issue polymer/polymer#1305 this.async(function() { this._updateKnob(this.value); }, 1); }, /** * 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)); }, _valueChanged: function() { this.fire('value-change'); }, _immediateValueChanged: function() { if (this.dragging) { this.fire('immediate-value-change'); } 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)); this.$.sliderKnob.style.left = (this.ratio * 100) + '%'; }, _calcKnobPosition: function(ratio) { return (this.max - this.min) * ratio + 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._w = this.$.sliderBar.offsetWidth; this._x = this.ratio * this._w; this._startx = this._x || 0; this._minx = - this._startx; this._maxx = this._w - this._startx; this.$.sliderKnob.classList.add('dragging'); this._setDragging(true); }, _trackX: function(e) { if (!this.dragging) { this._trackStart(e); } var dx = Math.min(this._maxx, Math.max(this._minx, e.detail.dx)); this._x = this._startx + dx; var immediateValue = this._calcStep(this._calcKnobPosition(this._x / this._w)); this._setImmediateValue(immediateValue); // update knob's position var translateX = ((this._calcRatio(immediateValue) * this._w) - this._startx); 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'); }, _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; 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'); }); // cancel selection event.preventDefault(); }, _knobTransitionEnd: function(event) { if (event.target === this.$.sliderKnob) { this._setTransiting(false); } }, _maxMarkersChanged: function(maxMarkers) { var l = (this.max - this.min) / this.step; if (!this.snaps && l > maxMarkers) { this._setMarkers([]); } else { this._setMarkers(new Array(l)); } }, _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 }); }, _incrementKey: function(event) { if (!this.disabled) { if (event.detail.key === 'end') { this.value = this.max; } else { this.increment(); } this.fire('change'); } }, _decrementKey: function(event) { if (!this.disabled) { if (event.detail.key === 'home') { this.value = this.min; } else { this.decrement(); } this.fire('change'); } }, _changeValue: function(event) { this.value = event.target.value; this.fire('change'); }, _inputKeyDown: function(event) { event.stopPropagation(); }, // create the element ripple inside the `sliderKnob` _createRipple: function() { this._rippleContainer = this.$.sliderKnob; return Polymer.PaperInkyFocusBehaviorImpl._createRipple.call(this); }, // Hide the ripple when user is not interacting with keyboard. // This behavior is different from other ripple-y controls, but is // according to spec: https://www.google.com/design/spec/components/sliders.html _focusedChanged: function(receivedFocusFromKeyboard) { if (receivedFocusFromKeyboard) { this.ensureRipple(); } if (this.hasRipple()) { // note, ripple must be un-hidden prior to setting `holdDown` if (receivedFocusFromKeyboard) { this._ripple.removeAttribute('hidden'); } else { this._ripple.setAttribute('hidden', ''); } this._ripple.holdDown = receivedFocusFromKeyboard; } } }); /** * Fired when the slider's value changes. * * @event value-change */ /** * Fired when the slider's immediateValue changes. * * @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 */ </script>