tiny-essentials 1.12.0 → 1.12.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -17,6 +17,7 @@ class TinyDragger {
17
17
  #destroyed = false;
18
18
  #offsetY = 0;
19
19
  #offsetX = 0;
20
+ #multiCollision = false;
20
21
  #lockInsideJail = false;
21
22
  #revertOnDrop = false;
22
23
  #dragging = false;
@@ -52,6 +53,7 @@ class TinyDragger {
52
53
  * @param {string} [options.classDragCollision='dragging-collision'] - CSS class applied to collision element.
53
54
  * @param {boolean} [options.lockInsideJail=false] - Restrict movement within the jail container.
54
55
  * @param {boolean} [options.dropInJailOnly=false] - Restrict drop within the jail container.
56
+ * @param {boolean} [options.multiCollision=false] - Enables returning multiple collided elements.
55
57
  * @param {VibrationPatterns|false} [options.vibration=false] - Vibration feedback configuration.
56
58
  * @param {boolean} [options.revertOnDrop=false] - Whether to return to original position on drop.
57
59
  * @param {string} [options.classHidden='drag-hidden'] - CSS class to hide original element during dragging.
@@ -90,6 +92,7 @@ class TinyDragger {
90
92
  validateBoolean(options.lockInsideJail, 'lockInsideJail');
91
93
  validateBoolean(options.dropInJailOnly, 'dropInJailOnly');
92
94
  validateBoolean(options.revertOnDrop, 'revertOnDrop');
95
+ validateBoolean(options.multiCollision, 'multiCollision');
93
96
  validateString(options.classDragging, 'classDragging');
94
97
  validateString(options.classBodyDragging, 'classBodyDragging');
95
98
  validateString(options.classJailDragging, 'classJailDragging');
@@ -121,6 +124,8 @@ class TinyDragger {
121
124
  this.#lockInsideJail = options.lockInsideJail;
122
125
  if (typeof options.dropInJailOnly === 'boolean')
123
126
  this.#dropInJailOnly = options.dropInJailOnly;
127
+ if (typeof options.multiCollision === 'boolean')
128
+ this.#multiCollision = options.multiCollision;
124
129
  /** @private */
125
130
  this._onMouseDown = this.#startDrag.bind(this);
126
131
  /** @private */
@@ -282,25 +287,60 @@ class TinyDragger {
282
287
  this.checkDragCollision(event);
283
288
  this.#dispatchEvent('drag');
284
289
  }
290
+ /** @type {HTMLElement[]} */
291
+ #collisionsMarked = [];
292
+ /**
293
+ * Marks an element as currently collided by adding the collision CSS class.
294
+ * The element is stored in an internal list for easy removal later.
295
+ *
296
+ * @param {HTMLElement|null} el - The element to mark as collided.
297
+ */
298
+ #addCollision(el) {
299
+ if (!el)
300
+ return;
301
+ el.classList.add(this.#classDragCollision);
302
+ this.#collisionsMarked.push(el);
303
+ }
304
+ /**
305
+ * Removes the collision CSS class from all previously marked elements.
306
+ * Also clears the last single collision element, if set.
307
+ *
308
+ */
309
+ #removeCollision() {
310
+ while (this.#collisionsMarked.length > 0) {
311
+ const el = this.#collisionsMarked.shift();
312
+ if (el)
313
+ el.classList.remove(this.#classDragCollision);
314
+ }
315
+ if (!this.#lastCollision)
316
+ return;
317
+ this.#lastCollision.classList.remove(this.#classDragCollision);
318
+ }
285
319
  /**
286
320
  * Handles dragging collision.
287
321
  * @param {MouseEvent|Touch} event - The drag event.
288
322
  */
289
323
  checkDragCollision(event) {
290
- const { collidedElement: collided } = this.execCollision(event);
291
- if (collided && collided !== this.#lastCollision) {
292
- if (navigator.vibrate && Array.isArray(this.#vibration.collide)) {
293
- navigator.vibrate(this.#vibration.collide);
294
- }
295
- this.#lastCollision = collided;
296
- if (this.#lastCollision)
297
- this.#lastCollision.classList.add(this.#classDragCollision);
324
+ const { collidedElements } = this.execCollision(event);
325
+ const first = collidedElements[0] || null;
326
+ // Removes old marking if necessary
327
+ if (this.#lastCollision && !collidedElements.includes(this.#lastCollision)) {
328
+ this.#removeCollision();
298
329
  }
299
- else if (!collided) {
300
- if (this.#lastCollision)
301
- this.#lastCollision.classList.remove(this.#classDragCollision);
302
- this.#lastCollision = null;
330
+ // Adds Marking for All Colluded
331
+ collidedElements.forEach((el) => this.#addCollision(el));
332
+ // Removes markings from who no longer collided
333
+ this.#collidables.forEach((el) => {
334
+ if (!collidedElements.includes(el)) {
335
+ el.classList.remove(this.#classDragCollision);
336
+ }
337
+ });
338
+ if (navigator.vibrate &&
339
+ Array.isArray(this.#vibration.collide) &&
340
+ collidedElements.length > 0) {
341
+ navigator.vibrate(this.#vibration.collide);
303
342
  }
343
+ this.#lastCollision = first;
304
344
  }
305
345
  /**
306
346
  * Handles dragging movement.
@@ -338,12 +378,12 @@ class TinyDragger {
338
378
  /**
339
379
  * Handles the collision of a drag.
340
380
  * @param {MouseEvent|Touch} event - The release event.
341
- * @returns {{ inJail: boolean; collidedElement: HTMLElement|null }}
381
+ * @returns {{ inJail: boolean; collidedElements: (HTMLElement | null)[] }}
342
382
  */
343
383
  execCollision(event) {
344
384
  if (this.#destroyed || !this.#dragProxy)
345
- return { inJail: false, collidedElement: null };
346
- let collidedElement;
385
+ return { inJail: false, collidedElements: [] };
386
+ let collidedElements = [];
347
387
  let inJail = true;
348
388
  const jailRect = this.#jail?.getBoundingClientRect();
349
389
  if (this.#collisionByMouse) {
@@ -353,20 +393,28 @@ class TinyDragger {
353
393
  inJail =
354
394
  x >= jailRect.left && x <= jailRect.right && y >= jailRect.top && y <= jailRect.bottom;
355
395
  }
356
- collidedElement = inJail ? this.getCollidedElement(x, y) : null;
396
+ collidedElements = inJail
397
+ ? this.#multiCollision
398
+ ? this.getAllCollidedElements(x, y)
399
+ : [this.getCollidedElement(x, y)].filter(Boolean)
400
+ : [];
357
401
  }
358
402
  else {
359
- const targetRect = this.#dragProxy.getBoundingClientRect();
403
+ const rect = this.#dragProxy.getBoundingClientRect();
360
404
  if (this.#dropInJailOnly && this.#jail && jailRect) {
361
405
  inJail =
362
- targetRect.left >= jailRect.left &&
363
- targetRect.right <= jailRect.right &&
364
- targetRect.top >= jailRect.top &&
365
- targetRect.bottom <= jailRect.bottom;
406
+ rect.left >= jailRect.left &&
407
+ rect.right <= jailRect.right &&
408
+ rect.top >= jailRect.top &&
409
+ rect.bottom <= jailRect.bottom;
366
410
  }
367
- collidedElement = inJail ? this.getCollidedElementByRect(targetRect) : null;
411
+ collidedElements = inJail
412
+ ? this.#multiCollision
413
+ ? this.getAllCollidedElementsByRect(rect)
414
+ : [this.getCollidedElementByRect(rect)].filter(Boolean)
415
+ : [];
368
416
  }
369
- return { inJail, collidedElement };
417
+ return { inJail, collidedElements };
370
418
  }
371
419
  /**
372
420
  * Handles the end of a drag.
@@ -388,7 +436,7 @@ class TinyDragger {
388
436
  document.removeEventListener('mouseup', this._onMouseUp);
389
437
  document.removeEventListener('touchmove', this._onTouchMove);
390
438
  document.removeEventListener('touchend', this._onTouchEnd);
391
- const { collidedElement } = this.execCollision(event);
439
+ const { collidedElements } = this.execCollision(event);
392
440
  if (navigator.vibrate && Array.isArray(this.#vibration.end)) {
393
441
  navigator.vibrate(this.#vibration.end);
394
442
  }
@@ -399,7 +447,7 @@ class TinyDragger {
399
447
  this.#dragProxy = null;
400
448
  }
401
449
  if (this.#lastCollision)
402
- this.#lastCollision.classList.remove(this.#classDragCollision);
450
+ this.#removeCollision();
403
451
  this.#lastCollision = null;
404
452
  this.#target.classList.remove(this.#dragHiddenClass);
405
453
  if (!this.#revertOnDrop) {
@@ -408,11 +456,43 @@ class TinyDragger {
408
456
  }
409
457
  const dropEvent = new CustomEvent('drop', {
410
458
  detail: {
411
- target: collidedElement,
459
+ targets: collidedElements,
460
+ first: collidedElements[0] || null,
412
461
  },
413
462
  });
414
463
  this.#target.dispatchEvent(dropEvent);
415
464
  }
465
+ /**
466
+ * Checks if the provided element intersects with the given bounding rectangle.
467
+ *
468
+ * @param {HTMLElement} el - The element to test for collision.
469
+ * @param {DOMRect} rect - The bounding rectangle to check against.
470
+ * @returns {boolean} True if the element intersects with the rectangle.
471
+ */
472
+ #getCollidedElementByRect(el, rect) {
473
+ const elRect = el.getBoundingClientRect();
474
+ return !(rect.right < elRect.left ||
475
+ rect.left > elRect.right ||
476
+ rect.bottom < elRect.top ||
477
+ rect.top > elRect.bottom);
478
+ }
479
+ /**
480
+ * Returns all elements currently colliding with the given rectangle.
481
+ *
482
+ * @param {DOMRect} rect - Bounding rectangle of the dragged proxy.
483
+ * @returns {HTMLElement[]} A list of all collided elements.
484
+ * @throws {Error} If the input is not a valid DOMRect with numeric bounds.
485
+ */
486
+ getAllCollidedElementsByRect(rect) {
487
+ this.#checkDestroy();
488
+ if (!(rect instanceof DOMRect) ||
489
+ typeof rect.left !== 'number' ||
490
+ typeof rect.right !== 'number' ||
491
+ typeof rect.top !== 'number' ||
492
+ typeof rect.bottom !== 'number')
493
+ throw new Error('getCollidedElementByRect expects a valid DOMRect object.');
494
+ return this.#collidables.filter((el) => this.#getCollidedElementByRect(el, rect));
495
+ }
416
496
  /**
417
497
  * Detects collision based on rectangle intersection.
418
498
  * @param {DOMRect} rect - Bounding rectangle of the dragged proxy.
@@ -427,13 +507,30 @@ class TinyDragger {
427
507
  typeof rect.top !== 'number' ||
428
508
  typeof rect.bottom !== 'number')
429
509
  throw new Error('getCollidedElementByRect expects a valid DOMRect object.');
430
- return (this.#collidables.find((el) => {
431
- const elRect = el.getBoundingClientRect();
432
- return !(rect.right < elRect.left ||
433
- rect.left > elRect.right ||
434
- rect.bottom < elRect.top ||
435
- rect.top > elRect.bottom);
436
- }) || null);
510
+ return this.#collidables.find((el) => this.#getCollidedElementByRect(el, rect)) || null;
511
+ }
512
+ /**
513
+ * Checks whether a given (x, y) coordinate is inside the bounding rectangle of an element.
514
+ *
515
+ * @param {HTMLElement} el - The element to test for collision.
516
+ * @param {number} x - Horizontal screen coordinate.
517
+ * @param {number} y - Vertical screen coordinate.
518
+ * @returns {boolean} True if the point is within the element's bounds.
519
+ */
520
+ #getCollidedElement(el, x, y) {
521
+ const rect = el.getBoundingClientRect();
522
+ return x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom;
523
+ }
524
+ /**
525
+ * @param {number} x - Horizontal screen coordinate.
526
+ * @param {number} y - Vertical screen coordinate.
527
+ * @returns {HTMLElement[]} The collided element or null.
528
+ */
529
+ getAllCollidedElements(x, y) {
530
+ this.#checkDestroy();
531
+ if (typeof x !== 'number' || typeof y !== 'number')
532
+ throw new Error('getCollidedElement expects numeric x and y coordinates.');
533
+ return this.#collidables.filter((el) => this.#getCollidedElement(el, x, y));
437
534
  }
438
535
  /**
439
536
  * Detects collision with a point using element bounding rectangles.
@@ -445,10 +542,7 @@ class TinyDragger {
445
542
  this.#checkDestroy();
446
543
  if (typeof x !== 'number' || typeof y !== 'number')
447
544
  throw new Error('getCollidedElement expects numeric x and y coordinates.');
448
- return (this.#collidables.find((el) => {
449
- const rect = el.getBoundingClientRect();
450
- return x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom;
451
- }) || null);
545
+ return this.#collidables.find((el) => this.#getCollidedElement(el, x, y)) || null;
452
546
  }
453
547
  /**
454
548
  * Dispatches a custom event from the target element.
@@ -671,7 +765,7 @@ class TinyDragger {
671
765
  document.removeEventListener('touchmove', this._onTouchMove);
672
766
  document.removeEventListener('touchend', this._onTouchEnd);
673
767
  if (this.#lastCollision)
674
- this.#lastCollision.classList.remove(this.#classDragCollision);
768
+ this.#removeCollision();
675
769
  if (this.#dragProxy) {
676
770
  this.#dragProxy.remove();
677
771
  this.#dragProxy = null;
@@ -54,6 +54,7 @@ new TinyDragger(targetElement, options?)
54
54
  | `classHidden` | `string` | `'drag-hidden'` | Class used to hide the original element while dragging |
55
55
  | `lockInsideJail` | `boolean` | `false` | Prevent drag from exceeding jail bounds |
56
56
  | `dropInJailOnly` | `boolean` | `false` | Prevent drop outside the jail area |
57
+ | `multiCollision` | `boolean` | `false` | Enables returning multiple collided elements |
57
58
  | `vibration` | `VibrationPatterns` or `false` | `false` | Vibration feedback configuration |
58
59
  | `revertOnDrop` | `boolean` | `false` | Return to original position after dropping |
59
60
 
@@ -133,6 +134,13 @@ Each pattern must be either `false` or an array of numbers.
133
134
 
134
135
  ---
135
136
 
137
+ ### `getAllCollidedElementsByRect(rect: DOMRect): HTMLElement[]`
138
+
139
+ 📌 Returns **all elements** currently intersecting the given rectangle. Useful for detecting multiple overlaps when dragging.
140
+ 🛑 Throws if `rect` is not a valid `DOMRect` with numeric `left`, `right`, `top`, and `bottom` properties.
141
+
142
+ ---
143
+
136
144
  ### `getCollidedElement(x: number, y: number): HTMLElement | null`
137
145
 
138
146
  📌 Detects if the given screen coordinates collide with any tracked element.
@@ -140,6 +148,13 @@ Each pattern must be either `false` or an array of numbers.
140
148
 
141
149
  ---
142
150
 
151
+ ### `getAllCollidedElements(x: number, y: number): HTMLElement[]`
152
+
153
+ 📌 Detects **all elements** currently under the given screen coordinates. Works well when `collisionByMouse` is enabled.
154
+ 🛑 Throws if `x` or `y` is not a number.
155
+
156
+ ---
157
+
143
158
  ### `getDragging(): boolean`
144
159
 
145
160
  🔄 Returns whether dragging is currently active.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tiny-essentials",
3
- "version": "1.12.0",
3
+ "version": "1.12.1",
4
4
  "description": "Collection of small, essential scripts designed to be used across various projects. These simple utilities are crafted for speed, ease of use, and versatility.",
5
5
  "scripts": {
6
6
  "test": "npm run test:mjs && npm run test:cjs && npm run test:js",