misttechnologies/custom-range-input

View on GitHub
src/js/customrangeinput.js

Summary

Maintainability
A
1 hr
Test Coverage
/* global require, ShadyCSS */
/**
 * @license
 * Copyright (c) 2017 Mist Technologies, Inc. All rights reserved.
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * * Redistributions of source code must retain the above copyright notice, this
 *   list of conditions and the following disclaimer.
 *
 * * Redistributions in binary form must reproduce the above copyright notice,
 *   this list of conditions and the following disclaimer in the documentation
 *   and/or other materials provided with the distribution.
 *
 * * Neither the name of the copyright holder nor the names of its
 *   contributors may be used to endorse or promote products derived from
 *   this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

/**
 * Actually CustomRangeInput extends HTMLInputElement (<input> element) but
 * it seems not to work on current browser implementations.
 * Thus we define CustomRangeInput from scratch, extending "plain" HTMLElement
 */
class CustomRangeInput extends HTMLElement {
  constructor() {
    super();

    // Though native shadow DOM spec allows elements to be inserted
    // directly into shadowRoot using innerHTML (without <template>),
    // creating shadow DOM via <template> is required by ShadyCSS to
    // prepare / convert styles so that they can 'shim'.
    const template = document.createElement("template");
    template.innerHTML =
      `
      <style>${require("!css-loader!sass-loader!./../css/style.scss")}</style>
      <div class="bar">
      <div class="loaded"></div>
      <div class="passed"></div>
      <div class="handle"></div>
      </div>
      `;
    if (window.ShadyCSS) {
      // Here styles (e.g. `:host`) go converted
      ShadyCSS.prepareTemplate(template, "custom-range-input");
    }

    // ShadowDOM construction
    this.attachShadow({mode: "open"});
    this.shadowRoot.appendChild(
        document.importNode(template.content, true));
    this._bar    = this.shadowRoot.querySelector(".bar");
    this._loaded = this.shadowRoot.querySelector(".loaded");
    this._passed = this.shadowRoot.querySelector(".passed");
    this._handle = this.shadowRoot.querySelector(".handle");

    this._setupListeners();
  }

  /**
   * @method connectedCallback
   * called when the element is appended to a document.
   * One of the Lifecycle Callbacks of customElements
   */
  connectedCallback() {
    // NOTE: we need to reassign and release unmanaged props which were set
    // before the element was upgraded.
    this.setAttribute("min", this.min || 0);
    delete this.min;
    this.setAttribute("max", this.max || 100);
    delete this.max;
    this.setAttribute("step", this.step || 0.1);
    delete this.step;
    this.setAttribute("value", this.value || 0);
    delete this.value;
    this.setAttribute("subvalue", this.subvalue || 0);
    delete this.subvalue;
  }
  /**
   * @method disconnectedCallback
   * called when the element is removed from a document.
   * One of the Lifecycle Callbacks of customElements
   */
  disconnectedCallback() {
  }

  _setupListeners() {
    const _onmd = (e) => {
      document.addEventListener("mousemove", _onmm, false);
      document.addEventListener("mouseup",   _onmu, false);
      document.addEventListener("touchmove", _onmm, false);
      document.addEventListener("touchend",  _onmu, false);
      _onmm(e);
    };
    this.addEventListener("mousedown",  _onmd);
    this.addEventListener("touchstart", _onmd);

    const _onmm = (e) => {
      let x = ("touches" in e) ?
        e.touches[0].pageX : e.pageX;
      const rect = this._bar.getBoundingClientRect();
      window.console.assert(rect.right-rect.left > 0);

      this.value =
        (x-rect.left) / (rect.right-rect.left) * (this.max-this.min) +
        this.min;

      /**
       * @event changing
       * dispatched when the value is about to changing
       */
      this.dispatchEvent(new CustomEvent("changing"));
      e.preventDefault();
    };

    const _onmu = () => {
      document.removeEventListener("mousemove", _onmm, false);
      document.removeEventListener("mouseup",   _onmu, false);
      document.removeEventListener("touchmove", _onmm, false);
      document.removeEventListener("touchend",  _onmu, false);

      /**
       * @event changed
       * dispatched when a series of value update is ended
       */
      this.dispatchEvent(new CustomEvent("changed"));
    };
  }

  /*********************************************************************
   * Here are attributes and properties managements below.
   * Synchronizing attrs with props.
   *********************************************************************/
  get value( ) { return Number(this.getAttribute("value") || 0.0); }
  set value(v) {               this.setAttribute("value", this._value(v));
    this._passed.style.width =
      this._handle.style.left =
      this._percentage(this.value) + "%";
  }
  get subvalue( ) { return Number(this.getAttribute("subvalue") || 0.0); }
  set subvalue(v) {               this.setAttribute("subvalue", this._value(v));
    this._loaded.style.width = this._percentage(this.subvalue) + "%";
  }
  get min( )  { return Number(this.getAttribute("min") || 0.0); }
  set min(v)  {               this.setAttribute("min", v); }
  get max( )  { return Number(this.getAttribute("max") || 100.0); }
  set max(v)  {               this.setAttribute("max", v); }
  get step( ) { return Number(this.getAttribute("step") || 0.1); }
  set step(v) {               this.setAttribute("step", v); }
  get type( ) { return "range"; }

  static get observedAttributes() {
    return ["value", "subvalue", "min", "max", "step"];
  }
  attributeChangedCallback(prop, oldValue, newValue) {
    if (oldValue != newValue) {
      this[prop] = Number(newValue);
      this.dispatchEvent(new CustomEvent("change"));
    }
  }

  /**
   * @private
   * @method _value
   * @params v {number|string}
   * Regulates the given value `v` to make it stay between `min` and `max`.
   */
  _value(v) {
    const k = Math.pow(10, String(this.step).split(".")[1].length);
    return Math.min(Math.max(
          (Math.round(v * k) * 1.0) / k,
          this.min), this.max);
  }
  _percentage(v) {
    return 100.0 * (v - this.min) / (this.max - this.min);
  }
}
CustomRangeInput.version = require("./../../package.json").version;
window.CustomRangeInput = CustomRangeInput;

export default CustomRangeInput;