/*
* 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 Knob#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 { Circular } from './circular.js';
import { DragValue } from '../modules/dragvalue.js';
import { ScrollValue } from '../modules/scrollvalue.js';
import { element, addClass, innerWidth, innerHeight } from '../utils/dom.js';
import { makeSVG } from '../utils/svg.js';
import { FORMAT } from '../utils/sprintf.js';
import { focusMoveDefault, announceFocusMoveKeys } from '../utils/keyboard.js';
import { objectAnd, objectSub } from '../utils/object.js';
import { defineRender } from '../renderer.js';
import { applyLegacyUsersetEventsRanged } from '../utils/legacy_userset_events.js';
const formatViewbox = FORMAT('0 0 %d %d');
function dblClick() {
if (!this.get('bind_dblclick')) return;
this.userset('value', this.options.reset);
/**
* Is fired when the knob receives a double click in order to reset to initial value.
*
* @event Knob#doubleclick
*
* @param {number} value - The value of the widget.
*/
this.emit('doubleclick', this.options.value);
}
function moduleRange() {
return this.parent.circular;
}
class KnobCircular extends Circular {
static get options() {
return {
hand: { width: 1, length: 10, margin: 25 },
margin: 13,
thickness: 6,
dots_defaults: { length: 6, margin: 13.5, width: 1, foo: 'hello' },
markers_defaults: { thickness: 2, margin: 11 },
labels_defaults: {
margin: 12,
align: 'outer',
format: function (val) {
return val;
},
},
direction: 'polar',
rotation: 45,
};
}
}
/**
* Knob is a {@link Circular} inside of an SVG which can be
* modified both by dragging and scrolling utilizing {@link DragValue}
* and {@link ScrollValue}.
* It inherits all options of {@link Circular} and {@link DragValue}.
* The options listed below consist of options from the contained widgets,
* only showing the default values.
*
* @class Knob
*
* @extends Widget
*
* @param {Object} [options={ }] - An object containing initial options.
*
* @property {Number} [options.reset] - Reset to this value on double click.
* @property {boolean} [options.bind_dblclick=true] - If true, bind the dblclick
* event to reset the value to the `reset` option.
* @property {Object} [options.hand={width: 1, length: 12, margin: 24}]
* @property {Number} [options.margin=13]
* @property {Number} [options.thickness=6]
* @property {Number} [options.step=1]
* @property {Number} [options.shift_up=4]
* @property {Number} [options.shift_down=0.25]
* @property {Object} [options.dots_defaults={length: 6, margin: 13, width: 2}]
* @property {Object} [options.markers_defaults={thickness: 6, margin: 13}]
* @property {Object} [options.labels_defaults={margin: 10, align: "outer", format: function(val){return val;}}]
* @property {Number} [options.basis=300] - Distance to drag between <code>min</code> and <code>max</code>.
* @property {String} [options.preset="medium"] - The preset to use. Presets
* are a functionality of {@link Widget}.
* @property {Object} [options.presets={
tiny: {margin:0, thickness:4, hand:{width: 1, length: 6, margin: 8}, dots_defaults:{length:4, margin:0, width:1}, markers_defaults: {thickness: 2, margin: 0}, show_labels:false},
small: {margin:0, thickness:5, hand:{width: 1, length: 8, margin: 10}, dots_defaults: {length:5, margin:0,width:1}, markers_defaults: {thickness: 2, margin: 0}, show_labels:false},
medium: {},
large: {hand:{width:1.5, length:12, margin:26}},
huge: {hand:{width:2, length:12, margin:28}},
}] - A set of available presets. Presets
* are a functionality of {@link Widget}.
*/
export class Knob extends Widget {
static get _options() {
return [
Widget.getOptionTypes(),
KnobCircular.getOptionTypes(),
DragValue.getOptionTypes(),
{
reset: 'number',
bind_dblclick: 'boolean',
},
];
}
static get options() {
return [
KnobCircular.getDefaultOptions(),
{
step: 1,
basis: 300,
blind_angle: 20,
shift_up: 4,
shift_down: 0.25,
preset: 'medium',
presets: {
tiny: {
margin: 0,
thickness: 4,
hand: { width: 1, length: 6, margin: 8 },
dots_defaults: { length: 4, margin: 0.5, width: 1 },
markers_defaults: { thickness: 2, margin: 0 },
show_labels: false,
},
small: {
margin: 8,
thickness: 4.5,
hand: { width: 1, length: 8, margin: 17 },
dots_defaults: { length: 4.5, margin: 8.5, width: 1 },
markers_defaults: { thickness: 2, margin: 8 },
labels_defaults: { margin: 9 },
show_labels: true,
},
medium: {
margin: 13,
thickness: 6,
hand: { width: 1, length: 10, margin: 25 },
dots_defaults: { length: 6, margin: 13.5, width: 1 },
markers_defaults: { thickness: 2, margin: 11 },
show_labels: true,
},
large: {
margin: 13,
thickness: 6,
hand: { width: 1.5, length: 12, margin: 26 },
dots_defaults: { length: 6, margin: 13.5, width: 1 },
markers_defaults: { thickness: 2, margin: 11 },
show_labels: true,
},
huge: {
margin: 13,
thickness: 6,
hand: { width: 2, length: 12, margin: 28 },
dots_defaults: { length: 6, margin: 13.5, width: 1 },
markers_defaults: { thickness: 2, margin: 11 },
show_labels: true,
},
},
bind_dblclick: true,
tabindex: 0,
role: 'slider',
set_ariavalue: true,
},
];
}
static get static_events() {
return {
dblclick: dblClick,
focus_move: focusMoveDefault(),
};
}
static get renderers() {
return [
defineRender('size', function (size) {
this.svg.setAttribute('viewBox', formatViewbox(size, size));
}),
defineRender('preset', function (preset) {
const { element, _lastPreset } = this;
if (_lastPreset !== null) {
removeClass(element, 'aux-preset-' + _lastPreset);
}
if (preset) addClass(element, 'aux-preset-' + preset);
this._lastPreset = preset || null;
}),
];
}
initialize(options) {
if (!options.element) options.element = element('div');
super.initialize(options);
options = this.options;
let S;
/**
* @member {HTMLDivElement} Knob#element - The main DIV container.
* Has class <code>.aux-knob</code>.
*/
/**
* @member {SVGImage} Knob#svg - The main SVG image.
*/
this.svg = S = makeSVG('svg');
let co = objectAnd(options, KnobCircular.getOptionTypes());
co = objectSub(co, Widget.getOptionTypes());
co.container = S;
co.aria_targets = [this.svg];
/**
* @member {Circular} Knob#circular - The {@link Circular} module.
*/
this.circular = new KnobCircular(co);
/**
* @member {DragValue} Knob#drag - Instance of {@link DragValue} used for the
* interaction.
*/
this.drag = new DragValue(this, {
node: S,
classes: this.element,
range: moduleRange,
direction: options.direction,
rotation: options.rotation,
blind_angle: options.blind_angle,
limit: true,
focus: S,
});
this.drag.on('startdrag', () => this.startInteracting());
this.drag.on('stopdrag', () => this.stopInteracting());
/**
* @member {ScrollValue} Knob#scroll - Instance of {@link ScrollValue} used for the
* interaction.
*/
this.scroll = new ScrollValue(this, {
node: S,
classes: this.element,
range: moduleRange,
limit: true,
focus: S,
});
this.scroll.on('scrollstarted', () => this.startInteracting());
this.scroll.on('scrollended', () => this.stopInteracting());
this.set('base', options.base);
if (options.reset === void 0) options.reset = options.value;
this.addChild(this.circular);
this._lastPreset = null;
}
getFocusTargets() {
return [this.svg];
}
getRoleTarget() {
return this.svg;
}
getRange() {
return this.circular;
}
draw(O, element) {
addClass(element, 'aux-knob');
element.appendChild(this.svg);
announceFocusMoveKeys.call(this);
super.draw(O, element);
}
destroy() {
this.drag.destroy();
this.scroll.destroy();
this.circular.destroyAndRemove();
this.svg.remove();
super.destroy();
}
getResizeTargets() {
return [this.element];
}
resize() {
super.resize();
const width = innerWidth(this.element, undefined, true);
const height = innerHeight(this.element, undefined, true);
const size = Math.min(width, height);
this.set('size', size);
}
/**
* This is an alias for {@link Circular#addLabel} of the internal
* circular instance.
*
* @method Knob#addLabel
*/
addLabel(x) {
return this.circular.addLabel(x);
}
/**
* This is an alias for {@link Circular#removeLabel} of the internal
* circular instance.
*
* @method Knob#removeLabel
*/
removeLabel(x) {
this.circular.removeLabel(x);
}
userset(key, value) {
if (key === 'value') {
const { transformation, snap_module } = this.circular.options;
return applyLegacyUsersetEventsRanged(
this,
transformation,
snap_module,
key,
value
);
} else {
return super.userset(key, value);
}
}
set(key, value) {
if (key === 'base') {
if (value === false) value = this.options.min;
}
if (!Widget.getOptionTypes()[key]) {
if (KnobCircular.getOptionTypes()[key])
value = this.circular.set(key, value);
if (DragValue.getOptionTypes()[key]) this.drag.set(key, value);
}
return super.set(key, value);
}
}