@jsenv/dom 0.1.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 (101) hide show
  1. package/dist/jsenv_dom.js +9653 -0
  2. package/index.js +101 -0
  3. package/package.json +47 -0
  4. package/src/attr/add_attribute_effect.js +93 -0
  5. package/src/attr/attributes.js +32 -0
  6. package/src/demos/3_columns_resize_demo.html +84 -0
  7. package/src/demos/3_rows_resize_demo.html +89 -0
  8. package/src/demos/aside_and_main_demo.html +93 -0
  9. package/src/demos/coordinates_demo.html +450 -0
  10. package/src/demos/document_autoscroll_demo.html +517 -0
  11. package/src/demos/drag_gesture_constraints_demo.html +701 -0
  12. package/src/demos/drag_gesture_demo.html +1047 -0
  13. package/src/demos/drag_gesture_element_to_impact_demo.html +445 -0
  14. package/src/demos/drag_reference_element_demo.html +480 -0
  15. package/src/demos/flex_details_set_demo.html +302 -0
  16. package/src/demos/flex_details_set_demo_2.html +315 -0
  17. package/src/demos/visible_rect_demo.html +525 -0
  18. package/src/interaction/drag/constraint_feedback_line.js +92 -0
  19. package/src/interaction/drag/drag_constraint.js +659 -0
  20. package/src/interaction/drag/drag_debug_markers.js +635 -0
  21. package/src/interaction/drag/drag_element_positioner.js +382 -0
  22. package/src/interaction/drag/drag_gesture.js +566 -0
  23. package/src/interaction/drag/drag_resize_demo.html +571 -0
  24. package/src/interaction/drag/drag_to_move.js +301 -0
  25. package/src/interaction/drag/drag_to_resize_gesture.js +68 -0
  26. package/src/interaction/drag/drop_target_detection.js +148 -0
  27. package/src/interaction/drag/sticky_frontiers.js +160 -0
  28. package/src/interaction/element_log.js +8 -0
  29. package/src/interaction/event_marker.js +14 -0
  30. package/src/interaction/focus/active_element.js +33 -0
  31. package/src/interaction/focus/arrow_navigation.js +599 -0
  32. package/src/interaction/focus/element_is_focusable.js +57 -0
  33. package/src/interaction/focus/element_is_visible.js +36 -0
  34. package/src/interaction/focus/find_focusable.js +21 -0
  35. package/src/interaction/focus/focus_group.js +91 -0
  36. package/src/interaction/focus/focus_group_registry.js +12 -0
  37. package/src/interaction/focus/focus_nav.js +12 -0
  38. package/src/interaction/focus/focus_nav_event_marker.js +14 -0
  39. package/src/interaction/focus/focus_trap.js +105 -0
  40. package/src/interaction/focus/tab_navigation.js +128 -0
  41. package/src/interaction/focus/tests/focus_group_skip_tab_test.html +206 -0
  42. package/src/interaction/focus/tests/tree_focus_test.html +304 -0
  43. package/src/interaction/focus/tests/tree_focus_test.jsx +261 -0
  44. package/src/interaction/focus/tests/tree_focus_test_preact.html +13 -0
  45. package/src/interaction/isolate_interactions.js +161 -0
  46. package/src/interaction/keyboard.js +26 -0
  47. package/src/interaction/scroll/capture_scroll.js +47 -0
  48. package/src/interaction/scroll/is_scrollable.js +159 -0
  49. package/src/interaction/scroll/scroll_container.js +110 -0
  50. package/src/interaction/scroll/scroll_trap.js +44 -0
  51. package/src/interaction/scroll/scrollbar_size.js +20 -0
  52. package/src/interaction/scroll/wheel_through.js +138 -0
  53. package/src/iterable_weak_set.js +66 -0
  54. package/src/position/dom_coords.js +340 -0
  55. package/src/position/offset_parent.js +15 -0
  56. package/src/position/position_fixed.js +15 -0
  57. package/src/position/position_sticky.js +213 -0
  58. package/src/position/sticky_rect.js +79 -0
  59. package/src/position/visible_rect.js +482 -0
  60. package/src/pub_sub.js +28 -0
  61. package/src/size/can_take_size.js +11 -0
  62. package/src/size/details_content_full_height.js +63 -0
  63. package/src/size/flex_details_set.js +974 -0
  64. package/src/size/get_available_height.js +22 -0
  65. package/src/size/get_available_width.js +22 -0
  66. package/src/size/get_border_sizes.js +14 -0
  67. package/src/size/get_height.js +4 -0
  68. package/src/size/get_inner_height.js +15 -0
  69. package/src/size/get_inner_width.js +15 -0
  70. package/src/size/get_margin_sizes.js +10 -0
  71. package/src/size/get_max_height.js +57 -0
  72. package/src/size/get_max_width.js +47 -0
  73. package/src/size/get_min_height.js +14 -0
  74. package/src/size/get_min_width.js +14 -0
  75. package/src/size/get_padding_sizes.js +10 -0
  76. package/src/size/get_width.js +4 -0
  77. package/src/size/hooks/use_available_height.js +27 -0
  78. package/src/size/hooks/use_available_width.js +27 -0
  79. package/src/size/hooks/use_max_height.js +10 -0
  80. package/src/size/hooks/use_max_width.js +10 -0
  81. package/src/size/hooks/use_resize_status.js +62 -0
  82. package/src/size/resize.js +695 -0
  83. package/src/size/resolve_css_size.js +32 -0
  84. package/src/style/dom_styles.js +97 -0
  85. package/src/style/style_composition.js +78 -0
  86. package/src/style/style_controller.js +345 -0
  87. package/src/style/style_parsing.js +317 -0
  88. package/src/transition/demos/animation_resumption_test.xhtml +500 -0
  89. package/src/transition/demos/height_toggle_test.xhtml +515 -0
  90. package/src/transition/dom_transition.js +254 -0
  91. package/src/transition/easing.js +48 -0
  92. package/src/transition/group_transition.js +261 -0
  93. package/src/transition/transform_style_parser.js +32 -0
  94. package/src/transition/transition_playback.js +366 -0
  95. package/src/transition/transition_timeline.js +79 -0
  96. package/src/traversal.js +247 -0
  97. package/src/ui_transition/demos/content_states_transition_demo.html +628 -0
  98. package/src/ui_transition/demos/smooth_height_transition_demo.html +149 -0
  99. package/src/ui_transition/demos/transition_testing.html +354 -0
  100. package/src/ui_transition/ui_transition.js +1492 -0
  101. package/src/utils.js +69 -0
@@ -0,0 +1,482 @@
1
+ import { getScrollContainer } from "../interaction/scroll/scroll_container.js";
2
+ import { createPubSub } from "../pub_sub.js";
3
+
4
+ const DEBUG = false;
5
+
6
+ // Creates a visible rect effect that tracks how much of an element is visible within its scrollable parent
7
+ // and within the document viewport. This is useful for implementing overlays, lazy loading, or any UI
8
+ // that needs to react to element visibility changes.
9
+ //
10
+ // The function returns two visibility ratios:
11
+ // - scrollVisibilityRatio: Visibility ratio relative to the scrollable parent (0-1)
12
+ // - visibilityRatio: Visibility ratio relative to the document viewport (0-1)
13
+ //
14
+ // When scrollable parent is the document, both ratios will be the same.
15
+ // When scrollable parent is a custom container, scrollVisibilityRatio might be 1.0 (fully visible
16
+ // within the container) while visibilityRatio could be 0.0 (container is scrolled out of viewport).
17
+ // A bit like https://tetherjs.dev/ but different
18
+ export const visibleRectEffect = (element, update) => {
19
+ const [teardown, addTeardown] = createPubSub();
20
+ const scrollContainer = getScrollContainer(element);
21
+ const scrollContainerIsDocument =
22
+ scrollContainer === document.documentElement;
23
+ let lastMeasuredWidth;
24
+ let lastMeasuredHeight;
25
+ const check = (reason) => {
26
+ if (DEBUG) {
27
+ console.group(`visibleRect.check("${reason}")`);
28
+ }
29
+
30
+ // 1. Calculate element position relative to scrollable parent
31
+ const { scrollLeft, scrollTop } = scrollContainer;
32
+ const visibleAreaLeft = scrollLeft;
33
+ const visibleAreaTop = scrollTop;
34
+
35
+ // Get element position relative to its scrollable parent
36
+ let elementAbsoluteLeft;
37
+ let elementAbsoluteTop;
38
+ if (scrollContainerIsDocument) {
39
+ // For document scrolling, use offsetLeft/offsetTop relative to document
40
+ const rect = element.getBoundingClientRect();
41
+ elementAbsoluteLeft = rect.left + scrollLeft;
42
+ elementAbsoluteTop = rect.top + scrollTop;
43
+ } else {
44
+ // For custom container, get position relative to the container
45
+ const elementRect = element.getBoundingClientRect();
46
+ const scrollContainerRect = scrollContainer.getBoundingClientRect();
47
+ elementAbsoluteLeft =
48
+ elementRect.left - scrollContainerRect.left + scrollLeft;
49
+ elementAbsoluteTop =
50
+ elementRect.top - scrollContainerRect.top + scrollTop;
51
+ }
52
+
53
+ const leftVisible =
54
+ visibleAreaLeft < elementAbsoluteLeft
55
+ ? elementAbsoluteLeft - visibleAreaLeft
56
+ : 0;
57
+ const topVisible =
58
+ visibleAreaTop < elementAbsoluteTop
59
+ ? elementAbsoluteTop - visibleAreaTop
60
+ : 0;
61
+ // Convert to overlay coordinates (adjust for custom scrollable container)
62
+ let overlayLeft = leftVisible;
63
+ let overlayTop = topVisible;
64
+ if (!scrollContainerIsDocument) {
65
+ const { left: scrollableLeft, top: scrollableTop } =
66
+ scrollContainer.getBoundingClientRect();
67
+ overlayLeft += scrollableLeft;
68
+ overlayTop += scrollableTop;
69
+ }
70
+
71
+ // 2. Calculate element visible width/height
72
+ const { width, height } = element.getBoundingClientRect();
73
+ lastMeasuredWidth = width;
74
+ lastMeasuredHeight = height;
75
+ const visibleAreaWidth = scrollContainer.clientWidth;
76
+ const visibleAreaHeight = scrollContainer.clientHeight;
77
+ const visibleAreaRight = visibleAreaLeft + visibleAreaWidth;
78
+ const visibleAreaBottom = visibleAreaTop + visibleAreaHeight;
79
+ // 2.1 Calculate visible width
80
+ let widthVisible;
81
+ {
82
+ const maxVisibleWidth = visibleAreaWidth - leftVisible;
83
+ const elementAbsoluteRight = elementAbsoluteLeft + width;
84
+ const elementLeftIsVisible = elementAbsoluteLeft >= visibleAreaLeft;
85
+ const elementRightIsVisible = elementAbsoluteRight <= visibleAreaRight;
86
+ if (elementLeftIsVisible && elementRightIsVisible) {
87
+ // Element fully visible horizontally
88
+ widthVisible = width;
89
+ } else if (elementLeftIsVisible && !elementRightIsVisible) {
90
+ // Element left is visible, right is cut off
91
+ widthVisible = visibleAreaRight - elementAbsoluteLeft;
92
+ } else if (!elementLeftIsVisible && elementRightIsVisible) {
93
+ // Element left is cut off, right is visible
94
+ widthVisible = elementAbsoluteRight - visibleAreaLeft;
95
+ } else {
96
+ // Element spans beyond both sides, show only visible area portion
97
+ widthVisible = maxVisibleWidth;
98
+ }
99
+ }
100
+ // 2.2 Calculate visible height
101
+ let heightVisible;
102
+ {
103
+ const maxVisibleHeight = visibleAreaHeight - topVisible;
104
+ const elementAbsoluteBottom = elementAbsoluteTop + height;
105
+ const elementTopIsVisible = elementAbsoluteTop >= visibleAreaTop;
106
+ const elementBottomIsVisible = elementAbsoluteBottom <= visibleAreaBottom;
107
+ if (elementTopIsVisible && elementBottomIsVisible) {
108
+ // Element fully visible vertically
109
+ heightVisible = height;
110
+ } else if (elementTopIsVisible && !elementBottomIsVisible) {
111
+ // Element top is visible, bottom is cut off
112
+ heightVisible = visibleAreaBottom - elementAbsoluteTop;
113
+ } else if (!elementTopIsVisible && elementBottomIsVisible) {
114
+ // Element top is cut off, bottom is visible
115
+ heightVisible = elementAbsoluteBottom - visibleAreaTop;
116
+ } else {
117
+ // Element spans beyond both sides, show only visible area portion
118
+ heightVisible = maxVisibleHeight;
119
+ }
120
+ }
121
+
122
+ // Calculate visibility ratios
123
+ const scrollVisibilityRatio =
124
+ (widthVisible * heightVisible) / (width * height);
125
+ // Calculate visibility ratio relative to document viewport
126
+ let documentVisibilityRatio;
127
+ if (scrollContainerIsDocument) {
128
+ documentVisibilityRatio = scrollVisibilityRatio;
129
+ } else {
130
+ // For custom containers, calculate visibility relative to document viewport
131
+ const elementRect = element.getBoundingClientRect();
132
+ const viewportWidth = window.innerWidth;
133
+ const viewportHeight = window.innerHeight;
134
+ // Calculate how much of the element is visible in the document viewport
135
+ const elementLeft = Math.max(0, elementRect.left);
136
+ const elementTop = Math.max(0, elementRect.top);
137
+ const elementRight = Math.min(viewportWidth, elementRect.right);
138
+ const elementBottom = Math.min(viewportHeight, elementRect.bottom);
139
+ const documentVisibleWidth = Math.max(0, elementRight - elementLeft);
140
+ const documentVisibleHeight = Math.max(0, elementBottom - elementTop);
141
+ documentVisibilityRatio =
142
+ (documentVisibleWidth * documentVisibleHeight) / (width * height);
143
+ }
144
+
145
+ const visibleRect = {
146
+ left: overlayLeft,
147
+ top: overlayTop,
148
+ right: overlayLeft + widthVisible,
149
+ bottom: overlayTop + heightVisible,
150
+ width: widthVisible,
151
+ height: heightVisible,
152
+ visibilityRatio: documentVisibilityRatio,
153
+ scrollVisibilityRatio,
154
+ };
155
+
156
+ if (DEBUG) {
157
+ console.log(`update(${JSON.stringify(visibleRect, null, " ")})`);
158
+ console.groupEnd();
159
+ }
160
+ update(visibleRect, {
161
+ width,
162
+ height,
163
+ });
164
+ };
165
+
166
+ check("initialization");
167
+
168
+ const [publishBeforeAutoCheck, onBeforeAutoCheck] = createPubSub();
169
+ auto_check: {
170
+ const autoCheck = (reason) => {
171
+ const beforeCheckResults = publishBeforeAutoCheck(reason);
172
+ check(reason);
173
+ for (const beforeCheckResult of beforeCheckResults) {
174
+ if (typeof beforeCheckResult === "function") {
175
+ beforeCheckResult();
176
+ }
177
+ }
178
+ };
179
+ // let rafId = null;
180
+ // const scheduleCheck = (reason) => {
181
+ // cancelAnimationFrame(rafId);
182
+ // rafId = requestAnimationFrame(() => {
183
+ // autoCheck(reason);
184
+ // });
185
+ // };
186
+ // addTeardown(() => {
187
+ // cancelAnimationFrame(rafId);
188
+ // });
189
+
190
+ on_scroll: {
191
+ // If scrollable parent is not document, also listen to document scroll
192
+ // to update UI position when the scrollable parent moves in viewport
193
+ const onDocumentScroll = () => {
194
+ autoCheck("document_scroll");
195
+ };
196
+ document.addEventListener("scroll", onDocumentScroll, {
197
+ passive: true,
198
+ });
199
+ addTeardown(() => {
200
+ document.removeEventListener("scroll", onDocumentScroll, {
201
+ passive: true,
202
+ });
203
+ });
204
+ if (!scrollContainerIsDocument) {
205
+ const onScroll = () => {
206
+ autoCheck("scrollable_parent_scroll");
207
+ };
208
+ scrollContainer.addEventListener("scroll", onScroll, {
209
+ passive: true,
210
+ });
211
+ addTeardown(() => {
212
+ scrollContainer.removeEventListener("scroll", onScroll, {
213
+ passive: true,
214
+ });
215
+ });
216
+ }
217
+ }
218
+ on_window_resize: {
219
+ const onWindowResize = () => {
220
+ autoCheck("window_size_change");
221
+ };
222
+ window.addEventListener("resize", onWindowResize);
223
+ addTeardown(() => {
224
+ window.removeEventListener("resize", onWindowResize);
225
+ });
226
+ }
227
+ on_element_resize: {
228
+ let handlingResize = true;
229
+ const resizeObserver = new ResizeObserver(() => {
230
+ if (handlingResize) {
231
+ return;
232
+ }
233
+ // we use directly the result of getBoundingClientRect() instead of the resizeEntry.contentRect or resizeEntry.borderBoxSize
234
+ // so that:
235
+ // - We can compare the dimensions measure in the last check and the current one
236
+ // - We don't have to check element boz-sizing to know what to compare
237
+ // - resizeEntry.borderBoxSize browser support is not that great
238
+ const { width, height } = element.getBoundingClientRect();
239
+ const widthDiff = Math.abs(width - lastMeasuredWidth);
240
+ const heightDiff = Math.abs(height - lastMeasuredHeight);
241
+ if (widthDiff === 0 && heightDiff === 0) {
242
+ return;
243
+ }
244
+ handlingResize = true;
245
+ autoCheck(`element_size_change (${width}x${height})`);
246
+ handlingResize = false;
247
+ });
248
+ resizeObserver.observe(element);
249
+ // Temporarily disconnect ResizeObserver to prevent feedback loops eventually caused by update function
250
+ onBeforeAutoCheck(() => {
251
+ resizeObserver.unobserve(element);
252
+ return () => {
253
+ // This triggers a new call to the resive observer that will be ignored thanks to
254
+ // the widthDiff/heightDiff early return
255
+ resizeObserver.observe(element);
256
+ };
257
+ });
258
+ addTeardown(() => {
259
+ resizeObserver.disconnect();
260
+ });
261
+ }
262
+ on_intersection_change: {
263
+ const documentIntersectionObserver = new IntersectionObserver(
264
+ () => {
265
+ autoCheck("element_intersection_with_document_change");
266
+ },
267
+ {
268
+ root: null,
269
+ rootMargin: "0px",
270
+ threshold: [0, 0.1, 0.9, 1],
271
+ },
272
+ );
273
+ documentIntersectionObserver.observe(element);
274
+ addTeardown(() => {
275
+ documentIntersectionObserver.disconnect();
276
+ });
277
+ if (!scrollContainerIsDocument) {
278
+ const scrollIntersectionObserver = new IntersectionObserver(
279
+ () => {
280
+ autoCheck("element_intersection_with_scroll_change");
281
+ },
282
+ {
283
+ root: scrollContainer,
284
+ rootMargin: "0px",
285
+ threshold: [0, 0, 1, 0.9, 1],
286
+ },
287
+ );
288
+ scrollIntersectionObserver.observe(element);
289
+ addTeardown(() => {
290
+ scrollIntersectionObserver.disconnect();
291
+ });
292
+ }
293
+ }
294
+ on_window_touchmove: {
295
+ const onWindowTouchMove = () => {
296
+ autoCheck("window_touchmove");
297
+ };
298
+ window.addEventListener("touchmove", onWindowTouchMove, {
299
+ passive: true,
300
+ });
301
+ addTeardown(() => {
302
+ window.removeEventListener("touchmove", onWindowTouchMove, {
303
+ passive: true,
304
+ });
305
+ });
306
+ }
307
+ }
308
+
309
+ return {
310
+ check,
311
+ onBeforeAutoCheck,
312
+ disconnect: () => {
313
+ teardown();
314
+ },
315
+ };
316
+ };
317
+
318
+ export const pickPositionRelativeTo = (
319
+ element,
320
+ target,
321
+ { alignToViewportEdgeWhenTargetNearEdge = 0, forcePosition } = {},
322
+ ) => {
323
+ if (
324
+ import.meta.dev &&
325
+ getScrollContainer(element) !== document.documentElement
326
+ ) {
327
+ // The idea behind this warning is that pickPositionRelativeTo is meant to position a tooltip/dropdown etc
328
+ // And for this use case the element to position should be document-relative
329
+ // (position: absolute with first scrollable parent being the document)
330
+ // Because this is how you achieve the best results:
331
+ // 1. The element naturally follow document scroll
332
+ // Which gives the best experience when user scrolls the page or the container
333
+ // 2. The element can take more visible size in case target is within a scrollable container
334
+ // or uses overflow: hidden somewhere in its ancestor chain
335
+ console.warn(
336
+ "pickPositionRelativeTo should be used only for document-relative element",
337
+ );
338
+ }
339
+
340
+ const viewportWidth = document.documentElement.clientWidth;
341
+ const viewportHeight = document.documentElement.clientHeight;
342
+ // Get viewport-relative positions
343
+ const elementRect = element.getBoundingClientRect();
344
+ const targetRect = target.getBoundingClientRect();
345
+ const {
346
+ left: elementLeft,
347
+ right: elementRight,
348
+ top: elementTop,
349
+ bottom: elementBottom,
350
+ } = elementRect;
351
+ const {
352
+ left: targetLeft,
353
+ right: targetRight,
354
+ top: targetTop,
355
+ bottom: targetBottom,
356
+ } = targetRect;
357
+ const elementWidth = elementRight - elementLeft;
358
+ const elementHeight = elementBottom - elementTop;
359
+ const targetWidth = targetRight - targetLeft;
360
+
361
+ // Calculate horizontal position (viewport-relative)
362
+ let elementPositionLeft;
363
+ {
364
+ // Check if target element is wider than viewport
365
+ const targetIsWiderThanViewport = targetWidth > viewportWidth;
366
+ if (targetIsWiderThanViewport) {
367
+ const targetLeftIsVisible = targetLeft >= 0;
368
+ const targetRightIsVisible = targetRight <= viewportWidth;
369
+
370
+ if (!targetLeftIsVisible && targetRightIsVisible) {
371
+ // Target extends beyond left edge but right side is visible
372
+ const viewportCenter = viewportWidth / 2;
373
+ const distanceFromRightEdge = viewportWidth - targetRight;
374
+ elementPositionLeft =
375
+ viewportCenter - distanceFromRightEdge / 2 - elementWidth / 2;
376
+ } else if (targetLeftIsVisible && !targetRightIsVisible) {
377
+ // Target extends beyond right edge but left side is visible
378
+ const viewportCenter = viewportWidth / 2;
379
+ const distanceFromLeftEdge = -targetLeft;
380
+ elementPositionLeft =
381
+ viewportCenter - distanceFromLeftEdge / 2 - elementWidth / 2;
382
+ } else {
383
+ // Target extends beyond both edges or is fully visible (center in viewport)
384
+ elementPositionLeft = viewportWidth / 2 - elementWidth / 2;
385
+ }
386
+ } else {
387
+ // Target fits within viewport width - center element relative to target
388
+ elementPositionLeft = targetLeft + targetWidth / 2 - elementWidth / 2;
389
+ // Special handling when element is wider than target
390
+ if (alignToViewportEdgeWhenTargetNearEdge) {
391
+ const elementIsWiderThanTarget = elementWidth > targetWidth;
392
+ const targetIsNearLeftEdge =
393
+ targetLeft < alignToViewportEdgeWhenTargetNearEdge;
394
+ if (elementIsWiderThanTarget && targetIsNearLeftEdge) {
395
+ elementPositionLeft = 0; // Left edge of viewport
396
+ }
397
+ }
398
+ }
399
+ // Constrain horizontal position to viewport boundaries
400
+ if (elementPositionLeft < 0) {
401
+ elementPositionLeft = 0;
402
+ } else if (elementPositionLeft + elementWidth > viewportWidth) {
403
+ elementPositionLeft = viewportWidth - elementWidth;
404
+ }
405
+ }
406
+
407
+ // Calculate vertical position (viewport-relative)
408
+ let position;
409
+ const spaceAboveTarget = targetTop;
410
+ const spaceBelowTarget = viewportHeight - targetBottom;
411
+ determine_position: {
412
+ if (forcePosition) {
413
+ position = forcePosition;
414
+ break determine_position;
415
+ }
416
+ const preferredPosition = element.getAttribute("data-position");
417
+ const minContentVisibilityRatio = 0.6; // 60% minimum visibility to keep position
418
+ if (preferredPosition) {
419
+ // Element has a preferred position - try to keep it unless we really struggle
420
+ const visibleRatio =
421
+ preferredPosition === "above"
422
+ ? spaceAboveTarget / elementHeight
423
+ : spaceBelowTarget / elementHeight;
424
+ const canShowMinimumContent = visibleRatio >= minContentVisibilityRatio;
425
+ if (canShowMinimumContent) {
426
+ position = preferredPosition;
427
+ break determine_position;
428
+ }
429
+ }
430
+ // No preferred position - use original logic (prefer below, fallback to above if more space)
431
+ const elementFitsBelow = spaceBelowTarget >= elementHeight;
432
+ if (elementFitsBelow) {
433
+ position = "below";
434
+ break determine_position;
435
+ }
436
+ const hasMoreSpaceBelow = spaceBelowTarget >= spaceAboveTarget;
437
+ position = hasMoreSpaceBelow ? "below" : "above";
438
+ }
439
+
440
+ let elementPositionTop;
441
+ {
442
+ if (position === "below") {
443
+ // Calculate top position when placing below target (ensure whole pixels)
444
+ const idealTopWhenBelow = targetBottom;
445
+ elementPositionTop =
446
+ idealTopWhenBelow % 1 === 0
447
+ ? idealTopWhenBelow
448
+ : Math.floor(idealTopWhenBelow) + 1;
449
+ } else {
450
+ // Calculate top position when placing above target
451
+ const idealTopWhenAbove = targetTop - elementHeight;
452
+ const minimumTopInViewport = 0;
453
+ elementPositionTop =
454
+ idealTopWhenAbove < minimumTopInViewport
455
+ ? minimumTopInViewport
456
+ : idealTopWhenAbove;
457
+ }
458
+ }
459
+
460
+ // Get document scroll for final coordinate conversion
461
+ const { scrollLeft, scrollTop } = document.documentElement;
462
+ const elementDocumentLeft = elementPositionLeft + scrollLeft;
463
+ const elementDocumentTop = elementPositionTop + scrollTop;
464
+ const targetDocumentLeft = targetLeft + scrollLeft;
465
+ const targetDocumentTop = targetTop + scrollTop;
466
+ const targetDocumentRight = targetRight + scrollLeft;
467
+ const targetDocumentBottom = targetBottom + scrollTop;
468
+
469
+ return {
470
+ position,
471
+ left: elementDocumentLeft,
472
+ top: elementDocumentTop,
473
+ width: elementWidth,
474
+ height: elementHeight,
475
+ targetLeft: targetDocumentLeft,
476
+ targetTop: targetDocumentTop,
477
+ targetRight: targetDocumentRight,
478
+ targetBottom: targetDocumentBottom,
479
+ spaceAboveTarget,
480
+ spaceBelowTarget,
481
+ };
482
+ };
package/src/pub_sub.js ADDED
@@ -0,0 +1,28 @@
1
+ export const createPubSub = () => {
2
+ const callbackSet = new Set();
3
+
4
+ const publish = (...args) => {
5
+ const results = [];
6
+ for (const callback of callbackSet) {
7
+ const result = callback(...args);
8
+ results.push(result);
9
+ }
10
+ return results;
11
+ };
12
+
13
+ const subscribe = (callback) => {
14
+ if (typeof callback !== "function") {
15
+ throw new TypeError("callback must be a function");
16
+ }
17
+ callbackSet.add(callback);
18
+ return () => {
19
+ callbackSet.delete(callback);
20
+ };
21
+ };
22
+
23
+ const clear = () => {
24
+ callbackSet.clear();
25
+ };
26
+
27
+ return [publish, subscribe, clear];
28
+ };
@@ -0,0 +1,11 @@
1
+ export const canTakeSize = (element) => {
2
+ const computedStyle = window.getComputedStyle(element);
3
+
4
+ if (computedStyle.display === "none") {
5
+ return false;
6
+ }
7
+ if (computedStyle.position === "absolute") {
8
+ return false;
9
+ }
10
+ return true;
11
+ };
@@ -0,0 +1,63 @@
1
+ import { addAttributeEffect } from "../attr/add_attribute_effect.js";
2
+ import { getHeight } from "./get_height.js";
3
+
4
+ export const ensureDetailsContentFullHeight = (details) => {
5
+ const updateHeight = () => {
6
+ if (!details.open) {
7
+ return;
8
+ }
9
+ let summary = details.querySelector("summary");
10
+ const summaryNextSiblingSet = new Set();
11
+ {
12
+ let child = summary;
13
+ let nextElementSibling;
14
+ while ((nextElementSibling = child.nextElementSibling)) {
15
+ nextElementSibling.style.height = "auto";
16
+ summaryNextSiblingSet.add(nextElementSibling);
17
+ child = nextElementSibling;
18
+ }
19
+ }
20
+
21
+ const detailsHeight = getHeight(details);
22
+ let summaryHeight = getHeight(summary);
23
+ let heightBefore = summaryHeight;
24
+ for (const nextElementSibling of summaryNextSiblingSet) {
25
+ const contentHeight = detailsHeight - heightBefore;
26
+ nextElementSibling.style.height = `${contentHeight}px`;
27
+ }
28
+ };
29
+
30
+ updateHeight();
31
+
32
+ const cleanupCallbackSet = new Set();
33
+ update_on_size_change: {
34
+ const resizeObserver = new ResizeObserver(() => {
35
+ requestAnimationFrame(() => {
36
+ updateHeight();
37
+ });
38
+ });
39
+ resizeObserver.observe(details);
40
+ cleanupCallbackSet.add(() => {
41
+ resizeObserver.disconnect();
42
+ });
43
+ }
44
+ update_on_toggle: {
45
+ const ontoggle = () => {
46
+ updateHeight();
47
+ };
48
+ details.addEventListener("toggle", ontoggle);
49
+ cleanupCallbackSet.add(() => {
50
+ details.removeEventListener("toggle", ontoggle);
51
+ });
52
+ }
53
+ return () => {
54
+ for (const cleanupCallback of cleanupCallbackSet) {
55
+ cleanupCallback();
56
+ }
57
+ cleanupCallbackSet.clear();
58
+ };
59
+ };
60
+
61
+ addAttributeEffect("data-details-content-full-height", (details) => {
62
+ return ensureDetailsContentFullHeight(details);
63
+ });