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