@jsenv/dom 0.6.0 → 0.7.0

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.
Files changed (109) hide show
  1. package/dist/jsenv_dom.js +262 -330
  2. package/package.json +2 -4
  3. package/index.js +0 -124
  4. package/src/attr/add_attribute_effect.js +0 -93
  5. package/src/attr/attributes.js +0 -32
  6. package/src/color/color_constrast.js +0 -69
  7. package/src/color/color_parsing.js +0 -319
  8. package/src/color/color_scheme.js +0 -28
  9. package/src/color/pick_light_or_dark.js +0 -34
  10. package/src/color/resolve_css_color.js +0 -60
  11. package/src/demos/3_columns_resize_demo.html +0 -84
  12. package/src/demos/3_rows_resize_demo.html +0 -89
  13. package/src/demos/aside_and_main_demo.html +0 -93
  14. package/src/demos/coordinates_demo.html +0 -450
  15. package/src/demos/document_autoscroll_demo.html +0 -517
  16. package/src/demos/drag_gesture_constraints_demo.html +0 -701
  17. package/src/demos/drag_gesture_demo.html +0 -1047
  18. package/src/demos/drag_gesture_element_to_impact_demo.html +0 -445
  19. package/src/demos/drag_reference_element_demo.html +0 -480
  20. package/src/demos/flex_details_set_demo.html +0 -302
  21. package/src/demos/flex_details_set_demo_2.html +0 -315
  22. package/src/demos/visible_rect_demo.html +0 -525
  23. package/src/element_signature.js +0 -100
  24. package/src/interaction/drag/constraint_feedback_line.js +0 -92
  25. package/src/interaction/drag/drag_constraint.js +0 -659
  26. package/src/interaction/drag/drag_debug_markers.js +0 -635
  27. package/src/interaction/drag/drag_element_positioner.js +0 -382
  28. package/src/interaction/drag/drag_gesture.js +0 -566
  29. package/src/interaction/drag/drag_resize_demo.html +0 -571
  30. package/src/interaction/drag/drag_to_move.js +0 -301
  31. package/src/interaction/drag/drag_to_resize_gesture.js +0 -68
  32. package/src/interaction/drag/drop_target_detection.js +0 -148
  33. package/src/interaction/drag/sticky_frontiers.js +0 -160
  34. package/src/interaction/event_marker.js +0 -14
  35. package/src/interaction/focus/active_element.js +0 -33
  36. package/src/interaction/focus/arrow_navigation.js +0 -599
  37. package/src/interaction/focus/element_is_focusable.js +0 -57
  38. package/src/interaction/focus/element_visibility.js +0 -111
  39. package/src/interaction/focus/find_focusable.js +0 -21
  40. package/src/interaction/focus/focus_group.js +0 -91
  41. package/src/interaction/focus/focus_group_registry.js +0 -12
  42. package/src/interaction/focus/focus_nav.js +0 -12
  43. package/src/interaction/focus/focus_nav_event_marker.js +0 -14
  44. package/src/interaction/focus/focus_trap.js +0 -105
  45. package/src/interaction/focus/tab_navigation.js +0 -128
  46. package/src/interaction/focus/tests/focus_group_skip_tab_test.html +0 -206
  47. package/src/interaction/focus/tests/tree_focus_test.html +0 -304
  48. package/src/interaction/focus/tests/tree_focus_test.jsx +0 -261
  49. package/src/interaction/focus/tests/tree_focus_test_preact.html +0 -13
  50. package/src/interaction/isolate_interactions.js +0 -161
  51. package/src/interaction/keyboard.js +0 -26
  52. package/src/interaction/scroll/capture_scroll.js +0 -47
  53. package/src/interaction/scroll/is_scrollable.js +0 -159
  54. package/src/interaction/scroll/scroll_container.js +0 -110
  55. package/src/interaction/scroll/scroll_trap.js +0 -44
  56. package/src/interaction/scroll/scrollbar_size.js +0 -20
  57. package/src/interaction/scroll/wheel_through.js +0 -138
  58. package/src/iterable_weak_set.js +0 -66
  59. package/src/position/dom_coords.js +0 -340
  60. package/src/position/offset_parent.js +0 -15
  61. package/src/position/position_fixed.js +0 -15
  62. package/src/position/position_sticky.js +0 -213
  63. package/src/position/sticky_rect.js +0 -79
  64. package/src/position/visible_rect.js +0 -486
  65. package/src/pub_sub.js +0 -31
  66. package/src/size/can_take_size.js +0 -11
  67. package/src/size/details_content_full_height.js +0 -63
  68. package/src/size/flex_details_set.js +0 -974
  69. package/src/size/get_available_height.js +0 -22
  70. package/src/size/get_available_width.js +0 -22
  71. package/src/size/get_border_sizes.js +0 -14
  72. package/src/size/get_height.js +0 -4
  73. package/src/size/get_inner_height.js +0 -15
  74. package/src/size/get_inner_width.js +0 -15
  75. package/src/size/get_margin_sizes.js +0 -10
  76. package/src/size/get_max_height.js +0 -57
  77. package/src/size/get_max_width.js +0 -47
  78. package/src/size/get_min_height.js +0 -14
  79. package/src/size/get_min_width.js +0 -14
  80. package/src/size/get_padding_sizes.js +0 -10
  81. package/src/size/get_width.js +0 -4
  82. package/src/size/hooks/use_available_height.js +0 -27
  83. package/src/size/hooks/use_available_width.js +0 -27
  84. package/src/size/hooks/use_max_height.js +0 -10
  85. package/src/size/hooks/use_max_width.js +0 -10
  86. package/src/size/hooks/use_resize_status.js +0 -62
  87. package/src/size/resize.js +0 -695
  88. package/src/size/resolve_css_size.js +0 -32
  89. package/src/style/dom_styles.js +0 -97
  90. package/src/style/style_composition.js +0 -121
  91. package/src/style/style_controller.js +0 -345
  92. package/src/style/style_default.js +0 -153
  93. package/src/style/style_default_demo.html +0 -128
  94. package/src/style/style_parsing.js +0 -375
  95. package/src/transition/demos/animation_resumption_test.xhtml +0 -500
  96. package/src/transition/demos/height_toggle_test.xhtml +0 -515
  97. package/src/transition/dom_transition.js +0 -254
  98. package/src/transition/easing.js +0 -48
  99. package/src/transition/group_transition.js +0 -261
  100. package/src/transition/transform_style_parser.js +0 -32
  101. package/src/transition/transition_playback.js +0 -366
  102. package/src/transition/transition_timeline.js +0 -79
  103. package/src/traversal.js +0 -247
  104. package/src/ui_transition/demos/content_states_transition_demo.html +0 -628
  105. package/src/ui_transition/demos/smooth_height_transition_demo.html +0 -149
  106. package/src/ui_transition/demos/transition_testing.html +0 -354
  107. package/src/ui_transition/ui_transition.js +0 -1491
  108. package/src/utils.js +0 -69
  109. package/src/value_effect.js +0 -35
@@ -1,659 +0,0 @@
1
- import { getElementSignature } from "../../element_signature.js";
2
- import {
3
- addScrollToRect,
4
- getScrollRelativeRect,
5
- } from "../../position/dom_coords.js";
6
- import { setupConstraintFeedbackLine } from "./constraint_feedback_line.js";
7
- import { setupDragDebugMarkers } from "./drag_debug_markers.js";
8
-
9
- const CONSOLE_DEBUG_BOUNDS = true;
10
- const CONSOLE_DEBUG_OBSTACLES = false;
11
-
12
- export const initDragConstraints = (
13
- dragGesture,
14
- {
15
- areaConstraint,
16
- obstaclesContainer,
17
- obstacleAttributeName,
18
- showConstraintFeedbackLine,
19
- showDebugMarkers,
20
- referenceElement,
21
- },
22
- ) => {
23
- const dragGestureName = dragGesture.gestureInfo.name;
24
- const direction = dragGesture.gestureInfo.direction;
25
- const scrollContainer = dragGesture.gestureInfo.scrollContainer;
26
- const leftAtGrab = dragGesture.gestureInfo.leftAtGrab;
27
- const topAtGrab = dragGesture.gestureInfo.topAtGrab;
28
-
29
- const constraintFunctions = [];
30
- const addConstraint = (constraint) => {
31
- constraintFunctions.push(constraint);
32
- };
33
-
34
- if (showConstraintFeedbackLine) {
35
- const constraintFeedbackLine = setupConstraintFeedbackLine(dragGesture);
36
- dragGesture.addDragCallback((gestureInfo) => {
37
- constraintFeedbackLine.onDrag(gestureInfo);
38
- });
39
- dragGesture.addReleaseCallback(() => {
40
- constraintFeedbackLine.onRelease();
41
- });
42
- }
43
- let dragDebugMarkers;
44
- if (showDebugMarkers) {
45
- dragDebugMarkers = setupDragDebugMarkers(dragGesture, {
46
- referenceElement,
47
- });
48
- dragGesture.addReleaseCallback(() => {
49
- dragDebugMarkers.onRelease();
50
- });
51
- }
52
-
53
- area: {
54
- const areaConstraintFunction = createAreaConstraint(areaConstraint, {
55
- scrollContainer,
56
- });
57
- if (areaConstraintFunction) {
58
- addConstraint(areaConstraintFunction);
59
- }
60
- }
61
- obstacles: {
62
- if (!obstacleAttributeName || !obstaclesContainer) {
63
- break obstacles;
64
- }
65
- const obstacleConstraintFunctions =
66
- createObstacleConstraintsFromQuerySelector(obstaclesContainer, {
67
- obstacleAttributeName,
68
- gestureInfo: dragGesture.gestureInfo,
69
- isDraggedElementSticky: false,
70
- // isStickyLeftOrHasStickyLeftAttr || isStickyTopOrHasStickyTopAttr,
71
- });
72
- for (const obstacleConstraintFunction of obstacleConstraintFunctions) {
73
- addConstraint(obstacleConstraintFunction);
74
- }
75
- }
76
-
77
- const applyConstraints = (
78
- layoutRequested,
79
- currentLayout,
80
- limitLayout,
81
- {
82
- elementWidth,
83
- elementHeight,
84
- scrollArea,
85
- scrollport,
86
- hasCrossedScrollportLeftOnce,
87
- hasCrossedScrollportTopOnce,
88
- autoScrollArea,
89
- dragEvent,
90
- },
91
- ) => {
92
- if (constraintFunctions.length === 0) {
93
- return;
94
- }
95
-
96
- const elementCurrentLeft = currentLayout.left;
97
- const elementCurrentTop = currentLayout.top;
98
- const elementLeftRequested = layoutRequested.left;
99
- const elementTopRequested = layoutRequested.top;
100
- let elementLeft = elementLeftRequested;
101
- let elementTop = elementTopRequested;
102
-
103
- const constraintInitParams = {
104
- leftAtGrab,
105
- topAtGrab,
106
- left: elementCurrentLeft,
107
- top: elementCurrentTop,
108
- right: elementCurrentLeft + elementWidth,
109
- bottom: elementCurrentTop + elementHeight,
110
- width: elementWidth,
111
- height: elementHeight,
112
- scrollContainer,
113
- scrollArea,
114
- scrollport,
115
- autoScrollArea,
116
- dragGestureName,
117
- dragEvent,
118
- };
119
- const constraints = constraintFunctions.map((fn) =>
120
- fn(constraintInitParams),
121
- );
122
- // Development safeguards: detect impossible/illogical constraints
123
- if (import.meta.dev) {
124
- validateConstraints(constraints, constraintInitParams);
125
- }
126
-
127
- const logConstraintEnforcement = (axis, constraint) => {
128
- if (!CONSOLE_DEBUG_BOUNDS && constraint.type === "bounds") {
129
- return;
130
- }
131
- if (!CONSOLE_DEBUG_OBSTACLES && constraint.type === "obstacle") {
132
- return;
133
- }
134
- const requested =
135
- axis === "x" ? elementLeftRequested : elementTopRequested;
136
- const constrained = axis === "x" ? elementLeft : elementTop;
137
- const action = constrained > requested ? "increased" : "capped";
138
- const property = axis === "x" ? "left" : "top";
139
- console.debug(
140
- `Drag by ${dragEvent.type}: ${property} ${action} from ${requested.toFixed(2)} to ${constrained.toFixed(2)} by ${constraint.type}:${constraint.name}`,
141
- constraint.element,
142
- );
143
- };
144
-
145
- // Apply each constraint in sequence, accumulating their effects
146
- // This allows multiple constraints to work together (e.g., bounds + obstacles)
147
- for (const constraint of constraints) {
148
- const result = constraint.apply({
149
- // each constraint works with scroll included coordinates
150
- // and coordinates we provide here includes the scroll of the container
151
- left: elementLeft,
152
- top: elementTop,
153
- right: elementLeft + elementWidth,
154
- bottom: elementTop + elementHeight,
155
- width: elementWidth,
156
- height: elementHeight,
157
- currentLeft: elementCurrentLeft,
158
- currentTop: elementCurrentTop,
159
- scrollport,
160
- hasCrossedScrollportLeftOnce,
161
- hasCrossedScrollportTopOnce,
162
- });
163
- if (!result) {
164
- continue;
165
- }
166
- const [elementLeftConstrained, elementTopConstrained] = result;
167
- if (direction.x && elementLeftConstrained !== elementLeft) {
168
- elementLeft = elementLeftConstrained;
169
- logConstraintEnforcement("x", constraint);
170
- }
171
- if (direction.y && elementTopConstrained !== elementTop) {
172
- elementTop = elementTopConstrained;
173
- logConstraintEnforcement("y", constraint);
174
- }
175
- }
176
-
177
- if (dragDebugMarkers) {
178
- dragDebugMarkers.onConstraints(constraints, {
179
- left: elementLeft,
180
- top: elementTop,
181
- right: elementLeft + elementWidth,
182
- bottom: elementTop + elementHeight,
183
- elementWidth,
184
- elementHeight,
185
- scrollport,
186
- autoScrollArea,
187
- });
188
- }
189
-
190
- const leftModified = elementLeft !== elementLeftRequested;
191
- const topModified = elementTop !== elementTopRequested;
192
- if (!leftModified && !topModified) {
193
- if (CONSOLE_DEBUG_BOUNDS || CONSOLE_DEBUG_OBSTACLES) {
194
- console.debug(
195
- `Drag by ${dragEvent.type}: no constraint enforcement needed (${elementLeftRequested.toFixed(2)}, ${elementTopRequested.toFixed(2)})`,
196
- );
197
- }
198
- return;
199
- }
200
-
201
- limitLayout(elementLeft, elementTop);
202
- };
203
-
204
- return { applyConstraints };
205
- };
206
-
207
- const createAreaConstraint = (areaConstraint, { scrollContainer }) => {
208
- if (!areaConstraint || areaConstraint === "none") {
209
- return null;
210
- }
211
- if (areaConstraint === "scrollport") {
212
- const scrollportConstraintFunction = ({ scrollport }) => {
213
- return createBoundConstraint(scrollport, {
214
- element: scrollContainer,
215
- name: "scrollport",
216
- });
217
- };
218
- return scrollportConstraintFunction;
219
- }
220
- if (areaConstraint === "scroll") {
221
- const scrollAreaConstraintFunction = ({ scrollArea }) => {
222
- return createBoundConstraint(scrollArea, {
223
- element: scrollContainer,
224
- name: "scroll_area",
225
- });
226
- };
227
- return scrollAreaConstraintFunction;
228
- }
229
- if (typeof areaConstraint === "function") {
230
- const dynamicAreaConstraintFunction = (params) => {
231
- const bounds = areaConstraint(params);
232
- return createBoundConstraint(bounds, {
233
- name: "dynamic_area",
234
- });
235
- };
236
- return dynamicAreaConstraintFunction;
237
- }
238
- if (typeof areaConstraint === "object") {
239
- const { left, top, right, bottom } = areaConstraint;
240
- const turnSidePropertyInToGetter = (value, side) => {
241
- if (value === "scrollport") {
242
- return ({ scrollport }) => scrollport[side];
243
- }
244
- if (value === "scroll") {
245
- return ({ scrollArea }) => scrollArea[side];
246
- }
247
- if (typeof value === "function") {
248
- return value;
249
- }
250
- if (value === undefined) {
251
- // defaults to scrollport
252
- return ({ scrollport }) => scrollport[side];
253
- }
254
- return () => value;
255
- };
256
- const getLeft = turnSidePropertyInToGetter(left, "left");
257
- const getRight = turnSidePropertyInToGetter(right, "right");
258
- const getTop = turnSidePropertyInToGetter(top, "top");
259
- const getBottom = turnSidePropertyInToGetter(bottom, "bottom");
260
-
261
- const dynamicAreaConstraintFunction = (params) => {
262
- const bounds = {
263
- left: getLeft(params),
264
- right: getRight(params),
265
- top: getTop(params),
266
- bottom: getBottom(params),
267
- };
268
- return createBoundConstraint(bounds, {
269
- name: "dynamic_area",
270
- });
271
- };
272
- return dynamicAreaConstraintFunction;
273
- }
274
- console.warn(
275
- `Unknown areaConstraint value: ${areaConstraint}. Expected "scrollport", "scroll", "none", an object with boundary definitions, or a function returning boundary definitions.`,
276
- );
277
- return null;
278
- };
279
-
280
- const createObstacleConstraintsFromQuerySelector = (
281
- scrollableElement,
282
- { obstacleAttributeName, gestureInfo, isDraggedElementSticky = false },
283
- ) => {
284
- const dragGestureName = gestureInfo.name;
285
- const obstacles = scrollableElement.querySelectorAll(
286
- `[${obstacleAttributeName}]`,
287
- );
288
- const obstacleConstraintFunctions = [];
289
- for (const obstacle of obstacles) {
290
- if (obstacle.closest("[data-drag-ignore]")) {
291
- continue;
292
- }
293
- if (dragGestureName) {
294
- const obstacleAttributeValue = obstacle.getAttribute(
295
- obstacleAttributeName,
296
- );
297
- if (obstacleAttributeValue) {
298
- const obstacleNames = obstacleAttributeValue.split(",");
299
- const found = obstacleNames.some(
300
- (obstacleName) =>
301
- obstacleName.trim().toLowerCase() === dragGestureName.toLowerCase(),
302
- );
303
- if (!found) {
304
- continue;
305
- }
306
- }
307
- }
308
-
309
- obstacleConstraintFunctions.push(
310
- ({ hasCrossedVisibleAreaLeftOnce, hasCrossedVisibleAreaTopOnce }) => {
311
- // Only apply the "before crossing visible area" logic when dragging sticky elements
312
- // Non-sticky elements should be able to cross sticky obstacles while stuck regardless of visible area crossing
313
- const useOriginalPositionEvenIfSticky = isDraggedElementSticky
314
- ? !hasCrossedVisibleAreaLeftOnce && !hasCrossedVisibleAreaTopOnce
315
- : true;
316
-
317
- const obstacleScrollRelativeRect = getScrollRelativeRect(
318
- obstacle,
319
- scrollableElement,
320
- {
321
- useOriginalPositionEvenIfSticky,
322
- },
323
- );
324
- let obstacleBounds;
325
- if (
326
- useOriginalPositionEvenIfSticky &&
327
- obstacleScrollRelativeRect.isSticky
328
- ) {
329
- obstacleBounds = obstacleScrollRelativeRect;
330
- } else {
331
- obstacleBounds = addScrollToRect(obstacleScrollRelativeRect);
332
- }
333
-
334
- // obstacleBounds are already in scrollable-relative coordinates, no conversion needed
335
- const obstacleObject = createObstacleContraint(obstacleBounds, {
336
- name: `${obstacleBounds.isSticky ? "sticky " : ""}obstacle (${getElementSignature(obstacle)})`,
337
- element: obstacle,
338
- });
339
- return obstacleObject;
340
- },
341
- );
342
- }
343
- return obstacleConstraintFunctions;
344
- };
345
-
346
- const createBoundConstraint = (bounds, { name, element } = {}) => {
347
- const leftBound = bounds.left;
348
- const rightBound = bounds.right;
349
- const topBound = bounds.top;
350
- const bottomBound = bounds.bottom;
351
-
352
- const apply = ({ left, top, right, bottom, width, height }) => {
353
- let leftConstrained = left;
354
- let topConstrained = top;
355
- // Left boundary: element's left edge should not go before leftBound
356
- if (leftBound !== undefined && left < leftBound) {
357
- leftConstrained = leftBound;
358
- }
359
- // Right boundary: element's right edge should not go past rightBound
360
- if (rightBound !== undefined && right > rightBound) {
361
- leftConstrained = rightBound - width;
362
- }
363
- // Top boundary: element's top edge should not go before topBound
364
- if (topBound !== undefined && top < topBound) {
365
- topConstrained = topBound;
366
- }
367
- // Bottom boundary: element's bottom edge should not go past bottomBound
368
- if (bottomBound !== undefined && bottom > bottomBound) {
369
- topConstrained = bottomBound - height;
370
- }
371
- return [leftConstrained, topConstrained];
372
- };
373
-
374
- return {
375
- type: "bounds",
376
- name,
377
- apply,
378
- element,
379
- bounds,
380
- };
381
- };
382
- const createObstacleContraint = (bounds, { element, name }) => {
383
- const leftBound = bounds.left;
384
- const rightBound = bounds.right;
385
- const topBound = bounds.top;
386
- const bottomBound = bounds.bottom;
387
- const leftBoundRounded = roundForConstraints(leftBound);
388
- const rightBoundRounded = roundForConstraints(rightBound);
389
- const topBoundRounded = roundForConstraints(topBound);
390
- const bottomBoundRounded = roundForConstraints(bottomBound);
391
-
392
- const apply = ({
393
- left,
394
- top,
395
- right,
396
- bottom,
397
- width,
398
- height,
399
- currentLeft,
400
- currentTop,
401
- }) => {
402
- // Simple collision detection: check where element is and prevent movement into obstacle
403
- {
404
- // Determine current position relative to obstacle
405
- const currentLeftRounded = roundForConstraints(currentLeft);
406
- const currentRightRounded = roundForConstraints(currentLeft + width);
407
- const currentTopRounded = roundForConstraints(currentTop);
408
- const currentBottomRounded = roundForConstraints(currentTop + height);
409
- const isOnTheLeft = currentRightRounded <= leftBoundRounded;
410
- const isOnTheRight = currentLeftRounded >= rightBoundRounded;
411
- const isAbove = currentBottomRounded <= topBoundRounded;
412
- const isBelow = currentTopRounded >= bottomBoundRounded;
413
- // Debug logging to understand element position
414
- if (CONSOLE_DEBUG_OBSTACLES) {
415
- const position = isOnTheLeft
416
- ? "left"
417
- : isOnTheRight
418
- ? "right"
419
- : isAbove
420
- ? "above"
421
- : isBelow
422
- ? "below"
423
- : "overlapping";
424
- console.log(`Element position relative to obstacle: ${position}`);
425
- console.log(
426
- `Element current position: left=${currentLeftRounded}, right=${currentRightRounded}, top=${currentTopRounded}, bottom=${currentBottomRounded}`,
427
- );
428
- console.log(
429
- `Obstacle position: leftBound=${leftBound}, rightBound=${rightBound}, topBound=${topBound}, bottomBound=${bottomBound}`,
430
- );
431
- }
432
-
433
- // If element is on the left, apply X constraint to prevent moving right into obstacle
434
- if (isOnTheLeft) {
435
- const wouldHaveYOverlap = top < bottomBound && bottom > topBound;
436
- if (wouldHaveYOverlap) {
437
- const maxLeft = leftBound - width;
438
- if (left > maxLeft) {
439
- return [maxLeft, top];
440
- }
441
- }
442
- }
443
- // If element is on the right, apply X constraint to prevent moving left into obstacle
444
- else if (isOnTheRight) {
445
- const wouldHaveYOverlap = top < bottomBound && bottom > topBound;
446
- if (wouldHaveYOverlap) {
447
- const minLeft = rightBound;
448
- if (left < minLeft) {
449
- return [minLeft, top];
450
- }
451
- }
452
- }
453
- // If element is above, apply Y constraint to prevent moving down into obstacle
454
- else if (isAbove) {
455
- const wouldHaveXOverlap = left < rightBound && right > leftBound;
456
- if (wouldHaveXOverlap) {
457
- const maxTop = topBound - height;
458
- if (top > maxTop) {
459
- return [left, maxTop];
460
- }
461
- }
462
- }
463
- // If element is below, apply Y constraint to prevent moving up into obstacle
464
- else if (isBelow) {
465
- const wouldHaveXOverlap = left < rightBound && right > leftBound;
466
- if (wouldHaveXOverlap) {
467
- const minTop = bottomBound;
468
- if (top < minTop) {
469
- return [left, minTop];
470
- }
471
- }
472
- }
473
- }
474
-
475
- // Element is overlapping with obstacle - push it out in the direction of least resistance
476
- // Calculate distances to push element out in each direction
477
- const distanceToLeft = right - leftBound; // Distance to push left
478
- const distanceToRight = rightBound - left; // Distance to push right
479
- const distanceToTop = bottom - topBound; // Distance to push up
480
- const distanceToBottom = bottomBound - top; // Distance to push down
481
- // Find the minimum distance (direction of least resistance)
482
- const minDistance = Math.min(
483
- distanceToLeft,
484
- distanceToRight,
485
- distanceToTop,
486
- distanceToBottom,
487
- );
488
- if (minDistance === distanceToLeft) {
489
- // Push left: element should not go past leftBound - elementWidth
490
- const maxLeft = leftBound - width;
491
- if (left > maxLeft) {
492
- return [maxLeft, top];
493
- }
494
- } else if (minDistance === distanceToRight) {
495
- // Push right: element should not go before rightBound
496
- const minLeft = rightBound;
497
- if (left < minLeft) {
498
- return [minLeft, top];
499
- }
500
- } else if (minDistance === distanceToTop) {
501
- // Push up: element should not go past topBound - elementHeight
502
- const maxTop = topBound - height;
503
- if (top > maxTop) {
504
- return [left, maxTop];
505
- }
506
- } else if (minDistance === distanceToBottom) {
507
- // Push down: element should not go before bottomBound
508
- const minTop = bottomBound;
509
- if (top < minTop) {
510
- return [left, minTop];
511
- }
512
- }
513
-
514
- return null;
515
- };
516
-
517
- return {
518
- type: "obstacle",
519
- name,
520
- apply,
521
- element,
522
- bounds,
523
- };
524
- };
525
-
526
- /**
527
- * Rounds coordinates to prevent floating point precision issues in constraint calculations.
528
- *
529
- * This is critical for obstacle detection because:
530
- * 1. Boundary detection relies on precise comparisons (e.g., elementRight <= obstacleLeft)
531
- * 2. Floating point arithmetic can produce values like 149.99999999 instead of 150
532
- * 3. This causes incorrect boundary classifications (element appears "on left" when it should be "overlapping")
533
- *
534
- * Scroll events are more susceptible to this issue because:
535
- * - Mouse events use integer pixel coordinates from the DOM (e.g., clientX: 150)
536
- * - Scroll events use element.scrollLeft which can have sub-pixel values from CSS transforms, zoom, etc.
537
- * - Scroll compensation calculations (scrollDelta * ratios) amplify floating point errors
538
- * - Multiple scroll events accumulate these errors over time
539
- *
540
- * Using 2-decimal precision maintains smooth sub-pixel positioning while ensuring
541
- * reliable boundary detection for constraint systems.
542
- */
543
- const roundForConstraints = (value) => {
544
- return Math.round(value * 100) / 100;
545
- };
546
-
547
- /**
548
- * Validates constraints for logical consistency and reports issues during development.
549
- * Helps catch configuration errors like inappropriate obstacle assignments.
550
- */
551
- const validateConstraints = (
552
- constraints,
553
- { elementWidth, elementHeight, name: dragName },
554
- ) => {
555
- const boundsConstraints = constraints.filter((c) => c.type === "bounds");
556
- const obstacleConstraints = constraints.filter((c) => c.type === "obstacle");
557
-
558
- // Check for impossible bounds constraints
559
- boundsConstraints.forEach((bounds) => {
560
- if (bounds.left >= bounds.right) {
561
- console.warn(
562
- `Impossible bounds constraint: left (${bounds.left}) >= right (${bounds.right})`,
563
- { constraint: bounds, dragName, element: bounds.element },
564
- );
565
- }
566
- if (bounds.top >= bounds.bottom) {
567
- console.warn(
568
- `Impossible bounds constraint: top (${bounds.top}) >= bottom (${bounds.bottom})`,
569
- { constraint: bounds, dragName, element: bounds.element },
570
- );
571
- }
572
-
573
- const availableWidth = bounds.right - bounds.left;
574
- const availableHeight = bounds.bottom - bounds.top;
575
- const roundedElementWidth = elementWidth;
576
- const roundedElementHeight = elementHeight;
577
-
578
- // Math.round because some values comes from getBoundingClientRect() (floats)
579
- // and some from scrollWidth/Height (integers) causing precision issues
580
- if (
581
- Math.round(availableWidth) < Math.round(roundedElementWidth) &&
582
- availableWidth >= 0
583
- ) {
584
- console.warn(
585
- `Bounds constraint too narrow: available width (${availableWidth.toFixed(2)}) < element width (${roundedElementWidth.toFixed(2)})`,
586
- { constraint: bounds, dragName, element: bounds.element },
587
- );
588
- }
589
- if (
590
- Math.round(availableHeight) < Math.round(roundedElementHeight) &&
591
- availableHeight >= 0
592
- ) {
593
- console.warn(
594
- `Bounds constraint too short: available height (${availableHeight.toFixed(2)}) < element height (${roundedElementHeight.toFixed(2)})`,
595
- { constraint: bounds, dragName, element: bounds.element },
596
- );
597
- }
598
- });
599
-
600
- // Check for problematic obstacle overlaps and inappropriate obstacle assignments
601
- obstacleConstraints.forEach((obstacle, index) => {
602
- // Check for impossible obstacle geometry
603
- if (
604
- obstacle.bounds.left > obstacle.bounds.right ||
605
- obstacle.bounds.top > obstacle.bounds.bottom
606
- ) {
607
- console.warn(
608
- `Impossible obstacle geometry: left=${obstacle.bounds.left}, right=${obstacle.bounds.right}, top=${obstacle.bounds.top}, bottom=${obstacle.bounds.bottom}`,
609
- { constraint: obstacle, dragName, element: obstacle.element },
610
- );
611
- }
612
-
613
- // Check for obstacles that completely block movement in all directions
614
- boundsConstraints.forEach((bounds) => {
615
- const obstacleWidth = obstacle.bounds.right - obstacle.bounds.left;
616
- const obstacleHeight = obstacle.bounds.bottom - obstacle.bounds.top;
617
- const boundsWidth = bounds.right - bounds.left;
618
- const boundsHeight = bounds.bottom - bounds.top;
619
-
620
- if (obstacleWidth >= boundsWidth && obstacleHeight >= boundsHeight) {
621
- console.warn(
622
- `Obstacle completely blocks bounds area: obstacle (${obstacleWidth.toFixed(2)}×${obstacleHeight.toFixed(2)}) >= bounds (${boundsWidth.toFixed(2)}×${boundsHeight.toFixed(2)})`,
623
- {
624
- obstacle,
625
- bounds,
626
- dragName,
627
- obstacleElement: obstacle.element,
628
- boundsElement: bounds.element,
629
- },
630
- );
631
- }
632
- });
633
-
634
- // Check for overlapping obstacles that might create conflicting constraints
635
- obstacleConstraints.forEach((otherObstacle, otherIndex) => {
636
- if (index >= otherIndex) return; // Avoid duplicate checks
637
-
638
- const hasOverlap = !(
639
- obstacle.bounds.right <= otherObstacle.bounds.left ||
640
- obstacle.bounds.left >= otherObstacle.bounds.right ||
641
- obstacle.bounds.bottom <= otherObstacle.bounds.top ||
642
- obstacle.bounds.top >= otherObstacle.bounds.bottom
643
- );
644
-
645
- if (hasOverlap) {
646
- console.warn(
647
- `Overlapping obstacles detected: may create conflicting constraints`,
648
- {
649
- obstacle1: obstacle,
650
- obstacle2: otherObstacle,
651
- dragName,
652
- element1: obstacle.element,
653
- element2: otherObstacle.element,
654
- },
655
- );
656
- }
657
- });
658
- });
659
- };