widgets/valuebutton.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 { defineChildWidget } from '../child_widget.js';
import { Button } from './button.js';
import { Value } from './value.js';
import { warning } from '../utils/warning.js';
import {
  rangedEvents,
  rangedOptionsDefaults,
  rangedOptionsTypes,
  rangedRenderers,
} from '../utils/ranged.js';
import { DragValue } from '../modules/dragvalue.js';
import { Scale } from './scale.js';
import { ScrollValue } from '../modules/scrollvalue.js';
import { addClass, createID, applyAttribute } from '../utils/dom.js';
import { FORMAT } from '../utils/sprintf.js';
import { focusMoveDefault, announceFocusMoveKeys } from '../utils/keyboard.js';
import { defineRender } from '../renderer.js';
import { selectAriaAttribute } from '../utils/select_aria_attribute.js';
import { mergeStaticEvents } from '../widget_helpers.js';

/**
 * 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 ValueButton#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
 */
/**
 * This widget combines a {@link Button}, a {@link Scale} and a {@link Value}.
 * ValueButton uses {@link DragValue} and {@link ScrollValue}
 * for setting its value.
 * It inherits all options of {@link DragValue} and {@link Scale}.
 *
 * @class ValueButton
 *
 * @extends Button
 *
 * @param {Object} [options={ }] - An object containing initial options.
 *
 * @property {Number} [options.value=0] - The value of the widget.
 * @property {String} [options.direction="polar"] - Direction for changing the value.
 *   Can be "polar", "vertical" or "horizontal". See {@link DragValue} for more details.
 * @property {Number} [options.blind_angle=20] - If `options.direction` is "polar",
 *   this is the angle of separation between positive and negative value changes.  See {@link DragValue} for more details.
 * @property {Number} [options.rotation=45] - Defines the angle of the center of the positive value
 *   changes. 0 means straight upward. For instance, a value of 45 leads to increasing value when
 *   moving towards top and right. See {@link DragValue} for more details.
 * @property {Number} [options.snap=0.01] - Snap value while dragging.
 * @property {Number} [options.basis=300] - Distance to drag between <code>min</code> and <code>max</code> in pixels.
 */
export class ValueButton extends Button {
  static get _options() {
    return [
      rangedOptionsTypes,
      {
        value: 'number',
        direction: 'string',
        rotation: 'number',
        blind_angle: 'number',
        snap: 'number',
        reset: 'number',
      },
    ];
  }

  static get options() {
    return [
      rangedOptionsDefaults,
      {
        value: 0,
        direction: 'polar',
        rotation: 45,
        blind_angle: 20,
        snap: 0.01,
        basis: 300,
        labels: FORMAT('%d'),
        layout: 'top',
        role: 'slider',
        set_ariavalue: true,
      },
    ];
  }

  static get static_events() {
    return mergeStaticEvents(rangedEvents, {
      set_direction: function (value) {
        this.drag.set('direction', value);
      },
      set_drag_rotation: function (value) {
        this.drag.set('rotation', value);
      },
      set_blind_angle: function (value) {
        this.drag.set('blind_angle', value);
      },
      focus_move: focusMoveDefault(),
      dblclick: function (e) {
        this.userset('value', this.options.reset);
        /**
         * Is fired when the user doubleclicks the valuebutton in order to to reset to initial value.
         * The Argument is the new value.
         *
         * @event ValueButton#doubleclick
         *
         * @param {number} value - The value of the widget.
         */
        this.emit('doubleclick', this.options.value);
      },
    });
  }

  static get renderers() {
    return [
      ...rangedRenderers,
      defineRender(
        ['label', 'aria_labelledby', 'value.aria_labelledby'],
        function (label, aria_labelledby, value_aria_labelledby) {
          if (value_aria_labelledby !== void 0) return;

          const { _input } = this.value;

          const value = selectAriaAttribute(
            aria_labelledby,
            label !== false ? this.label.get('id') : null
          );

          applyAttribute(_input, 'aria-labelledby', value);
        }
      ),
    ];
  }

  initialize(options) {
    super.initialize(options);

    /**
     * @member {HTMLDivElement} ValueButton#element - The main DIV container.
     *   Has class <code>.aux-valuebutton</code>.
     */

    /**
     * @member {DragValue} ValueButton#drag - The {@link DragValue} module.
     */
    this.drag = new DragValue(this, {
      node: this.element,
      direction: this.options.direction,
      rotation: this.options.rotation,
      blind_angle: this.options.blind_angle,
      limit: true,
    });
    this.drag.on('startdrag', () => this.startInteracting());
    this.drag.on('stopdrag', () => this.stopInteracting());
    /**
     * @member {ScrollValue} ValueButton#scroll - The {@link ScrollValue} module.
     */
    this.scroll = new ScrollValue(this, {
      node: this.element,
      limit: true,
    });
    this.scroll.on('scrollstarted', () => this.startInteracting());
    this.scroll.on('scrollended', () => this.stopInteracting());

    if (this.options.reset === void 0) this.options.reset = this.options.value;
    this._labelID = createID('aux-label-');
  }

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

    super.draw(O, element);
  }

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

  // GETTERS & SETTERS
  set(key, value) {
    switch (key) {
      case 'value':
        if (value > this.options.max || value < this.options.min)
          warning(this.element);
        value = this.options.snap_module.snap(value);
        break;
    }
    return super.set(key, value);
  }
}

function valueClicked() {
  const self = this.parent;
  self.scroll.set('active', false);
  self.drag.set('active', false);
  /**
   * Is fired when the user starts editing the value manually
   *
   * @event ValueButton#valueedit
   *
   * @param {number} value - The value of the widget.
   */
  self.emit('valueedit', self.options.value);
}
function valueDone() {
  const self = this.parent;
  self.scroll.set('active', true);
  self.drag.set('active', true);
  /**
   * Is fired when the user finished editing the value manually
   *
   * @event ValueButton#valueset
   *
   * @param {number} value - The value of the widget.
   */
  self.emit('valueset', self.options.value);
}
/**
 * @member {Value} ValueButton#value - The value widget for editing the value manually.
 */
defineChildWidget(ValueButton, 'value', {
  create: Value,
  show: true,
  map_options: {
    value: 'value',
  },
  default_options: {
    format: function (val) {
      return val.toFixed(2);
    },
    size: 5,
  },
  userset_delegate: true,
  static_events: {
    dblclick: function (e) {
      e.stopPropagation();
    },
    valueclicked: valueClicked,
    valuedone: valueDone,
  },
});

/**
 * @member {Scale} ValueButton#scale - The {@link Scale} showing the value.
 */
defineChildWidget(ValueButton, 'scale', {
  create: Scale,
  show: true,
  toggle_class: true,
  inherit_options: true,
  map_options: {
    value: 'bar',
  },
  blacklist_options: ['layout', 'set_ariavalue'],
  default_options: {
    layout: 'top',
  },
});