File : controls/sortableListControl/TouchListItemEL.js

1
/*
2
Copyright - 2017 2023 - wwwouaiebe - Contact: https://www.ouaie.be/
3
4
This  program is free software;
5
you can redistribute it and/or modify it under the terms of the
6
GNU General Public License as published by the Free Software Foundation;
7
either version 3 of the License, or any later version.
8
9
This program is distributed in the hope that it will be useful,
10
but WITHOUT ANY WARRANTY; without even the implied warranty of
11
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
GNU General Public License for more details.
13
14
You should have received a copy of the GNU General Public License
15
along with this program; if not, write to the Free Software
16
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
17
*/
18
/*
19
Changes:
20
    - v4.0.0:
21
        - created from v3.6.0
22
Doc reviewed 202208
23
 */
24
25
import { ZERO, ONE, TWO } from '../../main/Constants.js';
26
27
/* ------------------------------------------------------------------------------------------------------------------------- */
28
/**
29
touchstart, touchmove, touchend and touchcancel on an list item event listener
30
*/
31
/* ------------------------------------------------------------------------------------------------------------------------- */
32
33
class TouchListItemEL {
34
35
    /**
36
    The function to call when an item is droped
37
    @type {function}
38
    */
39
40
    #dropFunction;
41
42
    /**
43
    A boolean to store if the event was generated with a double click or not
44
    @type {boolean}
45
    */
46
47
    #isDoubleClick;
48
49
    /**
50
    The timestamp of the last touchstart event
51
    @type {Number}
52
    */
53
54
    #lastTouchStartTimeStamp;
55
56
    /**
57
    The timestamp of the last scroll action
58
    @type {Number}
59
    */
60
61
    #lastScrollTimeStamp;
62
63
    /**
64
    A clone of the html element selected for dragging
65
    @type {HTMLElement}
66
    */
67
68
    #clonedListItemHTMLElement;
69
70
    /**
71
    The container of the displayed list
72
    @type {HTMLElement}
73
    */
74
75
    #sortableListHTMLElement;
76
77
    /**
78
    The container with the scroll bars
79
    @type {HTMLElement}
80
    */
81
82
    #scrolledContainerHTMLElement;
83
84
    /**
85
    The Y position in pixels where the top scroll will start
86
    @type {Number}
87
    */
88
89
    #topScrollPosition;
90
91
    /**
92
    The Y position in pixels where the bottom scroll will start
93
    @type {Number}
94
    */
95
96
    #bottomScrollPosition;
97
98
    /**
99
    The Y position in pixels of the current touch event
100
    @type {Number}
101
    */
102
103
    #touchY;
104
105
    /**
106
    The drop target
107
    @type {HTMLElement}
108
    */
109
110
    #dropTargetHTMLElement;
111
112
    /**
113
    A boolean - true when the drop point is on top of the target and false when at bottom
114
    @type {boolean}
115
    */
116
117
    #dropOnTop;
118
119
    /**
120
    the X distance betwwen the touch point and the top left corner of the moved item
121
    @type {Number}
122
    */
123
124
    #deltaMoveX;
125
126
    /**
127
    the Y distance betwwen the touch point and the top left corner of the moved item
128
    @type {Number}
129
    */
130
131
    #deltaMoveY;
132
133
    /**
134
    A constant giving the max delay in ms between 2 clicks to consider it's a double click
135
    @type {Number}
136
    */
137
138
    // eslint-disable-next-line no-magic-numbers
139
    static get #DBL_CLICK_MAX_DELAY ( ) { return 1000; }
140
141
    /**
142
    A constant giving the delay in ms betwwen two scroll actions for the requestAnimationFrame ( ) method
143
    See also https://developer.mozilla.org/fr/docs/Web/API/Document/scroll_event
144
    @type {Number}
145
    */
146
147
    // eslint-disable-next-line no-magic-numbers
148
    static get #SCROLL_DELAY ( ) { return 40; }
149
150
    /**
151
    A constant giving the number of pixels to scroll
152
    @type {Number}
153
    */
154
155
    // eslint-disable-next-line no-magic-numbers
156
    static get #SCROLL_VALUE ( ) { return 5; }
157
158
    /**
159
    A constant giving the distance in pixel betwwen the top of the container and the place where
160
    the scroll will start
161
    @type {Number}
162
    */
163
164
    // eslint-disable-next-line no-magic-numbers
165
    static get #TOP_SCROLL_DISTANCE ( ) { return 200; }
166
167
    /**
168
    A constant giving the distance in pixel betwwen the bottom of the container and the place where
169
    the scroll will start
170
    @type {Number}
171
    */
172
173
    // eslint-disable-next-line no-magic-numbers
174
    static get #BOTTOM_SCROLL_DISTANCE ( ) { return 100; }
175
176
    /**
177
    Scroll the list container to the top
178
    @param {Number} scrollTimeStamp The time stamp of the call to the method. See window.requestAnimationFrame ( )
179
    */
180
181
    #scrollContainerToTop ( scrollTimeStamp ) {
182
        if (
183
            scrollTimeStamp !== this.#lastScrollTimeStamp
184
            &&
185
            TouchListItemEL.#SCROLL_DELAY < scrollTimeStamp - this.#lastScrollTimeStamp
186
        ) {
187
            this.#scrolledContainerHTMLElement.scrollTop -= TouchListItemEL.#SCROLL_VALUE;
188
            this.#lastScrollTimeStamp = scrollTimeStamp;
189
        }
190
        if (
191
            this.#topScrollPosition > this.#touchY
192
            &&
193
            ZERO < this.#scrolledContainerHTMLElement.scrollTop
194
        ) {
195
            window.requestAnimationFrame (
196
                scrollTime => { this.#scrollContainerToTop ( scrollTime ); }
197
            );
198
        }
199
    }
200
201
    /**
202
    Scroll the list container to the bottom
203
    @param {Number} scrollTimeStamp The time stamp of the call to the method. See window.requestAnimationFrame ( )
204
    */
205
206
    #scrollContainerToBottom ( scrollTimeStamp ) {
207
        if (
208
            scrollTimeStamp !== this.#lastScrollTimeStamp
209
            &&
210
            TouchListItemEL.#SCROLL_DELAY < scrollTimeStamp - this.#lastScrollTimeStamp
211
        ) {
212
            this.#scrolledContainerHTMLElement.scrollTop += TouchListItemEL.#SCROLL_VALUE;
213
            this.#lastScrollTimeStamp = scrollTimeStamp;
214
        }
215
        let isFullyScrolledDown =
216
        ONE > Math.abs (
217
            this.#scrolledContainerHTMLElement.scrollHeight -
218
            this.#scrolledContainerHTMLElement.clientHeight -
219
            this.#scrolledContainerHTMLElement.scrollTop
220
        );
221
        if (
222
            this.#bottomScrollPosition < this.#touchY
223
            &&
224
            ! isFullyScrolledDown
225
        ) {
226
            window.requestAnimationFrame (
227
                scrollTime => { this.#scrollContainerToBottom ( scrollTime ); }
228
            );
229
        }
230
    }
231
232
    /**
233
    Handle the touchstart event
234
    @param {Event} touchEvent The event to handle
235
    */
236
237
    #handleStartEvent ( touchEvent ) {
238
        const touch = touchEvent.changedTouches.item ( ZERO );
239
        if ( ONE === touchEvent.touches.length ) {
240
            if (
241
                TouchListItemEL.#DBL_CLICK_MAX_DELAY < touchEvent.timeStamp - this.#lastTouchStartTimeStamp
242
                ||
243
                ZERO === this.#lastTouchStartTimeStamp
244
            ) {
245
246
                // it's a simple click => we return, waiting a second click
247
                this.#lastTouchStartTimeStamp = touchEvent.timeStamp;
248
                return;
249
            }
250
251
            // It's a double click. Stopping the scroll in the container HTMLelement
252
            touchEvent.preventDefault ( );
253
254
            this.#isDoubleClick = true;
255
256
            // Saving the position of the list container
257
            this.#sortableListHTMLElement = touchEvent.currentTarget.parentNode;
258
            this.#scrolledContainerHTMLElement = touchEvent.currentTarget.parentNode.parentNode.parentNode;
259
            this.#touchY = touch.screenY;
260
261
            // Computing the distance betwwen the touch point and the top left corner of the moved item
262
            const boundingClientRect = touchEvent.currentTarget.getBoundingClientRect ( );
263
            this.#deltaMoveX = touch.screenX - boundingClientRect.left;
264
            this.#deltaMoveY = touch.screenY - boundingClientRect.top;
265
266
            // cloning the node and append it to the document
267
            this.#clonedListItemHTMLElement = touchEvent.currentTarget.cloneNode ( true );
268
            this.#clonedListItemHTMLElement.classList.add ( 'TravelNotes-SortableList-DraggedListItemHTMLElement' );
269
            document.body.appendChild ( this.#clonedListItemHTMLElement );
270
            this.#clonedListItemHTMLElement.style.left = String ( touch.screenX - this.#deltaMoveX ) + 'px';
271
            this.#clonedListItemHTMLElement.style.top = String ( touch.screenY - this.#deltaMoveY ) + 'px';
272
            this.#topScrollPosition =
273
                this.#sortableListHTMLElement.getBoundingClientRect ( ).y -
274
                this.#scrolledContainerHTMLElement.getBoundingClientRect ( ).y +
275
                this.#scrolledContainerHTMLElement.scrollTop +
276
                TouchListItemEL.#TOP_SCROLL_DISTANCE;
277
            this.#bottomScrollPosition =
278
            this.#scrolledContainerHTMLElement.getBoundingClientRect ( ).bottom -
279
                TouchListItemEL.#BOTTOM_SCROLL_DISTANCE;
280
        }
281
    }
282
283
    /**
284
    Handle the touchmove event
285
    @param {Event} touchEvent The event to handle
286
    */
287
288
    #handleMoveEvent ( touchEvent ) {
289
        if ( ! this.#isDoubleClick ) {
290
            return;
291
        }
292
        const touch = touchEvent.changedTouches.item ( ZERO );
293
        if ( ONE === touchEvent.touches.length ) {
294
            if ( this.#clonedListItemHTMLElement ) {
295
296
                // moving the cloned node to the touch position
297
                this.#clonedListItemHTMLElement.style.left = String ( touch.screenX - this.#deltaMoveX ) + 'px';
298
                this.#clonedListItemHTMLElement.style.top = String ( touch.screenY - this.#deltaMoveY ) + 'px';
299
300
                // scrolling the container to the top if needed
301
                this.#touchY = touch.screenY;
302
                if (
303
                    this.#topScrollPosition > this.#touchY
304
                    &&
305
                    ZERO < this.#scrolledContainerHTMLElement.scrollTop
306
                ) {
307
                    window.requestAnimationFrame (
308
                        scrollTime => { this.#scrollContainerToTop ( scrollTime ); }
309
                    );
310
                }
311
312
                // scrolling the container to the bottom if needed
313
                let isFullyScrolledDown =
314
                    ONE > Math.abs (
315
                        this.#scrolledContainerHTMLElement.scrollHeight -
316
                        this.#scrolledContainerHTMLElement.clientHeight -
317
                        this.#scrolledContainerHTMLElement.scrollTop
318
                    );
319
                if (
320
                    this.#bottomScrollPosition < this.#touchY
321
                    &&
322
                    ! isFullyScrolledDown
323
                ) {
324
                    window.requestAnimationFrame (
325
                        scrollTime => { this.#scrollContainerToBottom ( scrollTime ); }
326
                    );
327
                }
328
            }
329
        }
330
    }
331
332
    /**
333
    Set the drop target and the drop position from the touch of the dragend event
334
    @param {Touch} touch The dragend event touch
335
    */
336
337
    #setDropTargetAndPosition ( touch ) {
338
339
        // iterating on the listItems
340
        this.#sortableListHTMLElement.childNodes.forEach (
341
            listItem => {
342
                let clientRect = listItem.getBoundingClientRect ( );
343
344
                // Searching if the touch is in the bounding client rectangle
345
                if (
346
                    clientRect.left < touch.clientX
347
                    &&
348
                    clientRect.right > touch.clientX
349
                    &&
350
                    clientRect.top < touch.clientY
351
                    &&
352
                    clientRect.bottom > touch.clientY
353
                ) {
354
355
                    // setting the drop target
356
                    this.#dropTargetHTMLElement = listItem;
357
358
                    // setting the dropOnTop flag depending of the y position of the touch in the bounding client rectangle
359
                    this.#dropOnTop = touch.clientY - clientRect.top < clientRect.height / TWO;
360
                }
361
            }
362
        );
363
    }
364
365
    /**
366
    Handle the touchend event
367
    @param {Event} touchEvent The event to handle
368
    */
369
370
    #handleEndEvent ( touchEvent ) {
371
        if ( this.#isDoubleClick ) {
372
            let touch = touchEvent.changedTouches.item ( ZERO );
373
            this.#setDropTargetAndPosition ( touch );
374
            if ( this.#dropTargetHTMLElement ) {
375
376
                // Try ... catch because a lot of thing can be dragged in the dialog and the drop function
377
                // throw when an unknown objId is given
378
                try {
379
                    this.#dropFunction (
380
                        Number.parseInt ( touchEvent.currentTarget.dataset.tanObjId ),
381
                        Number.parseInt ( this.#dropTargetHTMLElement.dataset.tanObjId ),
382
                        this.#dropOnTop
383
                    );
384
                }
385
386
                // eslint-disable-next-line no-empty
387
                catch { }
388
            }
389
        }
390
        this.#reset ( );
391
    }
392
393
    /**
394
    reset some variables after a touchend or touchcancel event
395
    */
396
397
    #reset ( ) {
398
        if ( this.#clonedListItemHTMLElement ) {
399
            document.body.removeChild ( this.#clonedListItemHTMLElement );
400
        }
401
        this.#clonedListItemHTMLElement = null;
402
        this.#scrolledContainerHTMLElement = null;
403
        this.#isDoubleClick = false;
404
        this.#lastScrollTimeStamp = ZERO;
405
        this.#sortableListHTMLElement = null;
406
        this.#topScrollPosition = ZERO;
407
        this.#bottomScrollPosition = ZERO;
408
        this.#touchY = ZERO;
409
        this.#dropTargetHTMLElement = null;
410
        this.#dropOnTop = true;
411
    }
412
413
    /**
414
    The constructor
415
    @param {function} dropFunction The function to call when an item is droped
416
    */
417
418
    constructor ( dropFunction ) {
419
        Object.freeze ( this );
420
        this.#dropFunction = dropFunction;
421
        this.#clonedListItemHTMLElement = null;
422
        this.#lastTouchStartTimeStamp = ZERO;
423
        this.#reset ( );
424
    }
425
426
    /**
427
    Event listener method
428
    @param {Event} touchEvent The event to handle
429
    */
430
431
    handleEvent ( touchEvent ) {
432
        switch ( touchEvent.type ) {
433
        case 'touchstart' :
434
            this.#handleStartEvent ( touchEvent );
435
            break;
436
        case 'touchmove' :
437
            this.#handleMoveEvent ( touchEvent );
438
            break;
439
        case 'touchend' :
440
            this.#handleEndEvent ( touchEvent );
441
            break;
442
        case 'touchcancel' :
443
            this.#reset ( );
444
            break;
445
        default :
446
            break;
447
        }
448
    }
449
}
450
451
export default TouchListItemEL;
452
453
/* --- End of file --------------------------------------------------------------------------------------------------------- */
454