widgets/slider.js

/*
 * This file is part of AUX.
 *
 * AUX is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public
 * License as published by the Free Software Foundation; either
 * version 3 of the License, or (at your option) any later version.
 *
 * AUX is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * General Public License for more details.
 *
 * You should have received a copy of the GNU General
 * Public License along with this program; if not, write to the
 * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
 * Boston, MA  02110-1301  USA
 */

/**
 * The <code>useraction</code> event is emitted when a widget gets modified by user interaction.
 * The event is emitted for the option <code>value</code>.
 *
 * @event Slider#useraction
 *
 * @param {string} name - The name of the option which was changed due to the users action
 * @param {mixed} value - The new value of the option
 */
import { Widget } from './widget.js';
import { DragValue } from '../modules/dragvalue.js';
import { ScrollValue } from '../modules/scrollvalue.js';
import { warning } from '../utils/warning.js';
import { element, addClass, outerWidth, outerHeight } from '../utils/dom.js';
import {
  rangedOptionsDefaults,
  rangedOptionsTypes,
  makeRanged,
} from '../utils/make_ranged.js';
import { focusMoveDefault, announceFocusMoveKeys } from '../utils/keyboard.js';
import { warn } from '../utils/log.js';

function dblClick() {
  this.userset('value', this.options.reset);
  /**
   * Is fired when the slider receives a double click in order to reset to initial value.
   *
   * @event Slider#doubleclick
   *
   * @param {number} value - The value of the widget.
   */
  this.emit('doubleclick', this.options.value);
}

function setBackground(horiz, vert, size) {
  const E = this.element;
  E.style['background-position'] = '-' + horiz + 'px -' + vert + 'px';

  E.style['-webkit-background-size'] = size;
  E.style['-moz-background-size'] = size;
  E.style['-ms-background-size'] = size;
  E.style['-o-background-size'] = size;
  E.style['background-size'] = size;
}
/**
 * Slider is a {@link Widget} moving its background image
 * according to its value. It can be used to show strips of
 * e.g. 3D-rendered faders or knobs. It's important to set the
 * width and height of the widget in CSS according to the frames in
 * the background file. If alignment is `horizontal` the background image
 * is as height as the widget, the width keeps the ratio intact. Overall
 * width of the image should be frames * width. If alignment is `vertical`
 * the background image is as wide as the widget and the height of the
 * image keeps the ratio intact. The height should be height of widget
 * times the number of frames.
 * Slider uses {@link DragValue} and {@link ScrollValue}
 * for setting its value.
 * It inherits all options of {@link DragValue} and {@link Ranged}.
 *
 * @class Slider
 *
 * @extends Widget
 *
 * @param {Object} [options={ }] - An object containing initial options.
 *
 * @property {Number} [options.value=0] - The current value.
 * @property {Integer} [options.frames=1] - The amount of frames contained
 *     in the background image.
 * @property {String} [options.alignment="horizontal"] - The direction
 *     of the frames in the image, next to (`horizontal`) or among each other (`vertical`).
 * @property {String|Booelan} [options.image=false] - The image containing all frames for the slider.
 *     Set to `false` to set the background image via external CSS.
 *
 */
export class Slider extends Widget {
  static get _options() {
    return Object.assign(
      {},
      Widget.getOptionTypes(),
      rangedOptionsTypes,
      DragValue.getOptionTypes(),
      {
        value: 'number',
        frames: 'int',
        alignment: 'string',
        image: 'string|boolean',
        _width: 'number',
        _height: 'number',
      }
    );
  }

  static get options() {
    return Object.assign({}, rangedOptionsDefaults, {
      value: 0,
      frames: 1,
      alignment: 'horizontal',
      image: false,

      direction: 'polar',
      rotation: 45,
      blind_angle: 20,
      basis: 300,
      role: 'slider',
      tabindex: 0,
    });
  }

  static get static_events() {
    return {
      dblclick: dblClick,
      focus_move: focusMoveDefault(),
    };
  }

  initialize(options) {
    if (!options.element) options.element = element('div');
    super.initialize(options);
    options = this.options;

    const E = this.element;

    /**
     * @member {HTMLDivElement} Slider#element - The main DIV container.
     *   Has class <code>.aux-slider</code>.
     */
    /**
     * @member {DragValue} Knob#drag - Instance of {@link DragValue} used for
     *   interaction.
     */
    this.drag = new DragValue(this, {
      node: E,
      classes: E,
      direction: this.options.direction,
      rotation: this.options.rotation,
      blind_angle: this.options.blind_angle,
    });
    /**
     * @member {ScrollValue} Knob#scroll - Instance of {@link ScrollValue} used for
     *   interaction.
     */
    this.scroll = new ScrollValue(this, {
      node: E,
      classes: E,
    });

    if (options.reset === void 0) options.reset = options.value;
  }

  destroy() {
    this.drag.destroy();
    this.scroll.destroy();
    super.destroy();
  }

  draw(O, element) {
    addClass(element, 'aux-slider');
    announceFocusMoveKeys.call(this);

    super.draw(O, element);
  }

  redraw() {
    const I = this.invalid;
    const O = this.options;

    if (I.image) {
      I.image = false;
      if (O.image)
        this.element.style['background-image'] = "url('" + O.image + "')";
      else this.element.style['background-image'] = void 0;
      I.value = true;
    }

    if (I.value || I.alignment || O.frames) {
      I.value = false;
      I.alignment = false;
      I.frames = false;
      const transformation = O.transformation;
      const coef = transformation.valueToCoef(O.value);
      const frame = Math.round(Math.max(0, O.frames - 1) * coef);
      switch (O.alignment) {
        default:
          warn(
            "Unknown alignment, only 'vertical' and 'horizontal' are allowed"
          );
          break;
        case 'vertical':
          setBackground.call(this, 0, frame * O._width, '100% auto');
          break;
        case 'horizontal':
          setBackground.call(this, frame * O._height, 0, 'auto 100%');
          break;
      }
    }

    super.redraw();
  }

  resize() {
    this.set('_width', outerWidth(this.element));
    this.set('_height', outerHeight(this.element));
  }

  set(key, value) {
    switch (key) {
      case 'value':
        if (value > this.options.max || value < this.options.min)
          warning(this.element);
        value = this.get('snap_module').snap(value);
        break;
    }
    if (DragValue.hasOption(key)) this.drag.set(key, value);
    return super.set(key, value);
  }
}
makeRanged(Slider);