16073: Add default view for empty process IO panels
[arvados.git] / src / lib / cwl-svg / utils / dom-events.ts
1 export class DomEvents {
2
3     private handlers = new Map<{ removeEventListener: Function }, { [key: string]: Function[] }>();
4
5     constructor(private root: HTMLElement) {
6
7     }
8
9     public on(event: string, selector: string, handler: (event: UIEvent, target?: Element, root?: Element) => any, root?: Element): Function;
10     public on(event: string, handler: (event: UIEvent, target?: Element, root?: Element) => any, root?: Element): Function;
11     public on(...args: any[]) {
12
13         const event    = args.shift();
14         const selector = typeof args[0] === "string" ? args.shift() : undefined;
15         const handler  = typeof args[0] === "function" ? args.shift() : () => {
16         };
17         const root     = args.shift();
18
19         const eventHolder = root || this.root;
20
21         if (!this.handlers.has(eventHolder)) {
22             this.handlers.set(eventHolder, {});
23         }
24         if (!this.handlers.get(eventHolder)![event]) {
25             this.handlers.get(eventHolder)![event] = [];
26         }
27
28         const evListener = (ev: UIEvent) => {
29             let target: any;
30             if (selector) {
31                 const selected = Array.from(this.root.querySelectorAll(selector));
32                 target         = ev.target as HTMLElement;
33                 while (target) {
34                     // eslint-disable-next-line
35                     if (selected.find(el => el === target)) {
36                         break;
37                     }
38                     target = target.parentNode;
39                 }
40
41                 if (!target) {
42                     return;
43                 }
44             }
45
46             const handlerOutput = handler(ev, target || ev.target, this.root);
47             if (handlerOutput === false) {
48                 return false;
49             }
50
51             return false;
52         };
53
54         eventHolder.addEventListener(event, evListener);
55
56         this.handlers.get(eventHolder)![event].push(evListener);
57
58         return function off() {
59             eventHolder.removeEventListener(event, evListener);
60         }
61     }
62
63     public keyup() {
64
65     }
66
67     public adaptedDrag(selector: string,
68                        move?: (dx: number, dy: number, event: UIEvent, target?: Element, root?: Element) => any,
69                        start?: (event: UIEvent, target?: Element, root?: Element) => any,
70                        end?: (event: UIEvent, target?: Element, root?: Element) => any) {
71
72         let dragging       = false;
73         let lastMove: MouseEvent | undefined;
74         let draggedEl: Element | undefined;
75         let moveEventCount = 0;
76         let mouseDownEv: MouseEvent;
77         let threshold      = 3;
78         let mouseOverListeners: EventListener[];
79
80         const onMouseDown = (ev: MouseEvent, el: Element) => {
81             dragging    = true;
82             lastMove    = ev;
83             draggedEl   = el;
84             mouseDownEv = ev;
85
86             ev.preventDefault();
87
88             mouseOverListeners = this.detachHandlers("mouseover");
89
90             document.addEventListener("mousemove", moveHandler);
91             document.addEventListener("mouseup", upHandler);
92
93             return false;
94         };
95
96         const off = this.on("mousedown", selector, onMouseDown);
97
98         const moveHandler = (ev: MouseEvent) => {
99             if (!dragging) {
100                 return;
101             }
102
103             const dx = ev.screenX - lastMove!.screenX;
104             const dy = ev.screenY - lastMove!.screenY;
105             moveEventCount++;
106
107             if (moveEventCount === threshold && typeof start === "function") {
108                 start(mouseDownEv, draggedEl, this.root);
109             }
110
111             if (moveEventCount >= threshold && typeof move === "function") {
112                 move(dx, dy, ev, draggedEl, this.root);
113             }
114         };
115         const upHandler   = (ev: MouseEvent) => {
116             if (moveEventCount >= threshold) {
117                 if (dragging) {
118                     if (typeof end === "function") {
119                         end(ev, draggedEl, this.root)
120                     }
121                 }
122
123                 const parentNode        = draggedEl!.parentNode;
124                 const clickCancellation = (ev: MouseEvent) => {
125                     ev.stopPropagation();
126                     parentNode!.removeEventListener("click", clickCancellation, true);
127                 };
128                 parentNode!.addEventListener("click", clickCancellation, true);
129             }
130
131             dragging       = false;
132             draggedEl      = undefined;
133             lastMove       = undefined;
134             moveEventCount = 0;
135             document.removeEventListener("mouseup", upHandler);
136             document.removeEventListener("mousemove", moveHandler);
137
138             for (let i in mouseOverListeners) {
139                 this.root.addEventListener("mouseover", mouseOverListeners[i]);
140                 this.handlers.get(this.root)!["mouseover"] = [];
141                 this.handlers.get(this.root)!["mouseover"].push(mouseOverListeners[i]);
142             }
143         };
144
145         return off;
146     }
147
148
149     public drag(selector: string,
150                 move?: (dx: number, dy: number, event: UIEvent, target?: Element, root?: Element) => any,
151                 start?: (event: UIEvent, target?: Element, root?: Element) => any,
152                 end?: (event: UIEvent, target?: Element, root?: Element) => any) {
153
154         let dragging       = false;
155         let lastMove: MouseEvent | undefined;
156         let draggedEl: Element | undefined;
157         let moveEventCount = 0;
158         let mouseDownEv: MouseEvent;
159         let threshold      = 3;
160         let mouseOverListeners: EventListener[];
161
162         const onMouseDown = (ev: MouseEvent, el: Element, root: Element) => {
163             dragging    = true;
164             lastMove    = ev;
165             draggedEl   = el;
166             mouseDownEv = ev;
167
168             ev.preventDefault();
169
170             mouseOverListeners = this.detachHandlers("mouseover");
171
172             document.addEventListener("mousemove", moveHandler);
173             document.addEventListener("mouseup", upHandler);
174
175             return false;
176         };
177
178         const off = this.on("mousedown", selector, onMouseDown);
179
180         const moveHandler = (ev: MouseEvent) => {
181             if (!dragging) {
182                 return;
183             }
184
185             const dx = ev.screenX - lastMove!.screenX;
186             const dy = ev.screenY - lastMove!.screenY;
187             moveEventCount++;
188
189             if (moveEventCount === threshold && typeof start === "function") {
190                 start(mouseDownEv, draggedEl, this.root);
191             }
192
193             if (moveEventCount >= threshold && typeof move === "function") {
194                 move(dx, dy, ev, draggedEl, this.root);
195             }
196         };
197
198         const upHandler = (ev: MouseEvent) => {
199
200             if (moveEventCount >= threshold) {
201                 if (dragging) {
202                     if (typeof end === "function") {
203                         end(ev, draggedEl, this.root)
204                     }
205                 }
206
207                 // When releasing the mouse button, if it happens over the same element that we initially had
208                 // the mouseDown event, it will trigger a click event. We want to stop that, so we intercept
209                 // it by capturing click top-down and stopping its propagation.
210                 // However, if the mouseUp didn't happen above the starting element, it wouldn't trigger a click,
211                 // but it would intercept the next (unrelated) click event unless we prevent interception in the
212                 // first place by checking if we released above the starting element.
213                 if (draggedEl!.contains(ev.target as Node)) {
214                     const parentNode = draggedEl!.parentNode;
215
216                     const clickCancellation = (ev: MouseEvent) => {
217                         ev.stopPropagation();
218                         parentNode!.removeEventListener("click", clickCancellation, true);
219                     };
220                     parentNode!.addEventListener("click", clickCancellation, true);
221                 }
222
223             }
224
225             dragging       = false;
226             draggedEl      = undefined;
227             lastMove       = undefined;
228             moveEventCount = 0;
229             document.removeEventListener("mouseup", upHandler);
230             document.removeEventListener("mousemove", moveHandler);
231
232
233             for (let i in mouseOverListeners) {
234                 this.root.addEventListener("mouseover", mouseOverListeners[i]);
235                 this.handlers.get(this.root)!["mouseover"] = [];
236                 this.handlers.get(this.root)!["mouseover"].push(mouseOverListeners[i]);
237             }
238         };
239
240         return off;
241     }
242
243     public hover(element: HTMLElement,
244                  hover: (event: UIEvent, target?: HTMLElement, root?: HTMLElement) => any = () => {},
245                  enter: (event: UIEvent, target?: HTMLElement, root?: HTMLElement) => any = () => {},
246                  leave: (event: UIEvent, target?: HTMLElement, root?: HTMLElement) => any = () => {}) {
247
248         let hovering = false;
249
250         element.addEventListener("mouseenter", (ev: MouseEvent) => {
251             hovering = true;
252             enter(ev, element, this.root);
253
254         });
255
256         element.addEventListener("mouseleave", (ev) => {
257             hovering = false;
258             leave(ev, element, this.root);
259         });
260
261         element.addEventListener("mousemove", (ev) => {
262             if (!hovering) {
263                 return;
264             }
265             hover(ev, element, this.root);
266         });
267     }
268
269     public detachHandlers(evName: string, root?: HTMLElement): EventListener[] {
270         root                                = root || this.root;
271         let eventListeners: EventListener[] = [];
272         this.handlers.forEach((handlers: { [event: string]: EventListener[] }, listenerRoot: Element) => {
273             if (listenerRoot.id !== root!.id || listenerRoot !== root) {
274                 return;
275             }
276             for (let eventName in handlers) {
277                 if (eventName !== evName) {
278                     continue;
279                 }
280                 handlers[eventName].forEach((handler) => {
281                     eventListeners.push(handler);
282                     listenerRoot.removeEventListener(eventName, handler);
283                 });
284             }
285         });
286
287         delete this.handlers.get(this.root)![evName];
288
289         return eventListeners;
290     }
291
292     public detachAll() {
293         this.handlers.forEach((handlers: { [event: string]: EventListener[] }, listenerRoot: Element) => {
294             for (let eventName in handlers) {
295                 handlers[eventName].forEach(handler => listenerRoot.removeEventListener(eventName, handler));
296             }
297         });
298
299         this.handlers.clear();
300     }
301 }