572 lines
18 KiB
JavaScript
572 lines
18 KiB
JavaScript
|
/*
|
||
|
* This file is part of Toolkit.
|
||
|
*
|
||
|
* Toolkit 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.
|
||
|
*
|
||
|
* Toolkit 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
|
||
|
* Lesser 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
|
||
|
*/
|
||
|
"use strict";
|
||
|
(function(w, TK) {
|
||
|
function get_base(O) {
|
||
|
return Math.max(Math.min(O.max, O.base), O.min);
|
||
|
}
|
||
|
function vert(O) {
|
||
|
return O.layout === "left" || O.layout === "right";
|
||
|
}
|
||
|
function fill_interval(range, levels, i, from, to, min_gap, result) {
|
||
|
var level = levels[i];
|
||
|
var x, j, pos, last_pos, last;
|
||
|
var diff;
|
||
|
|
||
|
var to_pos = range.val2px(to);
|
||
|
last_pos = range.val2px(from);
|
||
|
|
||
|
if (Math.abs(to_pos - last_pos) < min_gap) return;
|
||
|
|
||
|
if (!result) result = {
|
||
|
values: [],
|
||
|
positions: [],
|
||
|
};
|
||
|
|
||
|
var values = result.values;
|
||
|
var positions = result.positions;
|
||
|
|
||
|
if (from > to) level = -level;
|
||
|
last = from;
|
||
|
|
||
|
for (j = ((to-from)/level)|0, x = from + level; j > 0; x += level, j--) {
|
||
|
pos = range.val2px(x);
|
||
|
diff = Math.abs(last_pos - pos);
|
||
|
if (Math.abs(to_pos - pos) < min_gap) break;
|
||
|
if (diff >= min_gap) {
|
||
|
if (i > 0 && diff >= min_gap * 2) {
|
||
|
// we have a chance to fit some more labels in
|
||
|
fill_interval(range, levels, i-1,
|
||
|
last, x, min_gap, result);
|
||
|
}
|
||
|
values.push(x);
|
||
|
positions.push(pos);
|
||
|
last_pos = pos;
|
||
|
last = x;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (i > 0 && Math.abs(last_pos - to_pos) >= min_gap * 2) {
|
||
|
fill_interval(range, levels, i-1, last, to, min_gap, result);
|
||
|
}
|
||
|
|
||
|
return result;
|
||
|
}
|
||
|
// remove collisions from a with b given a minimum gap
|
||
|
function remove_collisions(a, b, min_gap, vert) {
|
||
|
var pa = a.positions, pb = b.positions;
|
||
|
var va = a.values;
|
||
|
var dim;
|
||
|
|
||
|
min_gap = +min_gap;
|
||
|
|
||
|
if (typeof vert === "boolean")
|
||
|
dim = vert ? b.height : b.width;
|
||
|
|
||
|
if (!(min_gap > 0)) min_gap = 1;
|
||
|
|
||
|
if (!pb.length) return a;
|
||
|
|
||
|
var i, j;
|
||
|
var values = [];
|
||
|
var positions = [];
|
||
|
var pos_a, pos_b;
|
||
|
var size;
|
||
|
|
||
|
var last_pos = +pb[0],
|
||
|
last_size = min_gap;
|
||
|
|
||
|
if (dim) last_size += +dim[0] / 2;
|
||
|
|
||
|
// If pb is just length 1, it does not matter
|
||
|
var direction = pb.length > 1 && pb[1] < last_pos ? -1 : 1;
|
||
|
|
||
|
for (i = 0, j = 0; i < pa.length && j < pb.length;) {
|
||
|
pos_a = +pa[i];
|
||
|
pos_b = +pb[j];
|
||
|
size = min_gap;
|
||
|
|
||
|
if (dim) size += dim[j] / 2;
|
||
|
|
||
|
if (Math.abs(pos_a - last_pos) < last_size ||
|
||
|
Math.abs(pos_a - pos_b) < size) {
|
||
|
// try next position
|
||
|
i++;
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
if (j < pb.length - 1 && (pos_a - pos_b)*direction > 0) {
|
||
|
// we left the current interval, lets try the next one
|
||
|
last_pos = pos_b;
|
||
|
last_size = size;
|
||
|
j++;
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
values.push(+va[i]);
|
||
|
positions.push(pos_a);
|
||
|
|
||
|
i++;
|
||
|
}
|
||
|
|
||
|
return {
|
||
|
values: values,
|
||
|
positions: positions,
|
||
|
};
|
||
|
}
|
||
|
function create_dom_nodes(data, create) {
|
||
|
var nodes = [];
|
||
|
var values, positions;
|
||
|
var i;
|
||
|
var E = this.element;
|
||
|
var node;
|
||
|
|
||
|
data.nodes = nodes;
|
||
|
values = data.values;
|
||
|
positions = data.positions;
|
||
|
|
||
|
for (i = 0; i < values.length; i++) {
|
||
|
nodes.push(node = create(values[i], positions[i]));
|
||
|
E.appendChild(node);
|
||
|
}
|
||
|
}
|
||
|
function create_label(value, position) {
|
||
|
var O = this.options;
|
||
|
var elem = document.createElement("SPAN");
|
||
|
elem.className = "toolkit-label";
|
||
|
|
||
|
if (vert(O)) {
|
||
|
elem.style.bottom = position.toFixed(1) + "px";
|
||
|
} else {
|
||
|
elem.style.left = position.toFixed(1) + "px";
|
||
|
}
|
||
|
|
||
|
TK.set_content(elem, O.labels(value));
|
||
|
|
||
|
if (get_base(O) === value)
|
||
|
TK.add_class(elem, "toolkit-base");
|
||
|
if (O.max === value)
|
||
|
TK.add_class(elem, "toolkit-max");
|
||
|
if (O.min === value)
|
||
|
TK.add_class(elem, "toolkit-min");
|
||
|
|
||
|
return elem;
|
||
|
}
|
||
|
function create_dot(value, position) {
|
||
|
var O = this.options;
|
||
|
var elem = document.createElement("DIV");
|
||
|
elem.className = "toolkit-dot";
|
||
|
|
||
|
if (O.layout === "left" || O.layout === "right") {
|
||
|
elem.style.bottom = Math.round(position + 0.5) + "px";
|
||
|
} else {
|
||
|
elem.style.left = Math.round(position - 0.5) + "px";
|
||
|
}
|
||
|
|
||
|
if (get_base(O) === value)
|
||
|
TK.add_class(elem, "toolkit-base");
|
||
|
else if (O.max === value)
|
||
|
TK.add_class(elem, "toolkit-max");
|
||
|
else if (O.min === value)
|
||
|
TK.add_class(elem, "toolkit-min");
|
||
|
|
||
|
return elem;
|
||
|
}
|
||
|
function measure_dimensions(data) {
|
||
|
var nodes = data.nodes;
|
||
|
var width = [];
|
||
|
var height = [];
|
||
|
|
||
|
for (var i = 0; i < nodes.length; i++) {
|
||
|
width.push(TK.outer_width(nodes[i]));
|
||
|
height.push(TK.outer_height(nodes[i]));
|
||
|
}
|
||
|
|
||
|
data.width = width;
|
||
|
data.height = height;
|
||
|
}
|
||
|
function handle_end(O, labels, i) {
|
||
|
var node = labels.nodes[i];
|
||
|
var v = labels.values[i];
|
||
|
|
||
|
if (v === O.min) {
|
||
|
TK.add_class(node, "toolkit-min");
|
||
|
} else if (v === O.max) {
|
||
|
TK.add_class(node, "toolkit-max");
|
||
|
} else return;
|
||
|
}
|
||
|
function generate_scale(from, to, include_from, show_to) {
|
||
|
var O = this.options;
|
||
|
var labels;
|
||
|
|
||
|
if (O.show_labels || O.show_markers)
|
||
|
labels = {
|
||
|
values: [],
|
||
|
positions: [],
|
||
|
};
|
||
|
|
||
|
var dots = {
|
||
|
values: [],
|
||
|
positions: [],
|
||
|
};
|
||
|
var is_vert = vert(O);
|
||
|
var tmp;
|
||
|
|
||
|
if (include_from) {
|
||
|
tmp = this.val2px(from);
|
||
|
|
||
|
if (labels) {
|
||
|
labels.values.push(from);
|
||
|
labels.positions.push(tmp);
|
||
|
}
|
||
|
|
||
|
dots.values.push(from);
|
||
|
dots.positions.push(tmp);
|
||
|
}
|
||
|
|
||
|
var levels = O.levels;
|
||
|
|
||
|
fill_interval(this, levels, levels.length - 1, from, to, O.gap_dots, dots);
|
||
|
|
||
|
if (labels) {
|
||
|
if (O.levels_labels) levels = O.levels_labels;
|
||
|
|
||
|
fill_interval(this, levels, levels.length - 1, from, to, O.gap_labels, labels);
|
||
|
|
||
|
tmp = this.val2px(to);
|
||
|
|
||
|
if (show_to || Math.abs(tmp - this.val2px(from)) >= O.gap_labels) {
|
||
|
labels.values.push(to);
|
||
|
labels.positions.push(tmp);
|
||
|
|
||
|
dots.values.push(to);
|
||
|
dots.positions.push(tmp);
|
||
|
}
|
||
|
} else {
|
||
|
dots.values.push(to);
|
||
|
dots.positions.push(this.val2px(to));
|
||
|
}
|
||
|
|
||
|
if (O.show_labels) {
|
||
|
create_dom_nodes.call(this, labels, create_label.bind(this));
|
||
|
|
||
|
if (labels.values.length && labels.values[0] === get_base(O)) {
|
||
|
TK.add_class(labels.nodes[0], "toolkit-base");
|
||
|
}
|
||
|
}
|
||
|
|
||
|
var render_cb = function() {
|
||
|
var markers;
|
||
|
|
||
|
if (O.show_markers) {
|
||
|
markers = {
|
||
|
values: labels.values,
|
||
|
positions: labels.positions,
|
||
|
};
|
||
|
create_dom_nodes.call(this, markers, create_dot.bind(this));
|
||
|
for (var i = 0; i < markers.nodes.length; i++)
|
||
|
TK.add_class(markers.nodes[i], "toolkit-marker");
|
||
|
}
|
||
|
|
||
|
if (O.show_labels && labels.values.length > 1) {
|
||
|
handle_end(O, labels, 0);
|
||
|
handle_end(O, labels, labels.nodes.length-1);
|
||
|
}
|
||
|
|
||
|
if (O.avoid_collisions && O.show_labels) {
|
||
|
dots = remove_collisions(dots, labels, O.gap_dots, is_vert);
|
||
|
} else if (markers) {
|
||
|
dots = remove_collisions(dots, markers, O.gap_dots);
|
||
|
}
|
||
|
|
||
|
create_dom_nodes.call(this, dots, create_dot.bind(this));
|
||
|
};
|
||
|
|
||
|
if (O.show_labels && O.avoid_collisions)
|
||
|
TK.S.add(function() {
|
||
|
measure_dimensions(labels);
|
||
|
TK.S.add(render_cb.bind(this), 3);
|
||
|
}.bind(this), 2);
|
||
|
else render_cb.call(this);
|
||
|
}
|
||
|
function mark_markers(labels, dots) {
|
||
|
var i, j;
|
||
|
|
||
|
var a = labels.values;
|
||
|
var b = dots.values;
|
||
|
var nodes = dots.nodes;
|
||
|
|
||
|
for (i = j = 0; i < a.length && j < b.length;) {
|
||
|
if (a[i] < b[j]) i++;
|
||
|
else if (a[i] > b[j]) j++;
|
||
|
else {
|
||
|
TK.add_class(nodes[j], "toolkit-marker");
|
||
|
i++;
|
||
|
j++;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
/**
|
||
|
* TK.Scale can be used to draw scales. It is used in {@link TK.MeterBase} and
|
||
|
* {@link TK.Fader}. TK.Scale draws labels and markers based on its parameters
|
||
|
* and the available space. Scales can be drawn both vertically and horizontally.
|
||
|
* Scale mixes in {@link TK.Ranged} and inherits all its options.
|
||
|
*
|
||
|
* @extends TK.Widget
|
||
|
*
|
||
|
* @mixes TK.Ranged
|
||
|
*
|
||
|
* @class TK.Scale
|
||
|
*
|
||
|
* @param {Object} [options={ }] - An object containing initial options.
|
||
|
*
|
||
|
* @property {String} [options.layout="right"] - The layout of the TK.Scale. <code>right</code> and
|
||
|
* <code>left</code> are vertical layouts with the labels being drawn right and left of the scale,
|
||
|
* respectively. <code>top</code> and <code>bottom</code> are horizontal layouts for which the
|
||
|
* labels are drawn on top and below the scale, respectively.
|
||
|
* @property {Integer} [options.division=1] - Minimal step size of the markers.
|
||
|
* @property {Array<Number>} [options.levels=[1]] - Array of steps for labels and markers.
|
||
|
* @property {Number} [options.base=false]] - Base of the scale. If set to <code>false</code> it will
|
||
|
* default to the minimum value.
|
||
|
* @property {Function} [options.labels=TK.FORMAT("%.2f")] - Formatting function for the labels.
|
||
|
* @property {Integer} [options.gap_dots=4] - Minimum gap in pixels between two adjacent markers.
|
||
|
* @property {Integer} [options.gap_labels=40] - Minimum gap in pixels between two adjacent labels.
|
||
|
* @property {Boolean} [options.show_labels=true] - If <code>true</code>, labels are drawn.
|
||
|
* @property {Boolean} [options.show_max=true] - If <code>true</code>, display a label and a
|
||
|
* dot for the 'max' value.
|
||
|
* @property {Boolean} [options.show_min=true] - If <code>true</code>, display a label and a
|
||
|
* dot for the 'min' value.
|
||
|
* @property {Boolean} [options.show_base=true] - If <code>true</code>, display a label and a
|
||
|
* dot for the 'base' value.
|
||
|
* @property {Array<Number>|Boolean} [options.fixed_dots] - This option can be used to specify fixed positions
|
||
|
* for the markers to be drawn at. The values must be sorted in ascending order. <code>false</code> disables
|
||
|
* fixed markers.
|
||
|
* @property {Array<Number>|Boolean} [options.fixed_labels] - This option can be used to specify fixed positions
|
||
|
* for the labels to be drawn at. The values must be sorted in ascending order. <code>false</code> disables
|
||
|
* fixed labels.
|
||
|
* @property {Boolean} [options.show_markers=true] - If true, every dot which is located at the same
|
||
|
* position as a label has the <code>toolkit-marker</code> class set.
|
||
|
* @property {Number|Boolean} [options.pointer=false] - The value to set the pointers position to. Set to `false` to hide the pointer.
|
||
|
* @property {Number|Boolean} [options.bar=false] - The value to set the bars height to. Set to `false` to hide the bar.
|
||
|
*/
|
||
|
TK.Scale = TK.class({
|
||
|
_class: "Scale",
|
||
|
|
||
|
Extends: TK.Widget,
|
||
|
Implements: [TK.Ranged],
|
||
|
_options: Object.assign(Object.create(TK.Widget.prototype._options), TK.Ranged.prototype._options, {
|
||
|
layout: "string",
|
||
|
division: "number",
|
||
|
levels: "array",
|
||
|
levels_labels: "array",
|
||
|
base: "number",
|
||
|
labels: "function",
|
||
|
gap_dots: "number",
|
||
|
gap_labels: "number",
|
||
|
show_labels: "boolean",
|
||
|
show_min: "boolean",
|
||
|
show_max: "boolean",
|
||
|
show_base: "boolean",
|
||
|
fixed_dots: "boolean|array",
|
||
|
fixed_labels: "boolean|array",
|
||
|
avoid_collisions: "boolean",
|
||
|
show_markers: "boolean",
|
||
|
bar: "boolean|number",
|
||
|
pointer: "boolean|number",
|
||
|
}),
|
||
|
options: {
|
||
|
layout: "right",
|
||
|
division: 1,
|
||
|
levels: [1],
|
||
|
base: false,
|
||
|
labels: TK.FORMAT("%.2f"),
|
||
|
avoid_collisions: false,
|
||
|
gap_dots: 4,
|
||
|
gap_labels: 40,
|
||
|
show_labels: true,
|
||
|
show_min: true,
|
||
|
show_max: true,
|
||
|
show_base: true,
|
||
|
show_markers: true,
|
||
|
fixed_dots: false,
|
||
|
fixed_labels: false,
|
||
|
bar: false,
|
||
|
pointer: false,
|
||
|
},
|
||
|
|
||
|
initialize: function (options) {
|
||
|
var E;
|
||
|
TK.Widget.prototype.initialize.call(this, options);
|
||
|
/**
|
||
|
* @member {HTMLDivElement} TK.Scale#element - The main DIV element. Has class <code>toolkit-scale</code>
|
||
|
*/
|
||
|
if (!(E = this.element)) this.element = E = TK.element("div");
|
||
|
TK.add_class(E, "toolkit-scale");
|
||
|
this.element = this.widgetize(E, true, true, true);
|
||
|
},
|
||
|
|
||
|
redraw: function () {
|
||
|
TK.Widget.prototype.redraw.call(this);
|
||
|
|
||
|
var I = this.invalid;
|
||
|
var O = this.options;
|
||
|
var E = this.element;
|
||
|
|
||
|
if (I.layout) {
|
||
|
I.layout = false;
|
||
|
TK.remove_class(E, "toolkit-vertical", "toolkit-horizontal", "toolkit-top",
|
||
|
"toolkit-bottom", "toolkit-right", "toolkit-left");
|
||
|
switch (O.layout) {
|
||
|
case "left":
|
||
|
TK.add_class(E, "toolkit-vertical", "toolkit-left");
|
||
|
break;
|
||
|
case "right":
|
||
|
TK.add_class(E, "toolkit-vertical", "toolkit-right");
|
||
|
break;
|
||
|
case "top":
|
||
|
TK.add_class(E, "toolkit-horizontal", "toolkit-top");
|
||
|
break;
|
||
|
case "bottom":
|
||
|
TK.add_class(E, "toolkit-horizontal", "toolkit-bottom");
|
||
|
break;
|
||
|
default:
|
||
|
TK.warn("Unsupported layout setting:", O.layout);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (I.reverse) {
|
||
|
/* NOTE: reverse will be validated below */
|
||
|
TK.toggle_class(E, "toolkit-reverse", O.reverse);
|
||
|
}
|
||
|
|
||
|
if (I.validate("base", "show_base", "gap_labels", "min", "show_min", "division", "max", "show_markers",
|
||
|
"fixed_dots", "fixed_labels", "levels", "basis", "scale", "reverse", "show_labels")) {
|
||
|
TK.empty(E);
|
||
|
|
||
|
if (O.fixed_dots && O.fixed_labels) {
|
||
|
var labels;
|
||
|
|
||
|
if (O.show_labels) {
|
||
|
labels = {
|
||
|
values: O.fixed_labels,
|
||
|
positions: O.fixed_labels.map(this.val2px, this),
|
||
|
};
|
||
|
create_dom_nodes.call(this, labels, create_label.bind(this));
|
||
|
}
|
||
|
|
||
|
var dots = {
|
||
|
values: O.fixed_dots,
|
||
|
positions: O.fixed_dots.map(this.val2px, this),
|
||
|
};
|
||
|
create_dom_nodes.call(this, dots, create_dot.bind(this));
|
||
|
|
||
|
if (O.show_markers && labels) {
|
||
|
mark_markers(labels, dots);
|
||
|
}
|
||
|
} else {
|
||
|
var base = get_base(O);
|
||
|
|
||
|
if (base !== O.max) generate_scale.call(this, base, O.max, true, O.show_max);
|
||
|
if (base !== O.min) generate_scale.call(this, base, O.min, base === O.max, O.show_min);
|
||
|
}
|
||
|
if (this._bar)
|
||
|
this.element.appendChild(this._bar);
|
||
|
if (this._pointer)
|
||
|
this.element.appendChild(this._pointer);
|
||
|
}
|
||
|
},
|
||
|
|
||
|
resize: function () {
|
||
|
TK.Widget.prototype.resize.call(this);
|
||
|
var O = this.options;
|
||
|
this.set("basis", vert(O) ? TK.inner_height(this.element)
|
||
|
: TK.inner_width(this.element) );
|
||
|
},
|
||
|
|
||
|
// GETTER & SETTER
|
||
|
set: function (key, value) {
|
||
|
TK.Widget.prototype.set.call(this, key, value);
|
||
|
switch (key) {
|
||
|
case "division":
|
||
|
case "levels":
|
||
|
case "labels":
|
||
|
case "gap_dots":
|
||
|
case "gap_labels":
|
||
|
case "show_labels":
|
||
|
/**
|
||
|
* Gets fired when an option the rendering depends on was changed
|
||
|
*
|
||
|
* @event TK.Scale#scalechanged
|
||
|
*
|
||
|
* @param {string} key - The name of the option which changed the {@link TK.Scale}.
|
||
|
* @param {mixed} value - The value of the option.
|
||
|
*/
|
||
|
this.fire_event("scalechanged", key, value)
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
});
|
||
|
|
||
|
/**
|
||
|
* @member {HTMLDivElement} TK.Fader#_pointer - The DIV element of the pointer. It can be used to e.g. visualize the value set in the backend.
|
||
|
*/
|
||
|
TK.ChildElement(TK.Scale, "pointer", {
|
||
|
show: false,
|
||
|
toggle_class: true,
|
||
|
option: "pointer",
|
||
|
draw_options: Object.keys(TK.Ranged.prototype._options).concat([ "pointer", "basis" ]),
|
||
|
draw: function(O) {
|
||
|
if (this._pointer) {
|
||
|
var tmp = this.val2px(this.snap(O.pointer)) + "px";
|
||
|
if (vert(O)) {
|
||
|
if (TK.supports_transform)
|
||
|
this._pointer.style.transform = "translateY(-"+tmp+")";
|
||
|
else
|
||
|
this._pointer.style.bottom = tmp;
|
||
|
} else {
|
||
|
if (TK.supports_transform)
|
||
|
this._pointer.style.transform = "translateX("+tmp+")";
|
||
|
else
|
||
|
this._pointer.style.left = tmp;
|
||
|
}
|
||
|
}
|
||
|
},
|
||
|
});
|
||
|
|
||
|
/**
|
||
|
* @member {HTMLDivElement} TK.Fader#_bar - The DIV element of the bar. It can be used to e.g. visualize the value set in the backend or to draw a simple levelmeter.
|
||
|
*/
|
||
|
TK.ChildElement(TK.Scale, "bar", {
|
||
|
show: false,
|
||
|
toggle_class: true,
|
||
|
option: "bar",
|
||
|
draw_options: Object.keys(TK.Ranged.prototype._options).concat([ "bar", "basis" ]),
|
||
|
draw: function(O) {
|
||
|
if (this._bar) {
|
||
|
var tmp = this.val2px(this.snap(O.bar)) + "px";
|
||
|
if (vert(O))
|
||
|
this._bar.style.height = tmp;
|
||
|
else
|
||
|
this._bar.style.width = tmp;
|
||
|
}
|
||
|
},
|
||
|
});
|
||
|
|
||
|
})(this, this.TK);
|