/* * 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 document = window.document; /* this has no global symbol */ function CaptureState(start) { this.start = start; this.prev = start; this.current = start; } CaptureState.prototype = { /* distance from start */ distance: function() { var v = this.vdistance(); return Math.sqrt(v[0]*v[0] + v[1]*v[1]); }, set_current: function(ev) { this.prev = this.current; this.current = ev; return true; }, vdistance: function() { var start = this.start; var current = this.current; return [ current.clientX - start.clientX, current.clientY - start.clientY ]; }, prev_distance: function() { var prev = this.prev; var current = this.current; return [ current.clientX - prev.clientX, current.clientY - prev.clientY ]; }, }; /* general api */ function startcapture(state) { /* do nothing, let other handlers be called */ if (this.drag_state) return; /** * Capturing started. * * @event TK.DragCapture#startcapture * * @param {object} state - An internal state object. * @param {DOMEvent} start - The event object of the initial event. */ var v = this.fire_event("startcapture", state, state.start); if (v === true) { /* we capture this event */ this.drag_state = state; this.set("state", true); } return v; } function movecapture(ev) { var d = this.drag_state; /** * A movement was captured. * * @event TK.DragCapture#movecapture * * @param {DOMEvent} event - The event object of the current move event. */ if (!d.set_current(ev) || this.fire_event("movecapture", d) === false) { stopcapture.call(this, ev); return false; } } function stopcapture(ev) { var s = this.drag_state; if (s === null) return; /** * Capturing stopped. * * @event TK.DragCapture#stopcapture * * @param {object} state - An internal state object. * @param {DOMEvent} event - The event object of the current event. */ this.fire_event("stopcapture", s, ev); this.set("state", false); s.destroy(); this.drag_state = null; } /* mouse handling */ function MouseCaptureState(start) { this.__mouseup = null; this.__mousemove = null; CaptureState.call(this, start); } MouseCaptureState.prototype = Object.assign(Object.create(CaptureState.prototype), { set_current: function(ev) { var start = this.start; /* If the buttons have changed, we assume that the capture has ended */ if (!this.is_dragged_by(ev)) return false; return CaptureState.prototype.set_current.call(this, ev); }, init: function(widget) { this.__mouseup = mouseup.bind(widget); this.__mousemove = mousemove.bind(widget); document.addEventListener("mousemove", this.__mousemove); document.addEventListener("mouseup", this.__mouseup); }, destroy: function() { document.removeEventListener("mousemove", this.__mousemove); document.removeEventListener("mouseup", this.__mouseup); this.__mouseup = null; this.__mousemove = null; }, is_dragged_by: function(ev) { var start = this.start; if (start.buttons !== ev.buttons || start.which !== ev.which) return false; return true; }, }); function mousedown(ev) { var s = new MouseCaptureState(ev); var v = startcapture.call(this, s); /* ignore this event */ if (v === void(0)) return; ev.stopPropagation(); ev.preventDefault(); /* we did capture */ if (v === true) s.init(this); return false; } function mousemove(ev) { movecapture.call(this, ev); } function mouseup(ev) { stopcapture.call(this, ev); } /* touch handling */ /* * Old Safari versions will keep the same Touch objects for the full lifetime * and simply update the coordinates, etc. This is a bug, which we work around by * cloning the information we need. */ function clone_touch(t) { return { clientX: t.clientX, clientY: t.clientY, identifier: t.identifier, }; } function TouchCaptureState(start) { CaptureState.call(this, start); var touch = start.changedTouches.item(0); touch = clone_touch(touch); this.stouch = touch; this.ptouch = touch; this.ctouch = touch; } TouchCaptureState.prototype = Object.assign(Object.create(CaptureState.prototype), { find_touch: function(ev) { var id = this.stouch.identifier; var touches = ev.changedTouches; var touch; for (var i = 0; i < touches.length; i++) { touch = touches.item(i); if (touch.identifier === id) return touch; } return null; }, set_current: function(ev) { var touch = clone_touch(this.find_touch(ev)); this.ptouch = this.ctouch; this.ctouch = touch; return CaptureState.prototype.set_current.call(this, ev); }, vdistance: function() { var start = this.stouch; var current = this.ctouch; return [ current.clientX - start.clientX, current.clientY - start.clientY ]; }, prev_distance: function() { var prev = this.ptouch; var current = this.ctouch; return [ current.clientX - prev.clientX, current.clientY - prev.clientY ]; }, destroy: function() { }, is_dragged_by: function(ev) { return this.find_touch(ev) !== null; }, }); function touchstart(ev) { /* if cancelable is false, this is an async touchstart, which happens * during scrolling */ if (!ev.cancelable) return; /* the startcapture event handler has return false. we do not handle this * pointer */ var v = startcapture.call(this, new TouchCaptureState(ev)); if (v === void(0)) return; ev.preventDefault(); ev.stopPropagation(); return false; } function touchmove(ev) { if (!this.drag_state) return; /* we are scrolling, ignore the event */ if (!ev.cancelable) return; /* if we cannot find the right touch, some other touchpoint * triggered this event and we do not care about that */ if (!this.drag_state.find_touch(ev)) return; /* if movecapture returns false, the capture has ended */ if (movecapture.call(this, ev) !== false) { ev.preventDefault(); ev.stopPropagation(); return false; } } function touchend(ev) { var s; if (!ev.cancelable) return; s = this.drag_state; /* either we are not dragging or it is another touch point */ if (!s || !s.find_touch(ev)) return; stopcapture.call(this, ev); ev.stopPropagation(); ev.preventDefault(); return false; } function touchcancel(ev) { return touchend.call(this, ev); } var dummy = function() {}; function get_parents(e) { var ret = []; if (Array.isArray(e)) e.map(function(e) { e = e.parentNode; if (e) ret.push(e); }); else if (e = e.parentNode) ret.push(e); return ret; } var static_events = { set_node: function(value) { this.delegate_events(value); }, contextmenu: function() { return false; }, delegated: [ function(element, old_element) { /* cancel the current capture */ if (old_element) stopcapture.call(this); }, function(elem, old) { /* NOTE: this works around a bug in chrome (#673102) */ if (old) TK.remove_event_listener(get_parents(old), "touchstart", dummy); if (elem) TK.add_event_listener(get_parents(elem), "touchstart", dummy); } ], touchstart: touchstart, touchmove: touchmove, touchend: touchend, touchcancel: touchcancel, mousedown: mousedown, }; TK.DragCapture = TK.class({ /** * TK.DragCapture is a low-level class for tracking drag events on * both, touch and mouse events. It can be used for implementing drag'n'drop * functionality as well as dragging the value of e.g. {@link TK.Fader} or * {@link TK.Knob}. {@link TK.DragValue} derives from TK.DragCapture. * * @extends TK.Module * * @param {Object} widget - The parent widget making use of DragValue. * @param {Object} [options={ }] - An object containing initial options. * * @property {HTMLElement} [options.node] - The DOM element receiving the drag events. If not set the widgets element is used. * * @class TK.DragCapture */ Extends: TK.Module, _class: "DragCapture", _options: { node: "object", state: "boolean", /* internal, undocumented */ }, options: { state: false, }, static_events: static_events, initialize: function(widget, O) { TK.Module.prototype.initialize.call(this, widget, O); this.drag_state = null; if (O.node === void(0)) O.node = widget.element; this.set("node", O.node); }, destroy: function() { TK.Base.prototype.destroy.call(this); stopcapture.call(this); }, cancel_drag: stopcapture, dragging: function() { return this.options.state; }, state: function() { return this.drag_state; }, is_dragged_by: function(ev) { return this.drag_state !== null && this.drag_state.is_dragged_by(ev); }, }); })(this, this.TK);