/* * 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){ var MODES = [ "circular", "line-horizontal", "line-vertical", "block-top", "block-bottom", "block-left", "block-right", "block", ]; function normalize(v) { var n = Math.sqrt(v[0]*v[0] + v[1]*v[1]); v[0] /= n; v[1] /= n; } function scrollwheel(e) { var direction; e.preventDefault(); var d = e.wheelDelta !== void(0) && e.wheelDelta ? e.wheelDelta : e.detail; if (d > 0) { direction = 1; } else if (d < 0) { direction = -1; } else return; if (this.__sto) window.clearTimeout(this.__sto); this.set("dragging", true); TK.add_class(this.element, "toolkit-active"); this.__sto = window.setTimeout(function () { this.set("dragging", false); TK.remove_class(this.element, "toolkit-active"); this.fire_event("zchangeended", this.options.z); }.bind(this), 250); var s = this.range_z.get("step") * direction; if (e.ctrlKey && e.shiftKey) s *= this.range_z.get("shift_down"); else if (e.shiftKey) s *= this.range_z.get("shift_up"); this.userset("z", this.get("z") + s); if (!this._zwheel) this.fire_event("zchangestarted", this.options.z); this._zwheel = true; } /* The following functions turn positioning options * into somethine we can calculate with */ function ROT(a) { return [ +Math.sin(+a), +Math.cos(+a) ]; } var ZHANDLE_POSITION_circular = { "top": ROT(Math.PI), "center": [1e-10, 1e-10], "top-right": ROT(Math.PI*3/4), "right": ROT(Math.PI/2), "bottom-right": ROT(Math.PI/4), "bottom": ROT(0), "bottom-left": ROT(Math.PI*7/4), "left": ROT(Math.PI*3/2), "top-left": ROT(Math.PI*5/4), }; function get_zhandle_position_movable(O, X) { var vec = ZHANDLE_POSITION_circular[O.z_handle]; var x = (X[0]+X[2])/2; var y = (X[1]+X[3])/2; var R = (X[2] - X[0] - O.z_handle_size)/2; return [ x + R*vec[0], y + R*vec[1] ]; } var Z_HANDLE_SIZE_corner = [ 1, 1, 0, 0 ]; var Z_HANDLE_SIZE_horiz = [ 1, 0, 0, 1 ]; var Z_HANDLE_SIZE_vert = [ 0, 1, 1, 0 ]; function Z_HANDLE_SIZE(pos) { switch (pos) { default: TK.warn("Unsupported z_handle position:", pos); case "top-right": case "bottom-right": case "bottom-left": case "top-left": case "center": return Z_HANDLE_SIZE_corner; case "top": case "bottom": return Z_HANDLE_SIZE_vert; case "left": case "right": return Z_HANDLE_SIZE_horiz; } }; function get_zhandle_size(O, X) { var vec = Z_HANDLE_SIZE(O.z_handle); var z_handle_size = O.z_handle_size; var z_handle_centered = O.z_handle_centered; var width = X[2] - X[0]; var height = X[3] - X[1]; if (z_handle_centered < 1) { width *= z_handle_centered; height *= z_handle_centered; } else { width = z_handle_centered; height = z_handle_centered; } width = vec[0] * z_handle_size + vec[2] * width; height = vec[1] * z_handle_size + vec[3] * height; if (width < z_handle_size) width = z_handle_size; if (height < z_handle_size) height = z_handle_size; return [width, height]; } var Z_HANDLE_POS = { "top": [ 0, -1 ], "top-right": [ 1, -1 ], "right": [ 1, 0 ], "bottom-right": [ 1, 1 ], "bottom": [ 0, 1 ], "bottom-left": [ -1, 1 ], "left": [ -1, 0 ], "top-left": [ -1, -1 ], "center": [ 0, 0 ], }; function get_zhandle_position(O, X, zhandle_size) { var x = +(+X[0]+X[2]-+zhandle_size[0])/2; var y = +(+X[1]+X[3]-+zhandle_size[1])/2; var width = +X[2] - +X[0]; var height = +X[3] - +X[1]; var vec = Z_HANDLE_POS[O.z_handle] || Z_HANDLE_POS["top-right"]; x += +vec[0] * +(width - +zhandle_size[0])/2; y += +vec[1] * +(height - +zhandle_size[1])/2; return [x, y]; } function mode_to_handle(mode) { if (mode === "block-left" || mode === "block-right" || mode === "block-top" || mode === "block-bottom") return "block"; return mode; } var LABEL_ALIGN = { "line-vertical": { "top": "middle", "bottom": "middle", "left": "end", "top-left": "end", "bottom-left":"end", "right": "start", "top-right":"start", "bottom-right":"start", "center" : "middle", }, "line-horizontal": { "top": "middle", "bottom": "middle", "left": "start", "top-left": "start", "bottom-left":"start", "right": "end", "top-right":"end", "bottom-right":"end", "center" : "middle", }, "circular": { "top": "middle", "bottom": "middle", "left": "end", "top-left": "start", "bottom-left":"start", "right": "start", "top-right":"end", "bottom-right":"end", "center" : "middle", }, "block": { "top": "middle", "bottom": "middle", "left": "start", "top-left": "start", "bottom-left":"start", "right": "end", "top-right":"end", "bottom-right":"end", "center" : "middle", } } function get_label_align(O, pos) { return LABEL_ALIGN[mode_to_handle(O.mode)][pos]; } /* The following arrays contain multipliers, alternating x and y, starting with x. * The first pair is a multiplier for the handle width and height * The second pair is a multiplier for the label size * The third pair is a multiplier for the margin */ var LABEL_POSITION = { "line-vertical": { top: [ 0, -1, 0, 0, 0, 1 ], right: [ 1, 0, 0, -1/2, 1, 0 ], left: [ -1, 0, 0, -1/2, -1, 0 ], bottom: [ 0, 1, 0, -1, 0, -1 ], "bottom-left": [ -1, 1, 0, -1, -1, -1 ], "bottom-right": [ 1, 1, 0, -1, 1, -1 ], "top-right": [ 1, -1, 0, 0, 0, 1 ], "top-left": [ -1, -1, 0, 0, -1, 1 ], center: [ 0, 0, 0, -1/2, 0, 0 ], }, "line-horizontal": { top: [ 0, -1, 0, -1, 0, -1 ], right: [ 1, 0, 0, -1/2, 1, 0 ], left: [ -1, 0, 0, -1/2, -1, 0 ], bottom: [ 0, 1, 0, 0, 0, 1 ], "bottom-left": [ -1, 1, 0, 0, 1, 1 ], "bottom-right": [ 1, 1, 0, 0, -1, 1 ], "top-right": [ 1, -1, 0, -1, -1, -1 ], "top-left": [ -1, -1, 0, -1, 1, -1 ], center: [ 0, 0, 0, -1/2, 0, 0 ], }, "circular": { top: [ 0, -1, 0, -1, 0, -1 ], right: [ 1, 0, 0, -1/2, 1, 0 ], left: [ -1, 0, 0, -1/2, -1, 0 ], bottom: [ 0, 1, 0, 0, 0, 1 ], "bottom-left": [ -1, 1, 0, 0, 0, 1 ], "bottom-right": [ 1, 1, 0, 0, 0, 1 ], "top-right": [ 1, -1, 0, -1, 0, -1 ], "top-left": [ -1, -1, 0, -1, 0, -1 ], center: [ 0, 0, 0, -1/2, 0, 0 ], }, "block": { top: [ 0, -1, 0, 0, 0, 1 ], bottom: [ 0, 1, 0, -1, 0, -1 ], right: [ 1, 0, 0, -1/2, -1, 0 ], left: [ -1, 0, 0, -1/2, 1, 0 ], "bottom-left": [ -1, 1, 0, -1, 1, -1 ], "bottom-right": [ 1, 1, 0, -1, -1, -1 ], "top-right": [ 1, -1, 0, 0, -1, 1 ], "top-left": [ -1, -1, 0, 0, 1, 1 ], center: [ 0, 0, 0, -1/2, 0, 0 ], } } function get_label_position(O, X, pos, label_size) { /* X: array containing [X0, Y0, X1, Y1] of the handle * pos: string describing the position of the label ("top", "bottom-right", ...) * label_size: array containing width and height of the label */ var m = O.margin; // Pivot (x, y) is the center of the handle. var x = (X[0]+X[2])/2; var y = (X[1]+X[3])/2; // Size of handle var width = +X[2]-+X[0]; var height = +X[3]-+X[1]; // multipliers var vec = LABEL_POSITION[mode_to_handle(O.mode)][pos]; x += vec[0] * width/2 + vec[2] * label_size[0] + vec[4] * m; y += vec[1] * height/2 + vec[3] * label_size[1] + vec[5] * m; // result is [x, y] of the "real" label position. Please note that // the final x position depends on the LABEL_ALIGN value for pos. // Y value is the top border of the overall label. return [x,y]; } function remove_zhandle() { var E = this._zhandle; if (!E) return; this._zhandle = null; if (this.z_drag.get("node") === E) this.z_drag.set("node", null); E.remove(); } function create_zhandle() { var E; var O = this.options; if (this._zhandle) remove_zhandle.call(this); E = TK.make_svg( O.mode === "circular" ? "circle" : "rect", { "class": "toolkit-z-handle", } ); this._zhandle = E; if (this.z_drag.get("node") !== document) this.z_drag.set("node", E); } function create_line1() { if (this._line1) remove_line1.call(this); this._line1 = TK.make_svg("path", { "class": "toolkit-line toolkit-line-1" }); } function create_line2() { if (this._line2) remove_line2.call(this); this._line2 = TK.make_svg("path", { "class": "toolkit-line toolkit-line-2" }); } function remove_line1() { if (!this._line1) return; this._line1.remove(); this._line1 = null; } function remove_line2() { if (!this._line2) return; this._line2.remove(); this._line2 = null; } /* Prints a line, making sure that an offset of 0.5 px aligns them on * pixel boundaries */ var format_line = TK.FORMAT("M %.0f.5 %.0f.5 L %.0f.5 %.0f.5"); /* calculates the actual label positions based on given alignment * and dimensions */ function get_label_dimensions(align, X, label_size) { switch (align) { case "start": return [ X[0], X[1], X[0]+label_size[0], X[1]+label_size[1] ]; case "middle": return [ X[0]-label_size[0]/2, X[1], X[0]+label_size[0]/2, X[1]+label_size[1] ]; case "end": return [ X[0]-label_size[0], X[1], X[0], X[1]+label_size[1] ]; } } function redraw_handle(O, X) { var _handle = this._handle; if (!O.show_handle) { if (_handle) remove_handle.call(this); return; } var range_x = this.range_x; var range_y = this.range_y; var range_z = this.range_z; if (!range_x.options.basis || !range_y.options.basis) return; var x = range_x.val2px(O.x); var y = range_y.val2px(O.y); var z = range_z.val2coef(O.z); var tmp; if (O.mode === "circular") { tmp = Math.max(O.min_size, z * O.max_size)/2; X[0] = x-tmp; X[1] = y-tmp; X[2] = x+tmp; X[3] = y+tmp; _handle.setAttribute("r", tmp.toFixed(2)); _handle.setAttribute("cx", x.toFixed(2)); _handle.setAttribute("cy", y.toFixed(2)); } else if (O.mode === "block") { tmp = Math.max(O.min_size, z)/2; X[0] = x-tmp; X[1] = y-tmp; X[2] = x+tmp; X[3] = y+tmp; _handle.setAttribute("x", Math.round(+X[0]).toFixed(0)); _handle.setAttribute("y", Math.round(+X[1]).toFixed(0)); _handle.setAttribute("width", Math.round(+X[2]-X[0]).toFixed(0)); _handle.setAttribute("height", Math.round(+X[3]-X[1]).toFixed(0)); } else { var x_min = O.x_min !== false ? range_x.val2px(range_x.snap(O.x_min)) : 0; var x_max = O.x_max !== false ? range_x.val2px(range_x.snap(O.x_max)) : range_x.options.basis; if (x_min > x_max) { tmp = x_min; x_min = x_max; x_max = tmp; } var y_min = O.y_min !== false ? range_y.val2px(range_y.snap(O.y_min)) : 0; var y_max = O.y_max !== false ? range_y.val2px(range_y.snap(O.y_max)) : range_y.options.basis; if (y_min > y_max) { tmp = y_min; y_min = y_max; y_max = tmp; } tmp = O.min_size / 2; /* All other modes are drawn as rectangles */ switch (O.mode) { case "line-vertical": tmp = Math.max(tmp, z * O.max_size/2); X[0] = x-tmp; X[1] = y_min; X[2] = x+tmp; X[3] = y_max; break; case "line-horizontal": // line horizontal tmp = Math.max(tmp, z * O.max_size/2); X[0] = x_min; X[1] = y - tmp; X[2] = x_max; X[3] = y + tmp; break; case "block-left": // rect lefthand X[0] = 0; X[1] = y_min; X[2] = Math.max(x, tmp); X[3] = y_max; break; case "block-right": // rect righthand X[0] = x; X[1] = y_min; X[2] = range_x.options.basis; X[3] = y_max; if (X[2] - X[0] < tmp) X[0] = X[2] - tmp; break; case "block-top": // rect top X[0] = x_min; X[1] = 0; X[2] = x_max; X[3] = Math.max(y, tmp); break; case "block-bottom": // rect bottom X[0] = x_min; X[1] = y; X[2] = x_max; X[3] = range_y.options.basis; if (X[3] - X[1] < tmp) X[1] = X[3] - tmp; break; default: TK.warn("Unsupported mode:", O.mode); } /* Draw the rectangle */ _handle.setAttribute("x", Math.round(+X[0]).toFixed(0)); _handle.setAttribute("y", Math.round(+X[1]).toFixed(0)); _handle.setAttribute("width", Math.round(+X[2]-X[0]).toFixed(0)); _handle.setAttribute("height", Math.round(+X[3]-X[1]).toFixed(0)); } } function redraw_zhandle(O, X) { var vec; var size; var zhandle = this._zhandle; if (!O.show_handle || O.z_handle === false) { if (zhandle) remove_zhandle.call(this); return; } if (!zhandle.parentNode) this.element.appendChild(zhandle); if (this._handle && O.z_handle_below) this.element.appendChild(this._handle); if (O.mode === "circular") { /* * position the z_handle on the circle. */ vec = get_zhandle_position_movable(O, X); /* width and height are equal here */ zhandle.setAttribute("cx", vec[0].toFixed(1)); zhandle.setAttribute("cy", vec[1].toFixed(1)); zhandle.setAttribute("r", (O.z_handle_size / 2).toFixed(1)); this.zhandle_position = vec; } else if (O.mode === "block") { /* * position the z_handle on the box. */ vec = get_zhandle_position_movable(O, X); size = O.z_handle_size / 2; /* width and height are equal here */ zhandle.setAttribute("x", vec[0].toFixed(0) - size); zhandle.setAttribute("y", vec[1].toFixed(0) - size); zhandle.setAttribute("width", O.z_handle_size); zhandle.setAttribute("height", O.z_handle_size); this.zhandle_position = vec; } else { // all other handle types (lines/blocks) this.zhandle_position = vec = get_zhandle_size(O, X); zhandle.setAttribute("width", vec[0].toFixed(0)); zhandle.setAttribute("height", vec[1].toFixed(0)); vec = get_zhandle_position(O, X, vec); zhandle.setAttribute("x", vec[0].toFixed(0)); zhandle.setAttribute("y", vec[1].toFixed(0)); /* adjust to the center of the zhandle */ this.zhandle_position[0] /= 2; this.zhandle_position[1] /= 2; this.zhandle_position[0] += vec[0]; this.zhandle_position[1] += vec[1]; } this.zhandle_position[0] -= (X[0]+X[2])/2; this.zhandle_position[1] -= (X[1]+X[3])/2; normalize(this.zhandle_position); } function prevent_default(e) { e.preventDefault(); return false; } function create_label() { var E; this._label = E = TK.make_svg("text", { "class": "toolkit-label" }); TK.add_event_listener(E, "mousewheel", this._scrollwheel); TK.add_event_listener(E, "DOMMouseScroll", this._scrollwheel); TK.add_event_listener(E, 'contextmenu', prevent_default); } function remove_label() { var E = this._label; this._label = null; E.remove(); TK.remove_event_listener(E, "mousewheel", this._scrollwheel); TK.remove_event_listener(E, "DOMMouseScroll", this._scrollwheel); TK.remove_event_listener(E, 'contextmenu', prevent_default); this.label = [0,0,0,0]; } function STOP() { return false; }; function create_handle() { var O = this.options; var E; if (this._handle) remove_handle.call(this); E = TK.make_svg(O.mode === "circular" ? "circle" : "rect", { class: "toolkit-handle" }); TK.add_event_listener(E, "mousewheel", this._scrollwheel); TK.add_event_listener(E, "DOMMouseScroll", this._scrollwheel); TK.add_event_listener(E, 'selectstart', prevent_default); TK.add_event_listener(E, 'contextmenu', prevent_default); this._handle = E; this.element.appendChild(E); } function remove_handle() { var E = this._handle; if (!E) return; this._handle = null; E.remove(); TK.remove_event_listener(E, "mousewheel", this._scrollwheel); TK.remove_event_listener(E, "DOMMouseScroll", this._scrollwheel); TK.remove_event_listener(E, "selectstart", prevent_default); TK.remove_event_listener(E, 'contextmenu', prevent_default); } function redraw_label(O, X) { if (!O.show_handle || O.label === false) { if (this._label) remove_label.call(this); return false; } var a = O.label.call(this, O.title, O.x, O.y, O.z).split("\n"); var c = this._label.childNodes; while (c.length < a.length) { this._label.appendChild(TK.make_svg("tspan", {dy:"1.0em"})); } while (c.length > a.length) { this._label.removeChild(this._label.lastChild); } for (var i = 0; i < a.length; i++) { TK.set_text(c[i], a[i]); } if (!this._label.parentNode) this.element.appendChild(this._label); TK.S.add(function() { var w = 0; for (var i = 0; i < a.length; i++) { w = Math.max(w, c[i].getComputedTextLength()); } var bbox; try { bbox = this._label.getBBox(); } catch(e) { /* _label is not in the DOM yet */ return; } TK.S.add(function() { var label_size = [ w, bbox.height ]; var i; var pref = O.preferences; var area = 0; var label_position; var text_position; var text_anchor; var tmp; /* * Calculate possible positions of the labels and calculate their intersections. Choose * that position which has the smallest intersection area with all other handles and labels */ for (i = 0; i < pref.length; i++) { /* get alignment */ var align = get_label_align(O, pref[i]); /* get label position */ var LX = get_label_position(O, X, pref[i], label_size); /* calculate the label bounding box using anchor and dimensions */ var pos = get_label_dimensions(align, LX, label_size); tmp = O.intersect(pos, this); /* We require at least one square px smaller intersection * to avoid flickering label positions */ if (area === 0 || tmp.intersect + 1 < area) { area = tmp.intersect; label_position = pos; text_position = LX; text_anchor = align; /* there is no intersections, we are done */ if (area === 0) break; } } this.label = label_position; tmp = Math.round(text_position[0]) + "px"; this._label.setAttribute("x", tmp); this._label.setAttribute("y", Math.round(text_position[1]) + "px"); this._label.setAttribute("text-anchor", text_anchor); var c = this._label.childNodes; for (var i = 0; i < c.length; i++) c[i].setAttribute("x", tmp); redraw_lines.call(this, O, X); }.bind(this), 1); }.bind(this)); return true; } function redraw_lines(O, X) { if (!O.show_handle) { if (this._line1) remove_line1.call(this); if (this._line2) remove_line2.call(this); return; } var pos = this.label; var range_x = this.range_x; var range_y = this.range_y; var range_z = this.range_z; var x = range_x.val2px(O.x); var y = range_y.val2px(O.y); var z = range_z.val2px(O.z); switch (O.mode) { case "circular": case "block": if (O.show_axis) { this._line1.setAttribute("d", format_line(((y >= pos[1] && y <= pos[3]) ? Math.max(X[2], pos[2]) : X[2]) + O.margin, y, range_x.options.basis, y)); this._line2.setAttribute("d", format_line(x, ((x >= pos[0] && x <= pos[2]) ? Math.max(X[3], pos[3]) : X[3]) + O.margin, x, range_y.options.basis)); } else { if (this._line1) remove_line1.call(this); if (this._line2) remove_line2.call(this); } break; case "line-vertical": case "block-left": case "block-right": this._line1.setAttribute("d", format_line(x, X[1], x, X[3])); if (O.show_axis) { this._line2.setAttribute("d", format_line(0, y, range_x.options.basis, y)); } else if (this._line2) { remove_line2.call(this); } break; case "line-horizontal": case "block-top": case "block-bottom": this._line1.setAttribute("d", format_line(X[0], y, X[2], y)); if (O.show_axis) { this._line2.setAttribute("d", format_line(x, 0, x, range_y.options.basis)); } else if (this._line2) { remove_line2.call(this); } break; default: TK.warn("Unsupported mode", pref[i]); } if (this._line1 && !this._line1.parentNode) this.element.appendChild(this._line1); if (this._line2 && !this._line2.parentNode) this.element.appendChild(this._line2); } function set_main_class(O) { var E = this.element; var i; for (i = 0; i < MODES.length; i++) TK.remove_class(E, "toolkit-"+MODES[i]); TK.remove_class(E, "toolkit-line"); TK.remove_class(E, "toolkit-block"); switch (O.mode) { case "line-vertical": case "line-horizontal": TK.add_class(E, "toolkit-line"); case "circular": break; case "block-left": case "block-right": case "block-top": case "block-bottom": case "block": TK.add_class(E, "toolkit-block"); break; default: TK.warn("Unsupported mode:", O.mode); return; } TK.add_class(E, "toolkit-"+O.mode); } function startdrag() { this.draw_once(function() { var e = this.element; var p = e.parentNode; TK.add_class(e, "toolkit-active"); this.set("dragging", true); /* TODO: move this into the parent */ TK.add_class(this.parent.element, "toolkit-dragging"); this.global_cursor("move"); if (p.lastChild !== e) p.appendChild(e); }); } function enddrag() { this.draw_once(function() { var e = this.element; TK.remove_class(e, "toolkit-active"); this.set("dragging", false); /* TODO: move this into the parent */ TK.remove_class(this.parent.element, "toolkit-dragging"); this.remove_cursor("move"); }); } /** * Class which represents a draggable SVG element, which can be used to represent and change * a value inside of a {@link TK.ResponseHandler} and is drawn inside of a chart. * * @class TK.ResponseHandle * * @extends TK.Widget * * @param {Object} [options={ }] - An object containing initial options. * * @property {Function|Object} options.range_x - Callback returning a {@link TK.Range} * for the x-axis or an object with options for a {@link TK.Range}. This is usually * the x_range of the parent chart. * @property {Function|Object} options.range_y - Callback returning a {@link TK.Range} * for the y-axis or an object with options for a {@link TK.Range}. This is usually * the y_range of the parent chart. * @property {Function|Object} options.range_z - Callback returning a {@link TK.Range} * for the z-axis or an object with options for a {@link TK.Range}. * @property {String} [options.mode="circular"] - Type of the handle. Can be one out of * circular, line-vertical, line-horizontal, * block-left, block-right, block-top or * block-right. * @property {Number} [options.x] - Value of the x-coordinate. * @property {Number} [options.y] - Value of the y-coordinate. * @property {Number} [options.z] - Value of the z-coordinate. * @property {Number} [options.min_size=24] - Minimum size of the handle in px. * @property {Number} [options.max_size=100] - Maximum size of the handle in px. * @property {Function|Boolean} options.label - Label formatting function. Arguments are * title, x, y, z. If set to false, no label is displayed. * @property {Array} [options.preferences=["left", "top", "right", "bottom"]] - Possible label * positions by order of preference. Depending on the selected mode this can * be a subset of top, top-right, right, * bottom-right, bottom, bottom-left, * left, top-left and center. * @property {Number} [options.margin=3] - Margin in px between the handle and the label. * @property {Boolean|String} [options.z_handle=false] - If not false, a small handle is drawn at the given position (`top`, `top-left`, `top-right`, `left`, `center`, `right`, `bottom-left`, `bottom`, `bottom-right`), which can * be dragged to change the value of the z-coordinate. * @property {Number} [options.z_handle_size=6] - Size in px of the z-handle. * @property {Number} [options.z_handle_centered=0.1] - Size of the z-handle in center positions. * If this options is between 0 and 1, it is interpreted as a ratio, otherwise as a px size. * @property {Number} [options.z_handle_below=false] - Render the z-handle below the normal handle in the DOM. SVG doesn't know CSS attribute z-index, so this workaround is needed from time to time. * @property {Number} [options.x_min] - Minimum value of the x-coordinate. * @property {Number} [options.x_max] - Maximum value of the x-coordinate. * @property {Number} [options.y_min] - Minimum value of the y-coordinate. * @property {Number} [options.y_max] - Maximum value of the y-coordinate. * @property {Number} [options.z_min] - Minimum value of the z-coordinate. * @property {Number} [options.z_max] - Maximum value of the z-coordinate. * @property {Boolean} [options.show_axis=false] - If set to true, additional lines are drawn at the coordinate values. * * @mixes TK.Ranges * @mixes TK.Warning * @mixes TK.GlobalCursor */ /** * @member {SVGText} TK.ResponseHandle#_label - The label. Has class toolkit-label. */ /** * @member {SVGPath} TK.ResponseHandle#_line1 - The first line. Has class toolkit-line toolkit-line-1. */ /** * @member {SVGPath} TK.ResponseHandle#_line2 - The second line. Has class toolkit-line toolkit-line-2. */ function set_min(value, key) { var name = key.substr(0, 1); var O = this.options; if (value !== false && O[name] < value) this.set(name, value); } function set_max(value, key) { var name = key.substr(0, 1); var O = this.options; if (value !== false && O[name] > value) this.set(name, value); } /** * The useraction event is emitted when a widget gets modified by user interaction. * The event is emitted for the options x, y and z. * * @event TK.ResponseHandle#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. */ TK.ResponseHandle = TK.class({ _class: "ResponseHandle", Extends: TK.Widget, Implements: [TK.GlobalCursor, TK.Ranges, TK.Warning], _options: Object.assign(Object.create(TK.Widget.prototype._options), TK.Ranges.prototype._options, { range_x: "mixed", range_y: "mixed", range_z: "mixed", intersect: "function", mode: "string", preferences: "array", label: "function|boolean", x: "number", y: "number", z: "number", min_size: "number", max_size: "number", margin: "number", z_handle: "boolean|string", z_handle_size: "number", z_handle_centered: "number", z_handle_below: "boolean", min_drag: "number", x_min: "number", x_max: "number", y_min: "number", y_max: "number", z_min: "number", z_max: "number", active: "boolean", show_axis: "boolean", title: "string", hover: "boolean", dragging: "boolean", show_handle: "boolean" }), options: { range_x: {}, range_y: {}, range_z: {}, intersect: function () { return { intersect: 0, count: 0 } }, // NOTE: this is currently not a public API // callback function for checking intersections: function (x1, y1, x2, y2, id) {} // returns a value describing the amount of intersection with other handle elements. // intersections are weighted depending on the intersecting object. E.g. SVG borders have // a very high impact while intersecting in comparison with overlapping handle objects // that have a low impact on intersection mode: "circular", preferences: ["left", "top", "right", "bottom"], label: TK.FORMAT("%s\n%d Hz\n%.2f dB\nQ: %.2f"), x: 0, y: 0, z: 0, min_size: 24, max_size: 100, margin: 3, z_handle: false, z_handle_size: 6, z_handle_centered:0.1, z_handle_below: false, min_drag: 0, // NOTE: not yet a public API // amount of pixels the handle has to be dragged before it starts to move x_min: false, x_max: false, y_min: false, y_max: false, z_min: false, z_max: false, active: true, show_axis: false, hover: false, dragging: false, show_handle: true }, static_events: { set_show_axis: function(value) { var O = this.options; if (O.mode === "circular") create_line1.call(this); create_line2.call(this); }, set_label: function(value) { if (value !== false && !this._label) create_label.call(this); }, set_show_handle: function(value) { this.set("mode", this.options.mode); this.set("show_axis", this.options.show_axis); this.set("label", this.options.label); }, set_mode: function(value) { var O = this.options; if (!O.show_handle) return; create_handle.call(this); if (O.z_handle !== false) create_zhandle.call(this); if (value !== "circular") create_line1.call(this); }, set_x_min: set_min, set_y_min: set_min, set_z_min: set_min, set_x_max: set_max, set_y_max: set_max, set_z_max: set_max, mouseenter: function() { this.set("hover", true); }, mouseleave: function() { this.set("hover", false); }, set_active: function(v) { if (!v) { this.pos_drag.cancel_drag(); this.z_drag.cancel_drag(); } }, }, initialize: function (options) { this.label = [0,0,0,0]; this.handle = [0,0,0,0]; this._zwheel = false; this.__sto = 0; TK.Widget.prototype.initialize.call(this, options); var O = this.options; /** * @member {TK.Range} TK.ResponseHandle#range_x - The {@link TK.Range} for the x axis. */ /** * @member {TK.Range} TK.ResponseHandle#range_y - The {@link TK.Range} for the y axis. */ /** * @member {TK.Range} TK.ResponseHandle#range_z - The {@link TK.Range} for the z axis. */ this.add_range(O.range_x, "range_x"); this.add_range(O.range_y, "range_y"); this.add_range(O.range_z, "range_z"); var set_cb = function() { this.invalid.x = true; this.trigger_draw(); }.bind(this); this.range_x.add_event("set", set_cb); this.range_y.add_event("set", set_cb); this.range_z.add_event("set", set_cb); var E = TK.make_svg("g"); /** * @member {SVGGroup} TK.ResponseHandle#element - The main SVG group containing all handle elements. Has class toolkit-response-handle. */ this.element = E; this.widgetize(E, true, true); TK.add_class(E, "toolkit-response-handle"); /** * @member {SVGCircular} TK.ResponseHandle#_handle - The main handle. * Has class toolkit-handle. */ /** * @member {SVGCircular} TK.ResponseHandle#_zhandle - The handle for manipulating z axis. * Has class toolkit-z-handle. */ this._scrollwheel = scrollwheel.bind(this); this._handle = this._zhandle = this._line1 = this._line2 = this._label = null; this.z_drag = new TK.DragCapture(this, { node: null, onstartcapture: function(state) { var self = this.parent; var O = self.options; if (!O.active) return; state.z = self.range_z.val2px(O.z); /* the main handle is active, * this is a z gesture */ var pstate = self.pos_drag.state(); if (pstate) { var d; var v = [ state.current.clientX - pstate.prev.clientX, state.current.clientY - pstate.prev.clientY ]; normalize(v); state.vector = v; } else { state.vector = self.zhandle_position; } /** * Is fired when the user grabs the z-handle. The argument is the * actual z value. * * @event TK.ResponseHandle#zchangestarted * * @param {number} z - The z value. */ self.fire_event("zchangestarted", O.z); startdrag.call(self); return true; }, onmovecapture: function(state) { var self = this.parent; var O = self.options; var zv = state.vector; var v = state.vdistance(); var d = zv[0] * v[0] + zv[1] * v[1]; /* ignore small movements */ if (O.min_drag > 0 && O.min_drag > d) return; var range_z = self.range_z; var z = range_z.px2val(state.z + d); self.userset("z", z); }, onstopcapture: function() { var self = this.parent; /** * Is fired when the user releases the z-handle. The argument is the * actual z value. * * @event TK.ResponseHandle#zchangeended * * @param {number} z - The z value. */ self.fire_event("zchangeended", self.options.z); enddrag.call(self); }, }); this.pos_drag = new TK.DragCapture(this, { node: this.element, onstartcapture: function(state) { var self = this.parent; var O = self.options; if (!O.active) return; var button = state.current.button; var E = self.element; var p = E.parentNode; var ev = state.current; self.z_drag.set("node", document); /* right click triggers move to the back */ if (ev.button === 2) { if (E !== p.firstChild) self.draw_once(function() { var e = this.element; var p = e.parentNode; if (p && e !== p.firstChild) p.insertBefore(e, p.firstChild); }); /* cancel everything else, but do not drag */ ev.preventDefault(); ev.stopPropagation(); return false; } state.x = self.range_x.val2px(O.x); state.y = self.range_y.val2px(O.y); /** * Is fired when the main handle is grabbed by the user. * The argument is an object with the following members: * * * @event TK.ResponseHandle#handlegrabbed * * @param {Object} positions - An object containing all relevant positions of the pointer. */ self.fire_event("handlegrabbed", { x: O.x, y: O.y, pos_x: state.x, pos_y: state.y }); startdrag.call(self); return true; }, onmovecapture: function(state) { var self = this.parent; var O = self.options; /* ignore small movements */ if (O.min_drag > 0 && O.min_drag > state.distance()) return; /* we are changing z right now using a gesture, irgnore this movement */ if (self.z_drag.dragging()) return; var v = state.vdistance(); var range_x = self.range_x; var range_y = self.range_y; var x = range_x.px2val(state.x + v[0]); var y = range_y.px2val(state.y + v[1]); self.userset("x", x); self.userset("y", y); }, onstopcapture: function() { /** * Is fired when the user releases the main handle. * The argument is an object with the following members: * * * @event TK.ResponseHandle#handlereleased * * @param {Object} positions - An object containing all relevant positions of the pointer. */ var self = this.parent; var O = self.options; self.fire_event("handlereleased", { x: O.x, y: O.y, pos_x: self.range_x.val2px(O.x), pos_y: self.range_y.val2px(O.y), }); enddrag.call(self); self.z_drag.set("node", self._zhandle); }, }); this.set("mode", O.mode); this.set("show_handle", O.show_handle); this.set("show_axis", O.show_axis); this.set("active", O.active); this.set("x", O.x); this.set("y", O.y); this.set("z", O.z); this.set("z_handle", O.z_handle); this.set("label", O.label); }, redraw: function () { TK.Widget.prototype.redraw.call(this); var O = this.options; var I = this.invalid; var range_x = this.range_x; var range_y = this.range_y; var range_z = this.range_z; /* These are the coordinates of the corners (x1, y1, x2, y2) * NOTE: x,y are not necessarily in the midde. */ var X = this.handle; if (I.mode) set_main_class.call(this, O); if (I.hover) { I.hover = false; TK.toggle_class(this.element, "toolkit-hover", O.hover); } if (I.dragging) { I.dragging = false; TK.toggle_class(this.element, "toolkit-dragging", O.dragging); } if (I.active || I.disabled) { I.disabled = false; // TODO: this is not very nice, we should really use the options // for that. 1) set "active" from the mouse handlers 2) set disabled instead // of active TK.toggle_class(this.element, "toolkit-disabled", !O.active || O.disabled); } var moved = I.validate("x", "y", "z", "mode", "active", "show_handle"); if (moved) redraw_handle.call(this, O, X); // Z-HANDLE if (I.validate("z_handle") || moved) { redraw_zhandle.call(this, O, X); } var delay_lines; // LABEL if (I.validate("label", "title", "preference") || moved) { delay_lines = redraw_label.call(this, O, X); } // LINES if (I.validate("show_axis") || moved) { if (!delay_lines) redraw_lines.call(this, O, X); } }, set: function(key, value) { var O = this.options; switch (key) { case "z_handle": if (value !== false && !ZHANDLE_POSITION_circular[value]) { TK.warn("Unsupported z_handle option:", value); value = false; } if (value !== false) create_zhandle.call(this); break; case "x": value = this.range_x.snap(value); if (O.x_min !== false && value < O.x_min) value = O.x_min; if (O.x_max !== false && value > O.x_max) value = O.x_max; break; case "y": value = this.range_y.snap(value); if (O.y_min !== false && value < O.y_min) value = O.y_min; if (O.y_max !== false && value > O.y_max) value = O.y_max; break; case "z": value = this.range_z.snap(value); if (O.z_min !== false && value < O.z_min) { value = O.z_min; this.warning(this.element); } else if (O.z_max !== false && value > O.z_max) { value = O.z_max; this.warning(this.element); } break; } return TK.Widget.prototype.set.call(this, key, value); }, destroy: function () { remove_zhandle.call(this); remove_line1.call(this); remove_line2.call(this); remove_label.call(this); remove_handle.call(this); TK.Widget.prototype.destroy.call(this); }, }); })(this, this.TK);