--- /dev/null
+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();
+ }
+}