Source: modules/dragcapture.js

/*
 * 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);