@urbanstudio/ua-sortable 1.0.0 → 1.0.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.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,18 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
7
7
 
8
8
  ---
9
9
 
10
+ ## [1.0.1] — 2026-06-05
11
+
12
+ ### Fixed
13
+ - Drag direction detection: `direction: "auto"` now checks `display:flex/inline-flex` before reading `flex-direction` — previously non-flex containers were incorrectly detected as `horizontal` (CSS default value for `flex-direction` is `row` even on block elements)
14
+ - Placeholder now visible during drag as a dashed drop-indicator box (was `visibility:hidden`)
15
+ - `setPointerCapture` is now called before `pointer-events:none` is applied — fixes drag-down failing in some browsers
16
+ - Text selection during drag suppressed via `document.body.style.userSelect = "none"`
17
+ - Cross-frame compatibility: `instanceof HTMLElement` replaced with `nodeType === 1` check
18
+ - Original element is now used as its own drag ghost (no `cloneNode`) — renders with all inherited CSS intact
19
+
20
+ ---
21
+
10
22
  ## [1.0.0] — 2026-06-05
11
23
 
12
24
  ### Added
@@ -38,13 +38,15 @@
38
38
  #containerElement = null;
39
39
  #options = {};
40
40
  #draggableElements = [];
41
- #ghostElement = null;
42
41
  #draggedElement = null;
43
42
  #placeholderElement = null;
44
43
  #sourceContainerElement = null;
45
44
  #currentContainerElement = null;
46
45
  #pointerStartX = 0;
47
46
  #pointerStartY = 0;
47
+ #dragOffsetX = 0;
48
+ #dragOffsetY = 0;
49
+ #savedDragStyle = "";
48
50
  #delayTimer = null;
49
51
  #isDragging = false;
50
52
  #childObserver = null;
@@ -54,7 +56,7 @@
54
56
  #boundHandlePointerCancel = null;
55
57
 
56
58
  constructor(containerElement, options = {}) {
57
- if (!(containerElement instanceof HTMLElement)) {
59
+ if (!containerElement || containerElement.nodeType !== 1) {
58
60
  throw new Error("UA_Sortable: first argument must be an HTMLElement");
59
61
  }
60
62
  if (UA_Sortable.#instanceRegistry.has(containerElement)) {
@@ -212,17 +214,18 @@
212
214
  this.#sourceContainerElement = this.#containerElement;
213
215
  this.#currentContainerElement = this.#containerElement;
214
216
  const r = dragged.getBoundingClientRect();
215
- this.#ghostElement = dragged.cloneNode(true);
216
- this.#ghostElement.classList.add(this.#options.ghostClass);
217
- this.#ghostElement.style.cssText = `position:fixed;left:${r.left}px;top:${r.top}px;width:${r.width}px;height:${r.height}px;margin:0;pointer-events:none;z-index:9999;`;
218
- document.body.appendChild(this.#ghostElement);
217
+ this.#dragOffsetX = e.clientX - r.left;
218
+ this.#dragOffsetY = e.clientY - r.top;
219
219
  this.#placeholderElement = document.createElement(dragged.tagName);
220
- this.#placeholderElement.style.cssText = `width:${r.width}px;height:${r.height}px;opacity:0;pointer-events:none;`;
220
+ this.#placeholderElement.classList.add("ua-sortable-placeholder");
221
+ this.#placeholderElement.style.cssText = `width:${r.width}px;height:${r.height}px;pointer-events:none;`;
221
222
  dragged.parentNode.insertBefore(this.#placeholderElement, dragged);
223
+ try { dragged.setPointerCapture(e.pointerId); } catch (_) {}
224
+ this.#savedDragStyle = dragged.style.cssText;
225
+ dragged.style.cssText = `position:fixed;left:${r.left}px;top:${r.top}px;width:${r.width}px;margin:0;z-index:9999;pointer-events:none;`;
222
226
  dragged.classList.add(this.#options.dragClass);
223
- dragged.style.opacity = "0.001";
224
227
  this.#containerElement.classList.add("ua-sortable-active");
225
- try { dragged.setPointerCapture(e.pointerId); } catch (_) {}
228
+ document.body.style.userSelect = "none";
226
229
  document.addEventListener("pointermove", this.#boundHandlePointerMove);
227
230
  document.addEventListener("pointerup", this.#boundHandlePointerUp);
228
231
  document.addEventListener("pointercancel", this.#boundHandlePointerCancel);
@@ -230,10 +233,8 @@
230
233
  }
231
234
  #handlePointerMove(e) {
232
235
  if (!this.#isDragging) return;
233
- const dx = e.clientX - this.#pointerStartX, dy = e.clientY - this.#pointerStartY;
234
- const or = this.#draggedElement.getBoundingClientRect();
235
- this.#ghostElement.style.left = `${or.left + dx}px`;
236
- this.#ghostElement.style.top = `${or.top + dy}px`;
236
+ this.#draggedElement.style.left = `${e.clientX - this.#dragOffsetX}px`;
237
+ this.#draggedElement.style.top = `${e.clientY - this.#dragOffsetY}px`;
237
238
  const tc = this.#findTargetContainer(e.clientX, e.clientY);
238
239
  if (tc && tc !== this.#currentContainerElement) {
239
240
  this.#currentContainerElement.classList.remove("ua-sortable-active");
@@ -281,28 +282,28 @@
281
282
  return await Promise.resolve(this.#options.onMove(id, from, to, fromIds, toIds)) !== false;
282
283
  }
283
284
  #cleanupDragState() {
284
- this.#ghostElement?.remove();
285
285
  this.#placeholderElement?.remove();
286
286
  if (this.#draggedElement) {
287
+ this.#draggedElement.style.cssText = this.#savedDragStyle;
287
288
  this.#draggedElement.classList.remove(this.#options.dragClass);
288
- this.#draggedElement.style.opacity = "";
289
289
  }
290
290
  this.#containerElement.classList.remove("ua-sortable-active");
291
291
  if (this.#currentContainerElement && this.#currentContainerElement !== this.#containerElement) {
292
292
  this.#currentContainerElement.classList.remove("ua-sortable-active");
293
293
  }
294
294
  clearTimeout(this.#delayTimer);
295
+ document.body.style.userSelect = "";
295
296
  document.removeEventListener("pointermove", this.#boundHandlePointerMove);
296
297
  document.removeEventListener("pointerup", this.#boundHandlePointerUp);
297
298
  document.removeEventListener("pointercancel", this.#boundHandlePointerCancel);
298
- this.#ghostElement = this.#placeholderElement = this.#draggedElement =
299
+ this.#placeholderElement = this.#draggedElement =
299
300
  this.#sourceContainerElement = this.#currentContainerElement = this.#delayTimer = null;
300
301
  this.#isDragging = false;
301
302
  }
302
303
  #updatePlaceholderPosition(px, py) {
303
304
  const tc = this.#currentContainerElement;
304
305
  const children = [...tc.children].filter(c =>
305
- c !== this.#draggedElement && c !== this.#ghostElement &&
306
+ c !== this.#draggedElement &&
306
307
  c !== this.#placeholderElement &&
307
308
  (!this.#options.filter || !c.matches(this.#options.filter))
308
309
  );
@@ -317,8 +318,11 @@
317
318
  }
318
319
  #resolveDirection(el) {
319
320
  if (this.#options.direction !== "auto") return this.#options.direction;
320
- const fd = getComputedStyle(el).flexDirection;
321
- return (fd === "row" || fd === "row-reverse") ? "horizontal" : "vertical";
321
+ const style = getComputedStyle(el);
322
+ if (style.display === "flex" || style.display === "inline-flex") {
323
+ return (style.flexDirection === "row" || style.flexDirection === "row-reverse") ? "horizontal" : "vertical";
324
+ }
325
+ return "vertical";
322
326
  }
323
327
  #findDraggableParent(el) {
324
328
  let c = el;
@@ -344,7 +348,7 @@
344
348
  if (typeof document !== "undefined" && !document.getElementById("ua-sortable-styles")) {
345
349
  const s = document.createElement("style");
346
350
  s.id = "ua-sortable-styles";
347
- s.textContent = ".ua-sortable-ghost{opacity:.4;}.ua-sortable-drag{opacity:.4;}.ua-sortable-active>.ua-sortable-over{border-top:2px solid var(--accent,#2563eb);}.ua-drag-handle{cursor:grab;touch-action:none;}.ua-drag-handle:active{cursor:grabbing;}";
351
+ s.textContent = ".ua-sortable-drag{opacity:.95;box-shadow:0 8px 24px rgba(0,0,0,.18);transition:box-shadow .15s;}.ua-sortable-placeholder{border:2px dashed rgba(0,0,0,.18);border-radius:3px;box-sizing:border-box;background:rgba(0,0,0,.03);}.ua-sortable-active>.ua-sortable-over{border-top:2px solid var(--accent,#2563eb);}.ua-drag-handle{cursor:grab;touch-action:none;}.ua-drag-handle:active{cursor:grabbing;}";
348
352
  document.head.appendChild(s);
349
353
  }
350
354
 
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "@urbanstudio/ua-sortable",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "Pointer-Events-based drag-and-drop sorting for lists and grids. No dependencies.",
5
5
  "author": "Marian Feiler <mf@urbanstudio.de> (https://urbanstudio.de)",
6
6
  "license": "MIT",
7
7
  "homepage": "https://github.com/urbanstudioGmbH/ua-sortable#readme",
8
8
  "repository": {
9
9
  "type": "git",
10
- "url": "https://github.com/urbanstudioGmbH/ua-sortable.git"
10
+ "url": "git+https://github.com/urbanstudioGmbH/ua-sortable.git"
11
11
  },
12
12
  "bugs": {
13
13
  "url": "https://github.com/urbanstudioGmbH/ua-sortable/issues"
package/src/Sortable.js CHANGED
@@ -21,13 +21,15 @@ export class UA_Sortable {
21
21
  #containerElement = null;
22
22
  #options = {};
23
23
  #draggableElements = [];
24
- #ghostElement = null;
25
24
  #draggedElement = null;
26
25
  #placeholderElement = null;
27
26
  #sourceContainerElement = null;
28
27
  #currentContainerElement = null;
29
28
  #pointerStartX = 0;
30
29
  #pointerStartY = 0;
30
+ #dragOffsetX = 0;
31
+ #dragOffsetY = 0;
32
+ #savedDragStyle = "";
31
33
  #delayTimer = null;
32
34
  #isDragging = false;
33
35
  #childObserver = null;
@@ -43,7 +45,7 @@ export class UA_Sortable {
43
45
  * @param {object} options
44
46
  */
45
47
  constructor(containerElement, options = {}) {
46
- if (!(containerElement instanceof HTMLElement)) {
48
+ if (!containerElement || containerElement.nodeType !== 1) {
47
49
  throw new Error("UA_Sortable: first argument must be an HTMLElement");
48
50
  }
49
51
  if (UA_Sortable.#instanceRegistry.has(containerElement)) {
@@ -309,35 +311,36 @@ export class UA_Sortable {
309
311
  this.#currentContainerElement = this.#containerElement;
310
312
 
311
313
  const boundingRect = draggedElement.getBoundingClientRect();
312
- this.#ghostElement = draggedElement.cloneNode(true);
313
- this.#ghostElement.classList.add(this.#options.ghostClass);
314
- this.#ghostElement.style.cssText = [
315
- "position:fixed",
316
- `left:${boundingRect.left}px`,
317
- `top:${boundingRect.top}px`,
318
- `width:${boundingRect.width}px`,
319
- `height:${boundingRect.height}px`,
320
- "margin:0",
321
- "pointer-events:none",
322
- "z-index:9999",
323
- ].join(";");
324
- document.body.appendChild(this.#ghostElement);
325
314
 
315
+ this.#dragOffsetX = pointerEvent.clientX - boundingRect.left;
316
+ this.#dragOffsetY = pointerEvent.clientY - boundingRect.top;
317
+
318
+ // Placeholder keeps layout space — visible drop indicator
326
319
  this.#placeholderElement = document.createElement(draggedElement.tagName);
320
+ this.#placeholderElement.classList.add("ua-sortable-placeholder");
327
321
  this.#placeholderElement.style.cssText = [
328
322
  `width:${boundingRect.width}px`,
329
323
  `height:${boundingRect.height}px`,
330
- "opacity:0",
331
324
  "pointer-events:none",
332
325
  ].join(";");
333
326
  draggedElement.parentNode.insertBefore(this.#placeholderElement, draggedElement);
334
327
 
328
+ try { draggedElement.setPointerCapture(pointerEvent.pointerId); } catch (_) {}
329
+
330
+ this.#savedDragStyle = draggedElement.style.cssText;
331
+ draggedElement.style.cssText = [
332
+ "position:fixed",
333
+ `left:${boundingRect.left}px`,
334
+ `top:${boundingRect.top}px`,
335
+ `width:${boundingRect.width}px`,
336
+ "margin:0",
337
+ "z-index:9999",
338
+ "pointer-events:none",
339
+ ].join(";");
335
340
  draggedElement.classList.add(this.#options.dragClass);
336
- draggedElement.style.opacity = "0.001";
337
341
 
338
342
  this.#containerElement.classList.add("ua-sortable-active");
339
-
340
- try { draggedElement.setPointerCapture(pointerEvent.pointerId); } catch (_) {}
343
+ document.body.style.userSelect = "none";
341
344
 
342
345
  document.addEventListener("pointermove", this.#boundHandlePointerMove);
343
346
  document.addEventListener("pointerup", this.#boundHandlePointerUp);
@@ -349,12 +352,8 @@ export class UA_Sortable {
349
352
  #handlePointerMove(pointerEvent) {
350
353
  if (!this.#isDragging) return;
351
354
 
352
- const deltaX = pointerEvent.clientX - this.#pointerStartX;
353
- const deltaY = pointerEvent.clientY - this.#pointerStartY;
354
-
355
- const originalRect = this.#draggedElement.getBoundingClientRect();
356
- this.#ghostElement.style.left = `${originalRect.left + deltaX}px`;
357
- this.#ghostElement.style.top = `${originalRect.top + deltaY}px`;
355
+ this.#draggedElement.style.left = `${pointerEvent.clientX - this.#dragOffsetX}px`;
356
+ this.#draggedElement.style.top = `${pointerEvent.clientY - this.#dragOffsetY}px`;
358
357
 
359
358
  const targetContainer = this.#findTargetContainer(pointerEvent.clientX, pointerEvent.clientY);
360
359
  if (targetContainer && targetContainer !== this.#currentContainerElement) {
@@ -454,12 +453,11 @@ export class UA_Sortable {
454
453
  }
455
454
 
456
455
  #cleanupDragState() {
457
- this.#ghostElement?.remove();
458
456
  this.#placeholderElement?.remove();
459
457
 
460
458
  if (this.#draggedElement) {
459
+ this.#draggedElement.style.cssText = this.#savedDragStyle;
461
460
  this.#draggedElement.classList.remove(this.#options.dragClass);
462
- this.#draggedElement.style.opacity = "";
463
461
  }
464
462
 
465
463
  this.#containerElement.classList.remove("ua-sortable-active");
@@ -468,11 +466,11 @@ export class UA_Sortable {
468
466
  }
469
467
 
470
468
  clearTimeout(this.#delayTimer);
469
+ document.body.style.userSelect = "";
471
470
  document.removeEventListener("pointermove", this.#boundHandlePointerMove);
472
471
  document.removeEventListener("pointerup", this.#boundHandlePointerUp);
473
472
  document.removeEventListener("pointercancel", this.#boundHandlePointerCancel);
474
473
 
475
- this.#ghostElement = null;
476
474
  this.#placeholderElement = null;
477
475
  this.#draggedElement = null;
478
476
  this.#sourceContainerElement = null;
@@ -489,7 +487,6 @@ export class UA_Sortable {
489
487
  const targetContainer = this.#currentContainerElement;
490
488
  const children = [...targetContainer.children].filter(child =>
491
489
  child !== this.#draggedElement &&
492
- child !== this.#ghostElement &&
493
490
  child !== this.#placeholderElement &&
494
491
  (!this.#options.filter || !child.matches(this.#options.filter))
495
492
  );
@@ -519,10 +516,13 @@ export class UA_Sortable {
519
516
 
520
517
  #resolveDirection(containerElement) {
521
518
  if (this.#options.direction !== "auto") return this.#options.direction;
522
- const flexDirection = getComputedStyle(containerElement).flexDirection;
523
- return (flexDirection === "row" || flexDirection === "row-reverse")
524
- ? "horizontal"
525
- : "vertical";
519
+ const style = getComputedStyle(containerElement);
520
+ if (style.display === "flex" || style.display === "inline-flex") {
521
+ return (style.flexDirection === "row" || style.flexDirection === "row-reverse")
522
+ ? "horizontal"
523
+ : "vertical";
524
+ }
525
+ return "vertical";
526
526
  }
527
527
 
528
528
  #findDraggableParent(targetElement) {
@@ -563,8 +563,8 @@ export class UA_Sortable {
563
563
  const style = document.createElement("style");
564
564
  style.id = "ua-sortable-styles";
565
565
  style.textContent = [
566
- ".ua-sortable-ghost{opacity:.4;}",
567
- ".ua-sortable-drag{opacity:.4;}",
566
+ ".ua-sortable-drag{opacity:.95;box-shadow:0 8px 24px rgba(0,0,0,.18);transition:box-shadow .15s;}",
567
+ ".ua-sortable-placeholder{border:2px dashed rgba(0,0,0,.18);border-radius:3px;box-sizing:border-box;background:rgba(0,0,0,.03);}",
568
568
  ".ua-sortable-active>.ua-sortable-over{border-top:2px solid var(--accent,#2563eb);}",
569
569
  ".ua-drag-handle{cursor:grab;touch-action:none;}",
570
570
  ".ua-drag-handle:active{cursor:grabbing;}",