Add radix cwl graph visualization
[arvados-workbench2.git] / src / lib / cwl-svg / utils / dom-events.ts
diff --git a/src/lib/cwl-svg/utils/dom-events.ts b/src/lib/cwl-svg/utils/dom-events.ts
new file mode 100644 (file)
index 0000000..4f8132a
--- /dev/null
@@ -0,0 +1,300 @@
+export class DomEvents {
+
+    private handlers = new Map<{ removeEventListener: Function }, { [key: string]: Function[] }>();
+
+    constructor(private root: HTMLElement) {
+
+    }
+
+    public on(event: string, selector: string, handler: (event: UIEvent, target?: Element, root?: Element) => any, root?: Element): Function;
+    public on(event: string, handler: (event: UIEvent, target?: Element, root?: Element) => any, root?: Element): Function;
+    public on(...args: any[]) {
+
+        const event    = args.shift();
+        const selector = typeof args[0] === "string" ? args.shift() : undefined;
+        const handler  = typeof args[0] === "function" ? args.shift() : () => {
+        };
+        const root     = args.shift();
+
+        const eventHolder = root || this.root;
+
+        if (!this.handlers.has(eventHolder)) {
+            this.handlers.set(eventHolder, {});
+        }
+        if (!this.handlers.get(eventHolder)![event]) {
+            this.handlers.get(eventHolder)![event] = [];
+        }
+
+        const evListener = (ev: UIEvent) => {
+            let target: any;
+            if (selector) {
+                const selected = Array.from(this.root.querySelectorAll(selector));
+                target         = ev.target as HTMLElement;
+                while (target) {
+                    if (selected.find(el => el === target)) {
+                        break;
+                    }
+                    target = target.parentNode;
+                }
+
+                if (!target) {
+                    return;
+                }
+            }
+
+            const handlerOutput = handler(ev, target || ev.target, this.root);
+            if (handlerOutput === false) {
+                return false;
+            }
+
+            return false;
+        };
+
+        eventHolder.addEventListener(event, evListener);
+
+        this.handlers.get(eventHolder)![event].push(evListener);
+
+        return function off() {
+            eventHolder.removeEventListener(event, evListener);
+        }
+    }
+
+    public keyup() {
+
+    }
+
+    public adaptedDrag(selector: string,
+                       move?: (dx: number, dy: number, event: UIEvent, target?: Element, root?: Element) => any,
+                       start?: (event: UIEvent, target?: Element, root?: Element) => any,
+                       end?: (event: UIEvent, target?: Element, root?: Element) => any) {
+
+        let dragging       = false;
+        let lastMove: MouseEvent | undefined;
+        let draggedEl: Element | undefined;
+        let moveEventCount = 0;
+        let mouseDownEv: MouseEvent;
+        let threshold      = 3;
+        let mouseOverListeners: EventListener[];
+
+        const onMouseDown = (ev: MouseEvent, el: Element) => {
+            dragging    = true;
+            lastMove    = ev;
+            draggedEl   = el;
+            mouseDownEv = ev;
+
+            ev.preventDefault();
+
+            mouseOverListeners = this.detachHandlers("mouseover");
+
+            document.addEventListener("mousemove", moveHandler);
+            document.addEventListener("mouseup", upHandler);
+
+            return false;
+        };
+
+        const off = this.on("mousedown", selector, onMouseDown);
+
+        const moveHandler = (ev: MouseEvent) => {
+            if (!dragging) {
+                return;
+            }
+
+            const dx = ev.screenX - lastMove!.screenX;
+            const dy = ev.screenY - lastMove!.screenY;
+            moveEventCount++;
+
+            if (moveEventCount === threshold && typeof start === "function") {
+                start(mouseDownEv, draggedEl, this.root);
+            }
+
+            if (moveEventCount >= threshold && typeof move === "function") {
+                move(dx, dy, ev, draggedEl, this.root);
+            }
+        };
+        const upHandler   = (ev: MouseEvent) => {
+            if (moveEventCount >= threshold) {
+                if (dragging) {
+                    if (typeof end === "function") {
+                        end(ev, draggedEl, this.root)
+                    }
+                }
+
+                const parentNode        = draggedEl!.parentNode;
+                const clickCancellation = (ev: MouseEvent) => {
+                    ev.stopPropagation();
+                    parentNode!.removeEventListener("click", clickCancellation, true);
+                };
+                parentNode!.addEventListener("click", clickCancellation, true);
+            }
+
+            dragging       = false;
+            draggedEl      = undefined;
+            lastMove       = undefined;
+            moveEventCount = 0;
+            document.removeEventListener("mouseup", upHandler);
+            document.removeEventListener("mousemove", moveHandler);
+
+            for (let i in mouseOverListeners) {
+                this.root.addEventListener("mouseover", mouseOverListeners[i]);
+                this.handlers.get(this.root)!["mouseover"] = [];
+                this.handlers.get(this.root)!["mouseover"].push(mouseOverListeners[i]);
+            }
+        };
+
+        return off;
+    }
+
+
+    public drag(selector: string,
+                move?: (dx: number, dy: number, event: UIEvent, target?: Element, root?: Element) => any,
+                start?: (event: UIEvent, target?: Element, root?: Element) => any,
+                end?: (event: UIEvent, target?: Element, root?: Element) => any) {
+
+        let dragging       = false;
+        let lastMove: MouseEvent | undefined;
+        let draggedEl: Element | undefined;
+        let moveEventCount = 0;
+        let mouseDownEv: MouseEvent;
+        let threshold      = 3;
+        let mouseOverListeners: EventListener[];
+
+        const onMouseDown = (ev: MouseEvent, el: Element, root: Element) => {
+            dragging    = true;
+            lastMove    = ev;
+            draggedEl   = el;
+            mouseDownEv = ev;
+
+            ev.preventDefault();
+
+            mouseOverListeners = this.detachHandlers("mouseover");
+
+            document.addEventListener("mousemove", moveHandler);
+            document.addEventListener("mouseup", upHandler);
+
+            return false;
+        };
+
+        const off = this.on("mousedown", selector, onMouseDown);
+
+        const moveHandler = (ev: MouseEvent) => {
+            if (!dragging) {
+                return;
+            }
+
+            const dx = ev.screenX - lastMove!.screenX;
+            const dy = ev.screenY - lastMove!.screenY;
+            moveEventCount++;
+
+            if (moveEventCount === threshold && typeof start === "function") {
+                start(mouseDownEv, draggedEl, this.root);
+            }
+
+            if (moveEventCount >= threshold && typeof move === "function") {
+                move(dx, dy, ev, draggedEl, this.root);
+            }
+        };
+
+        const upHandler = (ev: MouseEvent) => {
+
+            if (moveEventCount >= threshold) {
+                if (dragging) {
+                    if (typeof end === "function") {
+                        end(ev, draggedEl, this.root)
+                    }
+                }
+
+                // When releasing the mouse button, if it happens over the same element that we initially had
+                // the mouseDown event, it will trigger a click event. We want to stop that, so we intercept
+                // it by capturing click top-down and stopping its propagation.
+                // However, if the mouseUp didn't happen above the starting element, it wouldn't trigger a click,
+                // but it would intercept the next (unrelated) click event unless we prevent interception in the
+                // first place by checking if we released above the starting element.
+                if (draggedEl!.contains(ev.target as Node)) {
+                    const parentNode = draggedEl!.parentNode;
+
+                    const clickCancellation = (ev: MouseEvent) => {
+                        ev.stopPropagation();
+                        parentNode!.removeEventListener("click", clickCancellation, true);
+                    };
+                    parentNode!.addEventListener("click", clickCancellation, true);
+                }
+
+            }
+
+            dragging       = false;
+            draggedEl      = undefined;
+            lastMove       = undefined;
+            moveEventCount = 0;
+            document.removeEventListener("mouseup", upHandler);
+            document.removeEventListener("mousemove", moveHandler);
+
+
+            for (let i in mouseOverListeners) {
+                this.root.addEventListener("mouseover", mouseOverListeners[i]);
+                this.handlers.get(this.root)!["mouseover"] = [];
+                this.handlers.get(this.root)!["mouseover"].push(mouseOverListeners[i]);
+            }
+        };
+
+        return off;
+    }
+
+    public hover(element: HTMLElement,
+                 hover: (event: UIEvent, target?: HTMLElement, root?: HTMLElement) => any = () => {},
+                 enter: (event: UIEvent, target?: HTMLElement, root?: HTMLElement) => any = () => {},
+                 leave: (event: UIEvent, target?: HTMLElement, root?: HTMLElement) => any = () => {}) {
+
+        let hovering = false;
+
+        element.addEventListener("mouseenter", (ev: MouseEvent) => {
+            hovering = true;
+            enter(ev, element, this.root);
+
+        });
+
+        element.addEventListener("mouseleave", (ev) => {
+            hovering = false;
+            leave(ev, element, this.root);
+        });
+
+        element.addEventListener("mousemove", (ev) => {
+            if (!hovering) {
+                return;
+            }
+            hover(ev, element, this.root);
+        });
+    }
+
+    public detachHandlers(evName: string, root?: HTMLElement): EventListener[] {
+        root                                = root || this.root;
+        let eventListeners: EventListener[] = [];
+        this.handlers.forEach((handlers: { [event: string]: EventListener[] }, listenerRoot: Element) => {
+            if (listenerRoot.id !== root!.id || listenerRoot !== root) {
+                return;
+            }
+            for (let eventName in handlers) {
+                if (eventName !== evName) {
+                    continue;
+                }
+                handlers[eventName].forEach((handler) => {
+                    eventListeners.push(handler);
+                    listenerRoot.removeEventListener(eventName, handler);
+                });
+            }
+        });
+
+        delete this.handlers.get(this.root)![evName];
+
+        return eventListeners;
+    }
+
+    public detachAll() {
+        this.handlers.forEach((handlers: { [event: string]: EventListener[] }, listenerRoot: Element) => {
+            for (let eventName in handlers) {
+                handlers[eventName].forEach(handler => listenerRoot.removeEventListener(eventName, handler));
+            }
+        });
+
+        this.handlers.clear();
+    }
+}