Source: slider.js

import Widget from "ui/core/widget";
import Constraint from "util/constraint";
import ConstraintSpec from "util/constraint-def";
import MathUtil from "util/util-math";

/**
 * Class representing a Slider widget.
 * @class
 * @implements {Widget}
 */
class Slider extends Widget {

  /**
   * @constructor
   * @param {object} container - DOM container for the widget.
   * @param {object} [o] - Options.
   * @param {number} [o.minVal=0] - The minimum possible value the slider can represent.
   * @param {number} [o.maxVal=127] - The maximum possible value teh slider can represent.
   * @param {number} [o.step=1] - Step granularity.
   * @param {string} [o.sliderBodyColor="#484848"] - The color of the slider bar.
   * @param {string} [o.sliderHandleColor="#484848"] - The color of the triangle used as the slider's needle.
   */
  constructor(container, o) {
    super(container, o);
    return this;
  }

  /* ===========================================================================
  *  INITIALIZATION METHODS
  */

  /**
   * Initialize the options
   * @override
   * @protected
   */
  _initOptions(o) {
    // set the defaults
    this.o = {
      minVal: 0,
      maxVal: 127,
      step: 1,
      sliderBodyColor: "#484848",
      sliderHandleColor: "#484848",
      mouseSensitivity: 1.2
    };

    // override defaults with provided options
    super._initOptions(o);

    // set the precision (num of decimal places used) based on the step interval
    this.o.stepPrecision =  MathUtil.getPrecision(this.o.step);
  }

  /**
   * Initialize state constraints
   * @override
   * @protected
   */
  _initStateConstraints() {
    const _this = this;

    this.stateConstraints = new ConstraintSpec({
      val: new Constraint({ 
        min: _this.o.minVal, 
        max: _this.o.maxVal, 
        transform: (num) => MathUtil.quantize(num, _this.o.step, _this.o.stepPrecision) 
      })   
    });
  }

  /**
   * Initialize state.
   * @override
   * @protected
   */
  _initState() {
    this.state = {
      val: this.o.minVal
    };

    // keep track of dimensions
    this.dims = {
      offsetBottom: 5,
      offsetTop: 5,
      bodyWidth: 2,
      handleWidth: 10,
      handleHeight: 10
    };
  }

  /**
   * Initialize the svg elements
   * @override
   * @protected
   */
  _initSvgEls() {
    const _this = this;

    this.svgEls = {
      body: document.createElementNS(_this.SVG_NS, "rect"),
      overlay: document.createElementNS(_this.SVG_NS, "rect"),
      handle: document.createElementNS(_this.SVG_NS, "polygon")
    };

    this._appendSvgEls();
    this._update();
  }

  /**
   * Initialize mouse and touch event handlers
   * @override
   * @protected
   */
  _initHandlers() {
    const _this = this;

    this.handlers = {

      touchBody: function touchBody(ev) {
        ev.preventDefault();
        ev.stopPropagation();

        let newVal = _this._calcTouchVal(ev.clientY);
        _this.setState({ val: newVal });
        
        _this.handlers.touchHandle(ev);
      },

      touchHandle: function touchHandle(ev) {
        ev.preventDefault();
        ev.stopPropagation();
        
        document.body.addEventListener("mousemove", _this.handlers.moveHandle);
        document.body.addEventListener("touchmove", _this.handlers.moveHandle);
        document.body.addEventListener("mouseup", _this.handlers.releaseHandle); 
        document.body.addEventListener("touchend", _this.handlers.releaseHandle); 
      },

      moveHandle: function moveHandle(ev) {
        ev.preventDefault();
        ev.stopPropagation();

        let newVal = _this._calcTouchVal(ev.clientY);
        
        _this.setState({ val: newVal });
      },

      releaseHandle: function releaseHandle(ev) {
        ev.preventDefault();
        ev.stopPropagation();

        document.body.removeEventListener("touchmove", _this.handlers.moveHandle); 
        document.body.removeEventListener("mousemove", _this.handlers.moveHandle);
        document.body.removeEventListener("mouseup", _this.handlers.releaseHandle); 
        document.body.removeEventListener("touchend", _this.handlers.releaseHandle); 
      }
    };

    this.svgEls.overlay.addEventListener("mousedown", _this.handlers.touchBody);
    this.svgEls.overlay.addEventListener("touchstart", _this.handlers.touchBody);
    this.svgEls.handle.addEventListener("mousedown", _this.handlers.touchHandle);
    this.svgEls.handle.addEventListener("touchstart", _this.handlers.touchHandle);
  }

  /**
   * Update (redraw) component based on state
   * @override
   * @protected
   */
  _update() {
    const _this = this;

    let sliderBodyPos = _this._calcSliderBodyPos();

    this.svgEls.body.setAttribute("x", sliderBodyPos.x);
    this.svgEls.body.setAttribute("y", sliderBodyPos.y);
    this.svgEls.body.setAttribute("width", _this.dims.bodyWidth);
    this.svgEls.body.setAttribute("height", _this._calcSliderBodyHeight());
    this.svgEls.body.setAttribute("fill", _this.o.sliderBodyColor);

    this.svgEls.overlay.setAttribute("x", sliderBodyPos.x);
    this.svgEls.overlay.setAttribute("y", sliderBodyPos.y);
    this.svgEls.overlay.setAttribute("width", _this.dims.bodyWidth + _this.dims.handleWidth);
    this.svgEls.overlay.setAttribute("height", _this._calcSliderBodyHeight());
    this.svgEls.overlay.setAttribute("fill", "transparent");

    let sliderHandlePoints = _this._calcSliderHandlePoints();

    this.svgEls.handle.setAttribute("points", sliderHandlePoints);
    this.svgEls.handle.setAttribute("fill", _this.o.sliderHandleColor);
  }

  /* ===========================================================================
  *  PUBLIC API
  */

  /**
   * Get the slider value.
   * @public
   */
  getVal() {
    return this.state.val;
  }

  /**
   * Set the current slider value.
   * Same as setVal(), but will not cause an observer callback trigger.
   * @public
   * @param {number} newVal - The new slider value.
   */
  setInternalVal(newVal) {
    this.setInternalState({ val: newVal });
  }

  /**
   * Set the current slider value.
   * Same as setInternalVal(), but will cause an observer callback trigger.
   * @public
   * @param {number} newVal - The new slider value.
   */
  setVal(newVal) {
    this.setState({ val: newVal });
  }

  /* ===========================================================================
  *  HELPER METHODS
  */

  /**
   * Returns the position and dimensions for the slider body.
   * @private
   * @returns {object} - {x, y} position.
   */
  _calcSliderBodyPos() {
    const _this = this;

    return {
      x: _this._getWidth() / 2 - 1,
      y: _this.dims.offsetTop
    };
  }

  /**
   * Returns the height of the slider body.
   * @private
   * @returns {number} - Height of the slider body.
   */
  _calcSliderBodyHeight() {
    return this._getHeight() - this.dims.offsetTop - this.dims.offsetBottom;
  }

  /**
   * Returns the height of the slider body.
   * @private
   * @returns {number} - Width of the slider body.
   */
  _calcSliderBodyWidth() {
    return this.dims.bodyWidth;
  }

    /**
   * Returns the position and dimensions for the slider body.
   * @private
   * @returns {object} - {x, y} position.
   */
  _calcSliderHandlePoints() {
    const _this = this;

    let sliderBodyHeight = _this._calcSliderBodyHeight();

    let x0 = (_this._getWidth() / 2) + 1;
    let y0 = (sliderBodyHeight - (_this.state.val / (_this.o.maxVal - _this.o.minVal)) * sliderBodyHeight) + _this.dims.offsetBottom;
    let x1 = x0 + this.dims.handleWidth;
    let y1 = y0 - this.dims.handleHeight / 2;
    let x2 = x1;
    let y2 = y0 + this.dims.handleHeight / 2;

    return x0 + "," + y0 + " " +
           x1 + "," + y1 + " " +
           x2 + "," + y2;
  }

  /**
   * Calculate the value of the slider touched at position y.
   * @private
   * @param {number} y - Y-value of the touch location.
   * @returns {number} - Value of the slider at the touched location.
   */
  _calcTouchVal(y) {
    let valRange = this.o.maxVal - this.o.minVal;
    let bodyY = (this._getHeight() - this._getRelativeY(y)) - this.dims.offsetBottom;
    let touchVal = ((bodyY / this._calcSliderBodyHeight()) * valRange) + this.o.minVal; 

    return touchVal;
  }

  /**
   * Calculates the precision with which the state value changes when moved.
   */
  _calcMovePrecision() {
    let precision = (this.o.maxVal - this.o.minVal) / 127;
    return precision;
  }
}

export default Slider;