<!--
@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>