modules/scrollvalue.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
 */

import { addClass, removeClass } from '../utils/dom.js';
import { createTimer, startTimer, destroyTimer } from '../utils/timers.js';
import { Module } from './module.js';
import { clamp } from '../utils/transformations.js';

function getClassesTarget(options) {
  return options.classes || options.node;
}

function scrollWheel(e) {
  const O = this.options;
  if (!O.active) return;
  e.preventDefault();
  if (O.focus) O.focus.focus();

  if (!this._isScrolling) {
    this._isScrolling = true;
    this._value = O.get.call(this);
    addClass(getClassesTarget(O), 'aux-scrolling');
    /**
     * Is fired when scrolling starts.
     *
     * @event ScrollValue#scrollstarted
     *
     * @param {DOMEvent} event - The native DOM event.
     */
    fireEvent.call(this, 'scrollstarted', e);
  }
  this._scrollTimer = startTimer(this._scrollTimer, 200);

  const range = O.range.call(this);
  const { scroll_direction } = O;
  const d =
    e.deltaX * scroll_direction[0] +
    e.deltaY * scroll_direction[1] +
    e.deltaZ * scroll_direction[2];
  const direction =
    (d > 0 ? 1 : -1) * (e.webkitDirectionInvertedFromDevice ? -1 : 1);

  const { step, shift_down, shift_up, transformation } = range.options;

  // calc step depending on options.step, .shift up and .shift down
  let distance = (step || 1) * direction;
  if (e.ctrlKey || e.altKey) {
    distance *= shift_down;
  } else if (e.shiftKey) {
    distance *= shift_up;
  }

  let value = transformation.pixelToValue(
    transformation.valueToPixel(this._value) + distance
  );

  if (O.limit) value = transformation.clampValue(value);

  O.set.call(this, value);

  /**
   * Is fired while scrolling happens.
   *
   * @event ScrollValue#scrolling
   *
   * @param {DOMEvent} event - The native DOM event.
   */
  fireEvent.call(this, 'scrolling', e);

  this._value = value;

  return false;
}
function fireEvent(title, event) {
  const O = this.options;
  // fire an event on this drag object and one with more
  // information on the draggified element
  this.emit(title, this, event);
  const e = O.events.call(this);
  if (e)
    e.emit(title, event, O.get.call(this), O.node, this, O.range.call(this));
}
/**
 * ScrollValue enables the scroll wheel for setting a value of an
 * object. For instance, it is used by {@link Knob} to allow turning
 * the knob using the scroll wheel.
 *
 * @class ScrollValue
 *
 * @extends Module
 *
 * @param {Object} [options={ }] - An object containing initial options.
 *
 * @property {HTMLElement} options.node - The element receiving the scroll event.
 * @property {Function} [options.get=function () { return this.parent.options.value; }] - Callback returning the value.
 * @property {Function} [options.set=function (v) { return this.parent.userset("value", v); }] - Callback setting the value.
 * @property {Function} [options.range=function () { return this.parent; }] - A function returning a {@link Range} instance or options for a new one.
 * @property {Function|Boolean} [options.events=false] - A function returning
 *   an element receiving events or <code>false</code> to fire events on the main element.
 * @property {HTMLElement|Boolean} [options.classes=false] - Element receiving
 *   classes or <code>false</code> to set classes on the main element.
 * @property {Boolean} [options.active=true] - Disable the scroll event.
 * @property {Array<Number>} [options.scroll_direction=[0, -1, 0]] - An array
 *   containing values for x, y and z defining the direction of scrolling.
 * @property {Boolean} [options.limit=false] - Clamp the returned value using clampValue of the tranformation.
 * @property {HTMLElement|Boolean} [options.focus=false] - Focus this element on scroll. Set to `false`
 *   if no focus should be set.
 */
export class ScrollValue extends Module {
  static get _options() {
    return {
      get: 'function',
      set: 'function',
      range: 'function',
      events: 'function',
      classes: 'object|boolean',
      node: 'object|boolean',
      active: 'boolean',
      scroll_direction: 'array',
      limit: 'boolean',
      focus: 'object|boolean',
    };
  }

  static get options() {
    return {
      range: function () {
        return this.parent;
      },
      events: function () {
        return this.parent;
      },
      classes: false,
      get: function () {
        return this.parent.options.value;
      },
      set: function (v) {
        return this.parent.userset('value', v);
      },
      active: true,
      scroll_direction: [0, -1, 0],
      limit: false,
      focus: false,
    };
  }

  initialize(widget, options) {
    super.initialize(widget, options);
    this._scrollTimer = createTimer(() => {
      /**
       * Is fired when scrolling ended.
       *
       * @event ScrollValue#scrollended
       */
      fireEvent.call(this, 'scrollended');
      this._isScrolling = false;
      this.set('scrolling', false);
      removeClass(getClassesTarget(this.options), 'aux-scrolling');
    });
    this._isScrolling = false;
    this._value = 0.0;
    this.set('node', this.options.node);
  }

  destroy() {
    super.destroy();
    this._scrollTimer = destroyTimer(this._scrollTimer);
  }

  static get static_events() {
    return {
      set_node: function (value) {
        this.delegateEvents(value);
      },
      wheel: scrollWheel,
    };
  }
}