X-Git-Url: https://git.arvados.org/arvados-workbench2.git/blobdiff_plain/4f5fdd49c0c9866fbb613c6c82641d686b0b8ad5..7f137c5a4162717b74668a513b8b10a1ed3d8577:/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 index 00000000..4f8132a0 --- /dev/null +++ b/src/lib/cwl-svg/utils/dom-events.ts @@ -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(); + } +}