@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,32 @@
1
+ export const resolveCSSSize = (
2
+ size,
3
+ { availableSize, fontSize, autoIsRelativeToFont } = {},
4
+ ) => {
5
+ if (typeof size === "string") {
6
+ if (size === "auto") {
7
+ return autoIsRelativeToFont ? fontSize : availableSize;
8
+ }
9
+ if (size.endsWith("%")) {
10
+ return availableSize * (parseFloat(size) / 100);
11
+ }
12
+ if (size.endsWith("px")) {
13
+ return parseFloat(size);
14
+ }
15
+ if (size.endsWith("em")) {
16
+ return parseFloat(size) * fontSize;
17
+ }
18
+ if (size.endsWith("rem")) {
19
+ return (
20
+ parseFloat(size) * getComputedStyle(document.documentElement).fontSize
21
+ );
22
+ }
23
+ if (size.endsWith("vw")) {
24
+ return (parseFloat(size) / 100) * window.innerWidth;
25
+ }
26
+ if (size.endsWith("vh")) {
27
+ return (parseFloat(size) / 100) * window.innerHeight;
28
+ }
29
+ return parseFloat(size);
30
+ }
31
+ return size;
32
+ };
@@ -0,0 +1,97 @@
1
+ import { elementToOwnerWindow } from "../utils.js";
2
+
3
+ export const getComputedStyle = (element) =>
4
+ elementToOwnerWindow(element).getComputedStyle(element);
5
+
6
+ export const getStyle = (element, name) =>
7
+ getComputedStyle(element).getPropertyValue(name);
8
+
9
+ const isCamelCase = (str) => {
10
+ // Check if string contains lowercase letter followed by uppercase letter (camelCase pattern)
11
+ return /[a-z][A-Z]/.test(str);
12
+ };
13
+ const kebabCase = (str) => {
14
+ // Convert camelCase to kebab-case by inserting a hyphen before uppercase letters
15
+ // and converting the uppercase letter to lowercase
16
+ return str.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase();
17
+ };
18
+ export const setStyle = (element, name, value) => {
19
+ if (import.meta.dev) {
20
+ if (isCamelCase(name)) {
21
+ console.warn(
22
+ `setStyle: style name "${name}" should be in kebab-case, not camelCase. Use "${kebabCase(name)}" instead.`,
23
+ );
24
+ }
25
+ }
26
+
27
+ const prevValue = element.style[name];
28
+ if (prevValue) {
29
+ element.style.setProperty(name, value);
30
+ return () => {
31
+ element.style.setProperty(name, prevValue);
32
+ };
33
+ }
34
+ element.style.setProperty(name, value);
35
+ return () => {
36
+ element.style.removeProperty(name);
37
+ };
38
+ };
39
+ export const forceStyle = (element, name, value) => {
40
+ const inlineStyleValue = element.style[name];
41
+ if (inlineStyleValue === value) {
42
+ return () => {};
43
+ }
44
+ const computedStyleValue = getStyle(element, name);
45
+ if (computedStyleValue === value) {
46
+ return () => {};
47
+ }
48
+ const restoreStyle = setStyle(element, name, value);
49
+ return restoreStyle;
50
+ };
51
+
52
+ export const addWillChange = (element, property) => {
53
+ const currentWillChange = element.style.willChange;
54
+ const willChangeValues = currentWillChange
55
+ ? currentWillChange
56
+ .split(",")
57
+ .map((v) => v.trim())
58
+ .filter(Boolean)
59
+ : [];
60
+
61
+ if (willChangeValues.includes(property)) {
62
+ // Property already exists, return no-op
63
+ return () => {};
64
+ }
65
+
66
+ willChangeValues.push(property);
67
+ element.style.willChange = willChangeValues.join(", ");
68
+ // Return function to remove only this property
69
+ return () => {
70
+ const newValues = willChangeValues.filter((v) => v !== property);
71
+ if (newValues.length === 0) {
72
+ element.style.removeProperty("will-change");
73
+ } else {
74
+ element.style.willChange = newValues.join(", ");
75
+ }
76
+ };
77
+ };
78
+
79
+ const createSetMany = (setter) => {
80
+ return (element, description) => {
81
+ const cleanupCallbackSet = new Set();
82
+ for (const name of Object.keys(description)) {
83
+ const value = description[name];
84
+ const restoreStyle = setter(element, name, value);
85
+ cleanupCallbackSet.add(restoreStyle);
86
+ }
87
+ return () => {
88
+ for (const cleanupCallback of cleanupCallbackSet) {
89
+ cleanupCallback();
90
+ }
91
+ cleanupCallbackSet.clear();
92
+ };
93
+ };
94
+ };
95
+
96
+ export const setStyles = createSetMany(setStyle);
97
+ export const forceStyles = createSetMany(forceStyle);
@@ -0,0 +1,78 @@
1
+ import { parseCSSTransform, stringifyCSSTransform } from "./style_parsing.js";
2
+
3
+ // Merge two style objects, handling special cases like transform
4
+ export const mergeStyles = (stylesA, stylesB) => {
5
+ const result = { ...stylesA };
6
+ for (const key of Object.keys(stylesB)) {
7
+ if (key === "transform") {
8
+ result[key] = mergeOneStyle(stylesA[key], stylesB[key], key);
9
+ } else {
10
+ result[key] = stylesB[key];
11
+ }
12
+ }
13
+ return result;
14
+ };
15
+
16
+ // Merge a single style property value with an existing value
17
+ export const mergeOneStyle = (
18
+ existingValue,
19
+ newValue,
20
+ propertyName,
21
+ context = "js",
22
+ ) => {
23
+ if (propertyName === "transform") {
24
+ // Matrix parsing is now handled automatically in parseCSSTransform
25
+
26
+ // Determine the types
27
+ const existingIsString =
28
+ typeof existingValue === "string" && existingValue !== "none";
29
+ const newIsString = typeof newValue === "string" && newValue !== "none";
30
+ const existingIsObject =
31
+ typeof existingValue === "object" && existingValue !== null;
32
+ const newIsObject = typeof newValue === "object" && newValue !== null;
33
+
34
+ // Case 1: Both are objects - merge directly
35
+ if (existingIsObject && newIsObject) {
36
+ const merged = { ...existingValue, ...newValue };
37
+ return context === "css" ? stringifyCSSTransform(merged) : merged;
38
+ }
39
+
40
+ // Case 2: New is object, existing is string - parse existing and merge
41
+ if (newIsObject && existingIsString) {
42
+ const parsedExisting = parseCSSTransform(existingValue);
43
+ const merged = { ...parsedExisting, ...newValue };
44
+ return context === "css" ? stringifyCSSTransform(merged) : merged;
45
+ }
46
+
47
+ // Case 3: New is string, existing is object - parse new and merge
48
+ if (newIsString && existingIsObject) {
49
+ const parsedNew = parseCSSTransform(newValue);
50
+ const merged = { ...existingValue, ...parsedNew };
51
+ return context === "css" ? stringifyCSSTransform(merged) : merged;
52
+ }
53
+
54
+ // Case 4: Both are strings - parse both and merge
55
+ if (existingIsString && newIsString) {
56
+ const parsedExisting = parseCSSTransform(existingValue);
57
+ const parsedNew = parseCSSTransform(newValue);
58
+ const merged = { ...parsedExisting, ...parsedNew };
59
+ return context === "css" ? stringifyCSSTransform(merged) : merged;
60
+ }
61
+
62
+ // Case 5: New is object, no existing or existing is none/null
63
+ if (newIsObject) {
64
+ return context === "css" ? stringifyCSSTransform(newValue) : newValue;
65
+ }
66
+
67
+ // Case 6: New is string, no existing or existing is none/null
68
+ if (newIsString) {
69
+ if (context === "css") {
70
+ return newValue; // Already a string
71
+ }
72
+ return parseCSSTransform(newValue); // Convert to object
73
+ }
74
+ }
75
+
76
+ // For all other properties, simple replacement
77
+ return newValue;
78
+ };
@@ -0,0 +1,345 @@
1
+ /**
2
+ * Style Controller System
3
+ *
4
+ * Solves CSS style manipulation problems in JavaScript:
5
+ *
6
+ * ## Main problems:
7
+ * 1. **Temporary style override**: Code wants to read current style, force another style,
8
+ * then restore original. With inline styles this is ugly and loses original info.
9
+ * 2. **Multiple code parts**: When different parts of code want to touch styles simultaneously,
10
+ * they step on each other (rare but happens).
11
+ * 3. **Transform composition**: CSS transforms are especially painful - you want to keep
12
+ * existing transforms but force specific parts (e.g., keep `rotate(45deg)` but override
13
+ * `translateX`). Native CSS overwrites the entire transform property.
14
+ *
15
+ * ## Solution:
16
+ * Controller pattern + Web Animations API to preserve inline styles. Code that sets
17
+ * inline styles expects to find them unchanged - we use animations for clean override:
18
+ *
19
+ * ```js
20
+ * const controller = createStyleController("myFeature");
21
+ *
22
+ * // Smart value conversion (100 → "100px", 45 → "45deg")
23
+ * controller.set(element, {
24
+ * transform: { translateX: 100, rotate: 45 }, // Individual transform properties
25
+ * opacity: 0.5
26
+ * });
27
+ *
28
+ * // Transform objects merged intelligently
29
+ * controller.set(element, {
30
+ * transform: { translateX: 50 } // Merges with existing transforms
31
+ * });
32
+ *
33
+ * // Get underlying value without this controller's influence
34
+ * const originalOpacity = controller.getUnderlyingValue(element, "opacity");
35
+ * const originalTranslateX = controller.getUnderlyingValue(element, "transform.translateX"); // Magic dot notation!
36
+ * const actualWidth = controller.getUnderlyingValue(element, "rect.width"); // Layout measurements
37
+ *
38
+ * controller.delete(element, "opacity"); // Only removes opacity, keeps transform
39
+ * controller.clear(element); // Removes all styles from this controller only
40
+ * controller.clearAll(); // Cleanup when done
41
+ * ```
42
+ *
43
+ * **Key features:**
44
+ * - **Transform composition**: Intelligently merges transform components instead of overwriting
45
+ * - **Magic properties**: Access transform components with dot notation (e.g., "transform.translateX")
46
+ * - **Layout measurements**: Access actual rendered dimensions with rect.* (e.g., "rect.width")
47
+ * - **getUnderlyingValue()**: Read the "natural" value without this controller's influence
48
+ * - **Smart units**: Numeric values get appropriate units automatically (px, deg, unitless)
49
+ *
50
+ * **Transform limitations:**
51
+ * - **3D Transforms**: Complex `matrix3d()` transforms are preserved as-is and cannot be decomposed
52
+ * into individual properties. Only `matrix3d()` that represent simple 2D transforms are converted
53
+ * to object notation. Magic properties like "transform.rotateX" work only with explicit CSS functions,
54
+ * not with complex 3D matrices.
55
+ *
56
+ * Multiple controllers can safely manage the same element without conflicts.
57
+ */
58
+
59
+ import { mergeOneStyle, mergeStyles } from "./style_composition.js";
60
+ import { normalizeStyle, normalizeStyles } from "./style_parsing.js";
61
+
62
+ // Global registry to track which controllers are managing each element's styles
63
+ const elementControllerSetRegistry = new WeakMap(); // element -> Set<controller>
64
+
65
+ // Top-level helpers for controller attachment tracking
66
+ const onElementControllerAdded = (element, controller) => {
67
+ if (!elementControllerSetRegistry.has(element)) {
68
+ elementControllerSetRegistry.set(element, new Set());
69
+ }
70
+ const elementControllerSet = elementControllerSetRegistry.get(element);
71
+ elementControllerSet.add(controller);
72
+ };
73
+ const onElementControllerRemoved = (element, controller) => {
74
+ const elementControllerSet = elementControllerSetRegistry.get(element);
75
+ if (elementControllerSet) {
76
+ elementControllerSet.delete(controller);
77
+
78
+ // Clean up empty element registry
79
+ if (elementControllerSet.size === 0) {
80
+ elementControllerSetRegistry.delete(element);
81
+ }
82
+ }
83
+ };
84
+
85
+ export const createStyleController = (name = "anonymous") => {
86
+ // Store element data for this controller: element -> { styles, animation }
87
+ const elementWeakMap = new WeakMap();
88
+
89
+ const set = (element, stylesToSet) => {
90
+ if (!element || typeof element !== "object") {
91
+ throw new Error("Element must be a valid DOM element");
92
+ }
93
+ if (!stylesToSet || typeof stylesToSet !== "object") {
94
+ throw new Error("styles must be an object");
95
+ }
96
+
97
+ const normalizedStylesToSet = normalizeStyles(stylesToSet, "js");
98
+ const elementData = elementWeakMap.get(element);
99
+ if (!elementData) {
100
+ const animation = createAnimationForStyles(
101
+ element,
102
+ normalizedStylesToSet,
103
+ name,
104
+ );
105
+ elementWeakMap.set(element, {
106
+ styles: normalizedStylesToSet,
107
+ animation,
108
+ });
109
+ onElementControllerAdded(element, controller);
110
+ return;
111
+ }
112
+
113
+ const { styles, animation } = elementData;
114
+ const mergedStyles = mergeStyles(styles, normalizedStylesToSet);
115
+ elementData.styles = mergedStyles;
116
+ updateAnimationStyles(animation, mergedStyles);
117
+ };
118
+
119
+ const get = (element, propertyName) => {
120
+ const elementData = elementWeakMap.get(element);
121
+ if (!elementData) {
122
+ return undefined;
123
+ }
124
+ const { styles } = elementData;
125
+ if (propertyName === undefined) {
126
+ return { ...styles };
127
+ }
128
+ if (propertyName.startsWith("transform.")) {
129
+ const transformProp = propertyName.slice("transform.".length);
130
+ return styles.transform?.[transformProp];
131
+ }
132
+ return styles[propertyName];
133
+ };
134
+
135
+ const deleteMethod = (element, propertyName) => {
136
+ const elementData = elementWeakMap.get(element);
137
+ if (!elementData) {
138
+ return;
139
+ }
140
+ const { styles, animation } = elementData;
141
+ const hasStyle = Object.hasOwn(styles, propertyName);
142
+ if (!hasStyle) {
143
+ return;
144
+ }
145
+ delete styles[propertyName];
146
+ const isEmpty = Object.keys(styles).length === 0;
147
+ // Clean up empty controller
148
+ if (isEmpty) {
149
+ animation.cancel();
150
+ elementWeakMap.delete(element);
151
+ onElementControllerRemoved(element, controller);
152
+ return;
153
+ }
154
+ updateAnimationStyles(animation, styles);
155
+ };
156
+
157
+ const commit = (element) => {
158
+ const elementData = elementWeakMap.get(element);
159
+ if (!elementData) {
160
+ return; // Nothing to commit on this element for this controller
161
+ }
162
+ const { styles, animation } = elementData;
163
+ // Cancel our animation permanently since we're committing styles to inline
164
+ // (Keep this BEFORE getComputedStyle to prevent computedStyle reading our animation styles)
165
+ animation.cancel();
166
+ // Now read the true underlying styles (without our animation influence)
167
+ const computedStyles = getComputedStyle(element);
168
+ // Convert controller styles to CSS and commit to inline styles
169
+ const cssStyles = normalizeStyles(styles, "css");
170
+ for (const [key, value] of Object.entries(cssStyles)) {
171
+ // Merge with existing computed styles for all properties
172
+ const existingValue = computedStyles[key];
173
+ element.style[key] = mergeOneStyle(existingValue, value, key, "css");
174
+ }
175
+ // Clear this controller's styles since they're now inline
176
+ elementWeakMap.delete(element);
177
+ // Clean up controller from element registry
178
+ onElementControllerRemoved(element, controller);
179
+ };
180
+
181
+ const clear = (element) => {
182
+ const elementData = elementWeakMap.get(element);
183
+ if (!elementData) {
184
+ return;
185
+ }
186
+ const { animation } = elementData;
187
+ animation.cancel();
188
+ elementWeakMap.delete(element);
189
+ onElementControllerRemoved(element, controller);
190
+ };
191
+
192
+ const getUnderlyingValue = (element, propertyName) => {
193
+ const elementControllerSet = elementControllerSetRegistry.get(element);
194
+
195
+ const normalizeValueForJs = (value) => {
196
+ // Use normalizeStyle to handle all property types including transform dot notation
197
+ return normalizeStyle(value, propertyName, "js");
198
+ };
199
+
200
+ const getFromOtherControllers = () => {
201
+ if (!elementControllerSet || elementControllerSet.size <= 1) {
202
+ return undefined;
203
+ }
204
+
205
+ let resultValue;
206
+ for (const otherController of elementControllerSet) {
207
+ if (otherController === controller) continue;
208
+ const otherStyles = otherController.get(element);
209
+ if (propertyName in otherStyles) {
210
+ resultValue = mergeOneStyle(
211
+ resultValue,
212
+ otherStyles[propertyName],
213
+ propertyName,
214
+ );
215
+ }
216
+ }
217
+
218
+ // Note: For CSS width/height properties, we can trust the values from other controllers
219
+ // because we assume box-sizing: border-box. If the element used content-box,
220
+ // the CSS width/height would differ from getBoundingClientRect() due to padding/borders,
221
+ // but since controllers set the final rendered size, the CSS value is what matters.
222
+ // For actual layout measurements, use rect.* properties instead.
223
+ return normalizeValueForJs(resultValue);
224
+ };
225
+
226
+ const getFromDOM = () => {
227
+ // Handle transform dot notation
228
+ if (propertyName.startsWith("transform.")) {
229
+ const transformValue = getComputedStyle(element).transform;
230
+ return normalizeValueForJs(transformValue);
231
+ }
232
+ // For all other CSS properties, use computed styles
233
+ const computedValue = getComputedStyle(element)[propertyName];
234
+ return normalizeValueForJs(computedValue);
235
+ };
236
+
237
+ const getFromDOMLayout = () => {
238
+ // For rect.* properties that reflect actual layout, always read from DOM
239
+ // These represent the actual rendered dimensions, bypassing any controller influence
240
+ if (propertyName === "rect.width") {
241
+ return element.getBoundingClientRect().width;
242
+ }
243
+ if (propertyName === "rect.height") {
244
+ return element.getBoundingClientRect().height;
245
+ }
246
+ if (propertyName === "rect.left") {
247
+ return element.getBoundingClientRect().left;
248
+ }
249
+ if (propertyName === "rect.top") {
250
+ return element.getBoundingClientRect().top;
251
+ }
252
+ if (propertyName === "rect.right") {
253
+ return element.getBoundingClientRect().right;
254
+ }
255
+ if (propertyName === "rect.bottom") {
256
+ return element.getBoundingClientRect().bottom;
257
+ }
258
+ return undefined;
259
+ };
260
+
261
+ const getWhileDisablingThisController = (fn) => {
262
+ const elementData = elementWeakMap.get(element);
263
+ if (!elementData) {
264
+ return fn();
265
+ }
266
+ const { styles, animation } = elementData;
267
+ // Temporarily cancel our animation to read underlying value
268
+ animation.cancel();
269
+ const underlyingValue = fn();
270
+ // Restore our animation
271
+ elementData.animation = createAnimationForStyles(element, styles, name);
272
+ return underlyingValue;
273
+ };
274
+
275
+ if (typeof propertyName === "function") {
276
+ return getWhileDisablingThisController(propertyName);
277
+ }
278
+
279
+ // Handle computed layout properties (rect.*) - always read from DOM, bypass controllers
280
+ if (propertyName.startsWith("rect.")) {
281
+ return getWhileDisablingThisController(getFromDOMLayout);
282
+ }
283
+ if (!elementControllerSet || !elementControllerSet.has(controller)) {
284
+ // This controller is not applied, just read current value
285
+ return getFromDOM();
286
+ }
287
+ // Check if other controllers would provide this style
288
+ const valueFromOtherControllers = getFromOtherControllers();
289
+ if (valueFromOtherControllers !== undefined) {
290
+ return valueFromOtherControllers;
291
+ }
292
+ return getWhileDisablingThisController(getFromDOM);
293
+ };
294
+
295
+ const clearAll = () => {
296
+ // Remove this controller from all elements and clean up animations
297
+ for (const [
298
+ element,
299
+ elementControllerSet,
300
+ ] of elementControllerSetRegistry) {
301
+ if (!elementControllerSet.has(controller)) {
302
+ continue;
303
+ }
304
+ const elementData = elementWeakMap.get(element);
305
+ if (!elementData) {
306
+ continue;
307
+ }
308
+ const { animation } = elementData;
309
+ animation.cancel();
310
+ elementWeakMap.delete(element);
311
+ onElementControllerRemoved(element, controller);
312
+ }
313
+ };
314
+ const controller = {
315
+ name,
316
+ set,
317
+ get,
318
+ delete: deleteMethod,
319
+ getUnderlyingValue,
320
+ commit,
321
+ clear,
322
+ clearAll,
323
+ };
324
+
325
+ return controller;
326
+ };
327
+
328
+ const createAnimationForStyles = (element, styles, id) => {
329
+ const cssStylesToSet = normalizeStyles(styles, "css");
330
+ const animation = element.animate([cssStylesToSet], {
331
+ duration: 0,
332
+ fill: "forwards",
333
+ });
334
+ animation.id = id; // Set a debug name for this animation
335
+ animation.play();
336
+ animation.pause();
337
+ return animation; // Return the created animation
338
+ };
339
+
340
+ const updateAnimationStyles = (animation, styles) => {
341
+ const cssStyles = normalizeStyles(styles, "css");
342
+ animation.effect.setKeyframes([cssStyles]);
343
+ animation.play();
344
+ animation.pause();
345
+ };