@mk-drag-and-drop/dom 0.4.3 → 0.4.5

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/README.md CHANGED
@@ -374,9 +374,14 @@ sortable axis: above or left of midpoint places before, while below or right of
374
374
  midpoint places after. Pointer-based targeting uses the pointer as that position;
375
375
  rect-based targeting such as `centerToCenter` uses the overlay center.
376
376
 
377
- `placementBoundary` is used after a preview placement already exists to control
378
- same-target reversal and hysteresis thresholds. It does not decide the initial
379
- side for cross-container sortable entry.
377
+ A container-only preview keeps that cross-container entry state through active
378
+ recomputes until the first item target in the destination list receives a
379
+ before/after placement.
380
+
381
+ `placementBoundary` is used after a preview placement already has an established
382
+ movement direction to control same-target reversal and hysteresis thresholds. It
383
+ does not decide the initial side for cross-container sortable entry or delay the
384
+ first movement that follows midpoint-based entry into a list.
380
385
 
381
386
  ## Modifiers
382
387
 
@@ -387,7 +392,10 @@ Modifiers transform pointer movement before drag state updates. Built-ins:
387
392
  - `restrictToContainer(resolver)`
388
393
 
389
394
  `restrictToContainer` receives a resolver function with
390
- `DragModifierSetupInput` and returns an `HTMLElement | null`.
395
+ `DragModifierSetupInput` and returns an `HTMLElement | null`. The resolver runs
396
+ at drag setup to choose the bounds element; the selected element's rect is
397
+ remeasured during movement so growing or shrinking containers keep constraining
398
+ against current bounds.
391
399
 
392
400
  Custom modifiers can provide:
393
401
 
@@ -1,4 +1,5 @@
1
1
  import { rectToDragRect } from "../geometry/rects.js";
2
+ const restrictToContainerElements = new WeakMap();
2
3
  export function lockToXAxis() {
3
4
  return {
4
5
  transform: (input) => ({
@@ -19,33 +20,43 @@ export function restrictToContainer(getContainer) {
19
20
  return {
20
21
  setup: (input) => {
21
22
  const container = getContainer(input);
22
- return container
23
- ? rectToDragRect(container.getBoundingClientRect())
24
- : null;
23
+ if (!container) {
24
+ return null;
25
+ }
26
+ const rect = rectToDragRect(container.getBoundingClientRect());
27
+ restrictToContainerElements.set(rect, container);
28
+ return rect;
25
29
  },
26
30
  transform: (input) => {
27
31
  if (input.state === null) {
28
32
  return input.pointerPosition;
29
33
  }
34
+ const containerRect = getRestrictToContainerRect(input.state);
30
35
  return {
31
36
  x: clampPointerAxis({
32
37
  pointerPosition: input.pointerPosition.x,
33
38
  overlayStart: input.overlayRect.left,
34
39
  overlayEnd: input.overlayRect.right,
35
- containerStart: input.state.left,
36
- containerEnd: input.state.right,
40
+ containerStart: containerRect.left,
41
+ containerEnd: containerRect.right,
37
42
  }),
38
43
  y: clampPointerAxis({
39
44
  pointerPosition: input.pointerPosition.y,
40
45
  overlayStart: input.overlayRect.top,
41
46
  overlayEnd: input.overlayRect.bottom,
42
- containerStart: input.state.top,
43
- containerEnd: input.state.bottom,
47
+ containerStart: containerRect.top,
48
+ containerEnd: containerRect.bottom,
44
49
  }),
45
50
  };
46
51
  },
47
52
  };
48
53
  }
54
+ function getRestrictToContainerRect(state) {
55
+ const container = restrictToContainerElements.get(state);
56
+ return container
57
+ ? rectToDragRect(container.getBoundingClientRect())
58
+ : state;
59
+ }
49
60
  function clampPointerAxis(input) {
50
61
  const overlaySize = input.overlayEnd - input.overlayStart;
51
62
  const containerSize = input.containerEnd - input.containerStart;
@@ -235,6 +235,7 @@ export class DragRuntime {
235
235
  this.notifyDragUpdate(updateEvent, {
236
236
  ...updateEvent,
237
237
  placementPosition: sortablePlacementPosition,
238
+ movementPosition: rawPointerPosition,
238
239
  });
239
240
  }
240
241
  updatePointerNowOrRelease(rawPointerPosition) {
@@ -465,6 +466,7 @@ export class DragRuntime {
465
466
  pointerPosition: effectivePointerPosition,
466
467
  overlayRect: startOverlayRect,
467
468
  }),
469
+ movementPosition: rawPointerPosition,
468
470
  };
469
471
  this.notifyDragStartSubscriptions(dragStartSubscriptionEvent);
470
472
  this.updateOverlayHost({
@@ -7,6 +7,10 @@ export type SortablePointerMovementState = {
7
7
  x: number;
8
8
  y: number;
9
9
  };
10
+ previousPlacementPosition?: {
11
+ x: number;
12
+ y: number;
13
+ };
10
14
  };
11
15
  export type SortablePreviewPlacementState = {
12
16
  activeDropTargetId: string | null;
@@ -26,10 +30,16 @@ export declare function isSortablePreviewTarget(input: {
26
30
  export declare function initializeSortablePointerMovement(registry: SortableRegistry, pointerPosition: {
27
31
  x: number;
28
32
  y: number;
33
+ }, placementPosition?: {
34
+ x: number;
35
+ y: number;
29
36
  }): void;
30
37
  export declare function updateSortablePointerMovement(registry: SortableRegistry, pointerPosition: {
31
38
  x: number;
32
39
  y: number;
40
+ }, placementPosition?: {
41
+ x: number;
42
+ y: number;
33
43
  }): void;
34
44
  export declare function clearSortablePointerMovement(registry: SortableRegistry): void;
35
45
  export declare function clearSortablePreviewPlacement(registry: SortableRegistry): void;
@@ -42,6 +52,10 @@ export declare function moveSortablePreview(input: {
42
52
  x: number;
43
53
  y: number;
44
54
  };
55
+ movementPosition: {
56
+ x: number;
57
+ y: number;
58
+ };
45
59
  options: NormalizedSortableOptions;
46
60
  }): void;
47
61
  export declare function getSortablePreviewPlacement(input: {
@@ -51,6 +65,10 @@ export declare function getSortablePreviewPlacement(input: {
51
65
  x: number;
52
66
  y: number;
53
67
  };
68
+ movementPosition?: {
69
+ x: number;
70
+ y: number;
71
+ };
54
72
  movement: SortablePointerMovementState | null;
55
73
  previewPlacement: SortablePreviewPlacementState | null;
56
74
  options: NormalizedSortableOptions;
@@ -17,19 +17,21 @@ export function snapshotSortableElement(registry, draggableId) {
17
17
  export function isSortablePreviewTarget(input) {
18
18
  return input.activeDropTargetId !== null;
19
19
  }
20
- export function initializeSortablePointerMovement(registry, pointerPosition) {
20
+ export function initializeSortablePointerMovement(registry, pointerPosition, placementPosition = pointerPosition) {
21
21
  registry.activeDrag.pointerMovement = {
22
22
  previousPointerPosition: { ...pointerPosition },
23
+ previousPlacementPosition: { ...placementPosition },
23
24
  };
24
25
  registry.activeDrag.previewPlacement = null;
25
26
  }
26
- export function updateSortablePointerMovement(registry, pointerPosition) {
27
+ export function updateSortablePointerMovement(registry, pointerPosition, placementPosition = pointerPosition) {
27
28
  const movement = registry.activeDrag.pointerMovement;
28
29
  if (!movement) {
29
- initializeSortablePointerMovement(registry, pointerPosition);
30
+ initializeSortablePointerMovement(registry, pointerPosition, placementPosition);
30
31
  return;
31
32
  }
32
33
  movement.previousPointerPosition = { ...pointerPosition };
34
+ movement.previousPlacementPosition = { ...placementPosition };
33
35
  }
34
36
  export function clearSortablePointerMovement(registry) {
35
37
  registry.activeDrag.pointerMovement = null;
@@ -75,10 +77,17 @@ export function moveSortablePreview(input) {
75
77
  addRegistrationContainerId(measurementIds, draggedRegistration);
76
78
  addRegistrationContainerId(measurementIds, target);
77
79
  if (target.capabilities.container) {
80
+ const sourceListElement = draggedElement.parentElement;
78
81
  measurementIds.add(target.id);
79
82
  const previousListElement = input.registry.activeDrag.previewPlacement?.containerElement ?? null;
80
83
  const pendingMidpointInitialPlacement = draggedElement.parentElement !== target.element ||
81
84
  (previousListElement !== null && previousListElement !== target.element);
85
+ if (!moveSortablePreviewIntoContainer({
86
+ draggedElement,
87
+ targetElement: target.element,
88
+ })) {
89
+ return;
90
+ }
82
91
  setSortablePreviewPlacementState({
83
92
  registry: input.registry,
84
93
  activeDropTargetId: target.id,
@@ -87,17 +96,18 @@ export function moveSortablePreview(input) {
87
96
  pendingMidpointInitialPlacement,
88
97
  movementDirection: getCurrentAxisMovementDirection({
89
98
  placementPosition: input.placementPosition,
99
+ movementPosition: input.movementPosition,
90
100
  movement: input.registry.activeDrag.pointerMovement,
91
101
  axis: input.options.axis,
92
102
  }),
93
103
  });
94
- if (!moveSortablePreviewIntoContainer({
95
- draggedElement,
96
- targetElement: target.element,
97
- })) {
98
- return;
104
+ if (sourceListElement && sourceListElement !== target.element) {
105
+ addSortableMeasurementIdsInContainer(measurementIds, input.registry, sourceListElement);
106
+ addSortableMeasurementIdsInContainer(measurementIds, input.registry, target.element);
107
+ }
108
+ else {
109
+ addSortableMeasurementIdsAroundElement(measurementIds, input.registry, draggedElement);
99
110
  }
100
- addSortableMeasurementIdsAroundElement(measurementIds, input.registry, draggedElement);
101
111
  refreshSortableDropTargetMeasurements({
102
112
  registry: input.registry,
103
113
  runtime: input.runtime,
@@ -108,6 +118,7 @@ export function moveSortablePreview(input) {
108
118
  }
109
119
  const targetElement = target.element;
110
120
  const listElement = targetElement.parentElement;
121
+ const sourceListElement = draggedElement.parentElement;
111
122
  if (!listElement) {
112
123
  return;
113
124
  }
@@ -129,6 +140,7 @@ export function moveSortablePreview(input) {
129
140
  activeDropTargetId,
130
141
  targetElement,
131
142
  placementPosition: input.placementPosition,
143
+ movementPosition: input.movementPosition,
132
144
  movement: input.registry.activeDrag.pointerMovement,
133
145
  previewPlacement: input.registry.activeDrag.previewPlacement,
134
146
  options: input.options,
@@ -156,8 +168,14 @@ export function moveSortablePreview(input) {
156
168
  else {
157
169
  targetElement.before(draggedElement);
158
170
  }
159
- addSortableMeasurementIdsAroundElement(measurementIds, input.registry, draggedElement);
160
- addSortableMeasurementIdsAroundElement(measurementIds, input.registry, targetElement);
171
+ if (sourceListElement && sourceListElement !== listElement) {
172
+ addSortableMeasurementIdsInContainer(measurementIds, input.registry, sourceListElement);
173
+ addSortableMeasurementIdsInContainer(measurementIds, input.registry, listElement);
174
+ }
175
+ else {
176
+ addSortableMeasurementIdsAroundElement(measurementIds, input.registry, draggedElement);
177
+ addSortableMeasurementIdsAroundElement(measurementIds, input.registry, targetElement);
178
+ }
161
179
  refreshSortableDropTargetMeasurements({
162
180
  registry: input.registry,
163
181
  runtime: input.runtime,
@@ -171,14 +189,14 @@ export function getSortablePreviewPlacement(input) {
171
189
  const targetRect = input.targetElement.getBoundingClientRect();
172
190
  const currentDirection = getCurrentAxisMovementDirection({
173
191
  placementPosition: input.placementPosition,
192
+ movementPosition: input.movementPosition,
174
193
  movement: input.movement,
175
194
  axis,
176
195
  });
177
196
  const previousPlacement = input.previewPlacement;
178
197
  if (!previousPlacement ||
179
198
  previousPlacement.activeDropTargetId !== input.activeDropTargetId ||
180
- previousPlacement.placement === null ||
181
- previousPlacement.movementDirection === "none") {
199
+ previousPlacement.placement === null) {
182
200
  if (input.useMidpointInitialPlacement) {
183
201
  const placement = getPlacementSideFromTargetMidpoint({
184
202
  axis,
@@ -187,7 +205,7 @@ export function getSortablePreviewPlacement(input) {
187
205
  });
188
206
  return {
189
207
  placement,
190
- movementDirection: getMovementDirectionFromPlacementSide(placement),
208
+ movementDirection: "none",
191
209
  };
192
210
  }
193
211
  return {
@@ -200,6 +218,32 @@ export function getSortablePreviewPlacement(input) {
200
218
  movementDirection: currentDirection,
201
219
  };
202
220
  }
221
+ if (previousPlacement.movementDirection === "none") {
222
+ if (currentDirection === "none") {
223
+ return {
224
+ placement: previousPlacement.placement,
225
+ movementDirection: previousPlacement.movementDirection,
226
+ };
227
+ }
228
+ const boundary = getPlacementBoundary({
229
+ targetRect,
230
+ axis,
231
+ direction: currentDirection,
232
+ startRatio: input.options.placementBoundary.start,
233
+ endRatio: input.options.placementBoundary.end,
234
+ });
235
+ const reversalPlacement = getPlacementSideFromBoundary(axisPosition, boundary);
236
+ if (reversalPlacement === previousPlacement.placement) {
237
+ return {
238
+ placement: previousPlacement.placement,
239
+ movementDirection: previousPlacement.movementDirection,
240
+ };
241
+ }
242
+ return {
243
+ placement: reversalPlacement,
244
+ movementDirection: currentDirection,
245
+ };
246
+ }
203
247
  if (currentDirection === "none" ||
204
248
  currentDirection === previousPlacement.movementDirection) {
205
249
  return {
@@ -256,15 +300,17 @@ function getPlacementSideFromTargetMidpoint(input) {
256
300
  const midpoint = targetStart + targetSize / 2;
257
301
  return input.axisPosition < midpoint ? "before" : "after";
258
302
  }
259
- function getMovementDirectionFromPlacementSide(placement) {
260
- return placement === "before" ? "backward" : "forward";
261
- }
262
303
  function getCurrentAxisMovementDirection(input) {
263
- const axisPosition = getAxisPosition(input.placementPosition, input.axis);
264
- const previousAxisPosition = input.movement
265
- ? getAxisPosition(input.movement.previousPointerPosition, input.axis)
266
- : axisPosition;
267
- return getAxisMovementDirection(previousAxisPosition, axisPosition);
304
+ const movementPosition = input.movementPosition ?? input.placementPosition;
305
+ const previousMovementPosition = input.movement?.previousPointerPosition ?? movementPosition;
306
+ const inputDirection = getAxisMovementDirection(getAxisPosition(previousMovementPosition, input.axis), getAxisPosition(movementPosition, input.axis));
307
+ if (inputDirection === "none") {
308
+ return "none";
309
+ }
310
+ const previousPlacementPosition = input.movement?.previousPlacementPosition ??
311
+ input.movement?.previousPointerPosition ??
312
+ input.placementPosition;
313
+ return getAxisMovementDirection(getAxisPosition(previousPlacementPosition, input.axis), getAxisPosition(input.placementPosition, input.axis));
268
314
  }
269
315
  export function isSortablePreviewAlreadyPlaced(input) {
270
316
  return input.placement === "before"
@@ -311,6 +357,11 @@ function addSortableMeasurementIdsAroundElement(dropTargetIds, registry, element
311
357
  addSortableMeasurementId(dropTargetIds, registry, element.previousElementSibling);
312
358
  addSortableMeasurementId(dropTargetIds, registry, element.nextElementSibling);
313
359
  }
360
+ function addSortableMeasurementIdsInContainer(dropTargetIds, registry, containerElement) {
361
+ for (let index = 0; index < containerElement.children.length; index += 1) {
362
+ addSortableMeasurementId(dropTargetIds, registry, containerElement.children.item(index));
363
+ }
364
+ }
314
365
  function addSortableMeasurementId(dropTargetIds, registry, element) {
315
366
  if (!(element instanceof HTMLElement)) {
316
367
  return;
@@ -32,6 +32,10 @@ export type DomSortableRuntime = DomDraggableRuntime & {
32
32
  x: number;
33
33
  y: number;
34
34
  };
35
+ movementPosition?: {
36
+ x: number;
37
+ y: number;
38
+ };
35
39
  }) => void;
36
40
  onDragUpdate?: (event: {
37
41
  draggableId: string;
@@ -44,6 +48,10 @@ export type DomSortableRuntime = DomDraggableRuntime & {
44
48
  x: number;
45
49
  y: number;
46
50
  };
51
+ movementPosition?: {
52
+ x: number;
53
+ y: number;
54
+ };
47
55
  activeDropTargetId: string | null;
48
56
  previousDropTargetId: string | null;
49
57
  }) => void;
@@ -22,26 +22,30 @@ export function getSortableRegistry(runtime) {
22
22
  };
23
23
  runtime.subscribe({
24
24
  onDragStart: (event) => {
25
- initializeSortablePointerMovement(registry, event.placementPosition ?? event.pointerPosition);
25
+ const placementPosition = event.placementPosition ?? event.pointerPosition;
26
+ initializeSortablePointerMovement(registry, event.movementPosition ?? placementPosition, placementPosition);
26
27
  snapshotSortableElement(registry, event.draggableId);
27
28
  },
28
29
  onDragUpdate: (event) => {
29
30
  if (isSortablePreviewTarget({
30
31
  activeDropTargetId: event.activeDropTargetId,
31
32
  })) {
33
+ const placementPosition = event.placementPosition ?? event.pointerPosition;
32
34
  moveSortablePreview({
33
35
  registry,
34
36
  runtime,
35
37
  draggedDraggableId: event.draggableId,
36
38
  activeDropTargetId: event.activeDropTargetId,
37
- placementPosition: event.placementPosition ?? event.pointerPosition,
39
+ placementPosition,
40
+ movementPosition: event.movementPosition ?? placementPosition,
38
41
  options: getSortableOptions(registry, event.draggableId),
39
42
  });
40
43
  }
41
44
  else {
42
45
  clearSortablePreviewPlacement(registry);
43
46
  }
44
- updateSortablePointerMovement(registry, event.placementPosition ?? event.pointerPosition);
47
+ const placementPosition = event.placementPosition ?? event.pointerPosition;
48
+ updateSortablePointerMovement(registry, event.movementPosition ?? placementPosition, placementPosition);
45
49
  },
46
50
  onDragEnd: (event) => {
47
51
  releaseSortableActiveDragState(registry, event.draggableId);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mk-drag-and-drop/dom",
3
- "version": "0.4.3",
3
+ "version": "0.4.5",
4
4
  "description": "Headless DOM drag-and-drop primitives for draggable, droppable, sortable, and grouped interactions.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://mkramer.dev",