@inc2734/unitone-css 0.94.2 → 0.95.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 (163) hide show
  1. package/dist/app.css +1 -1
  2. package/dist/app.js +1 -1
  3. package/dist/behaviors/dividers.js +1 -0
  4. package/dist/behaviors/index.js +1 -0
  5. package/dist/behaviors/stairs.js +1 -0
  6. package/dist/compatibility/fluid-typography.js +1 -0
  7. package/dist/compatibility/index.js +1 -0
  8. package/dist/layout-primitives/both-sides/react.js +1 -0
  9. package/dist/layout-primitives/center/react.js +1 -0
  10. package/dist/layout-primitives/cluster/react.js +1 -0
  11. package/dist/layout-primitives/container/react.js +1 -0
  12. package/dist/layout-primitives/cover/react.js +1 -0
  13. package/dist/layout-primitives/decorator/react.js +1 -0
  14. package/dist/layout-primitives/float/react.js +1 -0
  15. package/dist/layout-primitives/frame/react.js +1 -0
  16. package/dist/layout-primitives/gutters/react.js +1 -0
  17. package/dist/layout-primitives/index.js +1 -0
  18. package/dist/layout-primitives/layers/react.js +1 -0
  19. package/dist/layout-primitives/marquee/behavior.js +1 -0
  20. package/dist/layout-primitives/marquee/react.js +1 -0
  21. package/dist/layout-primitives/masonry/react.js +1 -0
  22. package/dist/layout-primitives/reel/react.js +1 -0
  23. package/dist/layout-primitives/responsive-grid/react.js +1 -0
  24. package/dist/layout-primitives/stack/react.js +1 -0
  25. package/dist/layout-primitives/switcher/react.js +1 -0
  26. package/dist/layout-primitives/text/react.js +1 -0
  27. package/dist/layout-primitives/texture/react.js +1 -0
  28. package/dist/layout-primitives/vertical-writing/behavior.js +1 -0
  29. package/dist/layout-primitives/vertical-writing/react.js +1 -0
  30. package/dist/layout-primitives/with-sidebar/react.js +1 -0
  31. package/dist/library.js +1 -1
  32. package/package.json +24 -19
  33. package/src/app.js +3 -123
  34. package/src/app.scss +4 -4
  35. package/src/behaviors/_index.scss +43 -0
  36. package/src/{helper → behaviors}/_typography.scss +1 -1
  37. package/src/behaviors/dividers.js +8 -0
  38. package/src/behaviors/index.js +2 -0
  39. package/src/behaviors/stairs.js +8 -0
  40. package/src/compatibility/fluid-typography.js +18 -0
  41. package/src/compatibility/index.js +1 -0
  42. package/src/foundation/_foundation-core.scss +107 -0
  43. package/src/foundation/_foundation.scss +2 -106
  44. package/src/foundation/_index.scss +1 -0
  45. package/src/helper/_helper.scss +2 -42
  46. package/src/layout-primitives/_index.scss +1 -0
  47. package/src/layout-primitives/_layout-primitives-core.scss +41 -0
  48. package/src/layout-primitives/_layout-primitives.scss +2 -42
  49. package/src/layout-primitives/both-sides/_both-sides-core.scss +31 -0
  50. package/src/layout-primitives/both-sides/_both-sides.scss +2 -30
  51. package/src/layout-primitives/both-sides/_index.scss +1 -0
  52. package/src/layout-primitives/both-sides/react.jsx +1 -0
  53. package/src/layout-primitives/center/_center-core.scss +22 -0
  54. package/src/layout-primitives/center/_center.scss +2 -21
  55. package/src/layout-primitives/center/_index.scss +1 -0
  56. package/src/layout-primitives/center/react.jsx +1 -0
  57. package/src/layout-primitives/cluster/_cluster-core.scss +126 -0
  58. package/src/layout-primitives/cluster/_cluster.scss +2 -125
  59. package/src/layout-primitives/cluster/_index.scss +1 -0
  60. package/src/layout-primitives/cluster/react.jsx +3 -0
  61. package/src/layout-primitives/container/_container-core.scss +18 -0
  62. package/src/layout-primitives/container/_container.scss +2 -17
  63. package/src/layout-primitives/container/_index.scss +1 -0
  64. package/src/layout-primitives/container/react.jsx +1 -0
  65. package/src/layout-primitives/cover/_cover-core.scss +80 -0
  66. package/src/layout-primitives/cover/_cover.scss +2 -79
  67. package/src/layout-primitives/cover/_index.scss +1 -0
  68. package/src/layout-primitives/cover/react.jsx +1 -0
  69. package/src/layout-primitives/decorator/_decorator-core.scss +104 -0
  70. package/src/layout-primitives/decorator/_decorator.scss +2 -103
  71. package/src/layout-primitives/decorator/_index.scss +1 -0
  72. package/src/layout-primitives/decorator/react.jsx +1 -0
  73. package/src/layout-primitives/float/_float-core.scss +29 -0
  74. package/src/layout-primitives/float/_float.scss +2 -28
  75. package/src/layout-primitives/float/_index.scss +1 -0
  76. package/src/layout-primitives/float/react.jsx +1 -0
  77. package/src/layout-primitives/frame/_frame-core.scss +36 -0
  78. package/src/layout-primitives/frame/_frame.scss +2 -35
  79. package/src/layout-primitives/frame/_index.scss +1 -0
  80. package/src/layout-primitives/frame/react.jsx +1 -0
  81. package/src/layout-primitives/gutters/_gutters-core.scss +12 -0
  82. package/src/layout-primitives/gutters/_gutters.scss +2 -11
  83. package/src/layout-primitives/gutters/_index.scss +1 -0
  84. package/src/layout-primitives/gutters/react.jsx +1 -0
  85. package/src/layout-primitives/index.js +2 -20
  86. package/src/layout-primitives/layers/_index.scss +1 -0
  87. package/src/layout-primitives/layers/_layers-core.scss +139 -0
  88. package/src/layout-primitives/layers/_layers.scss +2 -138
  89. package/src/layout-primitives/layers/react.jsx +1 -0
  90. package/src/layout-primitives/marquee/_index.scss +1 -0
  91. package/src/layout-primitives/marquee/_marquee-core.scss +73 -0
  92. package/src/layout-primitives/marquee/_marquee.scss +2 -72
  93. package/src/layout-primitives/marquee/behavior.js +8 -0
  94. package/src/layout-primitives/marquee/react.jsx +3 -0
  95. package/src/layout-primitives/masonry/_index.scss +1 -0
  96. package/src/layout-primitives/masonry/_masonry-core.scss +26 -0
  97. package/src/layout-primitives/masonry/_masonry.scss +2 -25
  98. package/src/layout-primitives/masonry/react.jsx +1 -0
  99. package/src/layout-primitives/reel/_index.scss +1 -0
  100. package/src/layout-primitives/reel/_reel-core.scss +55 -0
  101. package/src/layout-primitives/reel/_reel.scss +2 -54
  102. package/src/layout-primitives/reel/react.jsx +1 -0
  103. package/src/layout-primitives/responsive-grid/_index.scss +1 -0
  104. package/src/layout-primitives/responsive-grid/_responsive-grid-core.scss +249 -0
  105. package/src/layout-primitives/responsive-grid/_responsive-grid.scss +2 -248
  106. package/src/layout-primitives/responsive-grid/react.jsx +4 -0
  107. package/src/layout-primitives/stack/_index.scss +1 -0
  108. package/src/layout-primitives/stack/_stack-core.scss +201 -0
  109. package/src/layout-primitives/stack/_stack.scss +2 -200
  110. package/src/layout-primitives/stack/react.jsx +3 -0
  111. package/src/layout-primitives/switcher/_index.scss +1 -0
  112. package/src/layout-primitives/switcher/_switcher-core.scss +70 -0
  113. package/src/layout-primitives/switcher/_switcher.scss +2 -69
  114. package/src/layout-primitives/switcher/react.jsx +3 -0
  115. package/src/layout-primitives/text/_index.scss +1 -0
  116. package/src/layout-primitives/text/_text-core.scss +169 -0
  117. package/src/layout-primitives/text/_text.scss +2 -168
  118. package/src/layout-primitives/text/react.jsx +1 -0
  119. package/src/layout-primitives/texture/_index.scss +1 -0
  120. package/src/layout-primitives/texture/_texture-core.scss +235 -0
  121. package/src/layout-primitives/texture/_texture.scss +2 -234
  122. package/src/layout-primitives/texture/react.jsx +1 -0
  123. package/src/layout-primitives/vertical-writing/_index.scss +1 -0
  124. package/src/layout-primitives/vertical-writing/_vertical-writing-core.scss +118 -0
  125. package/src/layout-primitives/vertical-writing/_vertical-writing.scss +2 -117
  126. package/src/layout-primitives/vertical-writing/behavior.js +8 -0
  127. package/src/layout-primitives/vertical-writing/react.jsx +3 -0
  128. package/src/layout-primitives/with-sidebar/_index.scss +1 -0
  129. package/src/layout-primitives/with-sidebar/_with-sidebar-core.scss +337 -0
  130. package/src/layout-primitives/with-sidebar/_with-sidebar.scss +2 -336
  131. package/src/layout-primitives/with-sidebar/react.jsx +3 -0
  132. package/src/library.js +807 -321
  133. package/src/register-layout-initializer.js +132 -0
  134. package/src/settings/_html.scss +1 -1
  135. package/src/settings/_index.scss +1 -0
  136. package/src/settings/_root.scss +1 -1
  137. package/src/settings/_settings-core.scss +3 -0
  138. package/src/settings/_settings.scss +3 -3
  139. package/src/variables/_index.scss +1 -0
  140. package/src/variables/_variables-core.scss +78 -0
  141. package/src/variables/_variables.scss +2 -77
  142. package/dist/index.js +0 -1
  143. package/src/index.js +0 -1
  144. /package/src/{helper → behaviors}/_align-content.scss +0 -0
  145. /package/src/{helper → behaviors}/_align-items.scss +0 -0
  146. /package/src/{helper → behaviors}/_align-self.scss +0 -0
  147. /package/src/{helper → behaviors}/_align.scss +0 -0
  148. /package/src/{helper → behaviors}/_auto-phrase.scss +0 -0
  149. /package/src/{helper → behaviors}/_auto-repeat.scss +0 -0
  150. /package/src/{helper → behaviors}/_background-clip.scss +0 -0
  151. /package/src/{helper → behaviors}/_gap.scss +0 -0
  152. /package/src/{helper → behaviors}/_gutters.scss +0 -0
  153. /package/src/{helper → behaviors}/_justify-content.scss +0 -0
  154. /package/src/{helper → behaviors}/_justify-items.scss +0 -0
  155. /package/src/{helper → behaviors}/_justify-self.scss +0 -0
  156. /package/src/{helper → behaviors}/_link-decoration.scss +0 -0
  157. /package/src/{helper → behaviors}/_mix-blend-mode.scss +0 -0
  158. /package/src/{helper → behaviors}/_negative-gap.scss +0 -0
  159. /package/src/{helper → behaviors}/_overflow.scss +0 -0
  160. /package/src/{helper → behaviors}/_padding.scss +0 -0
  161. /package/src/{helper → behaviors}/_position.scss +0 -0
  162. /package/src/{helper → behaviors}/_stairs.scss +0 -0
  163. /package/src/{helper → behaviors}/_text-orientation.scss +0 -0
package/src/library.js CHANGED
@@ -1,3 +1,459 @@
1
+ const layoutAttributeName = 'data-unitone-layout';
2
+ const layoutIntersectionMargin = 200;
3
+ const layoutIntersectionRootMargin = `${layoutIntersectionMargin}px 0px`;
4
+
5
+ /**
6
+ * Returns layout tokens from the target element.
7
+ *
8
+ * @param {Element | null | undefined} element Target element.
9
+ * @returns {string[]} Layout tokens.
10
+ */
11
+ const getLayoutTokens = (element) =>
12
+ (element.getAttribute(layoutAttributeName) ?? '').split(/\s+/).filter(Boolean);
13
+
14
+ /**
15
+ * Returns tokens with the specified values removed.
16
+ *
17
+ * @param {string[]} tokens Source tokens.
18
+ * @param {string[]} removedTokens Tokens to remove.
19
+ * @returns {string[]} Filtered tokens.
20
+ */
21
+ const withoutLayoutTokens = (tokens, removedTokens) =>
22
+ tokens.filter((value) => !removedTokens.includes(value));
23
+
24
+ /**
25
+ * Updates the layout token attribute on the element.
26
+ *
27
+ * @param {Element} element Target element.
28
+ * @param {string[]} tokens Tokens to set.
29
+ * @returns {void}
30
+ */
31
+ const setLayoutTokens = (element, tokens) => {
32
+ const nextValue = tokens.filter(Boolean).join(' ');
33
+ if ((element.getAttribute(layoutAttributeName) ?? '') !== nextValue) {
34
+ element.setAttribute(layoutAttributeName, nextValue);
35
+ }
36
+ };
37
+
38
+ /**
39
+ * Observes target resizes and invokes the callback when a relevant change is detected.
40
+ *
41
+ * @param {Element} target Target element.
42
+ * @param {(target: Element, entry?: ResizeObserverEntry) => void} callback Callback to run.
43
+ * @param {{ getValue?: (entry: ResizeObserverEntry) => unknown, delay?: number }} [options]
44
+ * @returns {ResizeObserver} ResizeObserver instance.
45
+ */
46
+ const createResizeObserver = (target, callback, { getValue, delay = 250 } = {}) => {
47
+ let prevValue;
48
+ let isFirstEntry = true;
49
+
50
+ const observer = new ResizeObserver(
51
+ debounce((entries) => {
52
+ for (const entry of entries) {
53
+ const currentValue = getValue?.(entry);
54
+ if (isFirstEntry) {
55
+ prevValue = currentValue;
56
+ isFirstEntry = false;
57
+ continue;
58
+ }
59
+
60
+ if (undefined === currentValue || currentValue !== prevValue) {
61
+ callback(entry.target, entry);
62
+ prevValue = currentValue;
63
+ }
64
+ }
65
+ }, delay),
66
+ );
67
+
68
+ observer.observe(target);
69
+
70
+ return observer;
71
+ };
72
+
73
+ /**
74
+ * Creates a MutationObserver for the target node.
75
+ *
76
+ * @param {Node} target Target node.
77
+ * @param {MutationObserverInit} options Observer options.
78
+ * @param {(entries: MutationRecord[]) => void} callback Callback to run.
79
+ * @returns {MutationObserver} MutationObserver instance.
80
+ */
81
+ const createMutationObserver = (target, options, callback) => {
82
+ const observer = new MutationObserver((entries) => {
83
+ requestAnimationFrame(() => {
84
+ if (!target?.isConnected) {
85
+ return;
86
+ }
87
+
88
+ callback(entries);
89
+ });
90
+ });
91
+
92
+ observer.observe(target, options);
93
+
94
+ return observer;
95
+ };
96
+
97
+ /**
98
+ * Creates an IntersectionObserver for the target element.
99
+ *
100
+ * @param {Element} target Target element.
101
+ * @param {(entry: IntersectionObserverEntry) => void} callback Callback to run.
102
+ * @returns {IntersectionObserver} IntersectionObserver instance.
103
+ */
104
+ const createIntersectionObserver = (target, callback) => {
105
+ const observer = new IntersectionObserver(
106
+ ([entry]) => {
107
+ if (entry) {
108
+ callback(entry);
109
+ }
110
+ },
111
+ { rootMargin: layoutIntersectionRootMargin },
112
+ );
113
+
114
+ observer.observe(target);
115
+
116
+ return observer;
117
+ };
118
+
119
+ /**
120
+ * Returns a scheduler that coalesces re-application work into a single frame.
121
+ *
122
+ * @param {Element} target Target element.
123
+ * @param {(target: Element) => void} callback Callback to run.
124
+ * @returns {() => void} Schedule function.
125
+ */
126
+ const createScheduledTargetCallback = (target, callback) => {
127
+ let rafId = 0;
128
+ let defaultView;
129
+
130
+ return () => {
131
+ defaultView = target?.ownerDocument?.defaultView;
132
+ if (!defaultView?.requestAnimationFrame) {
133
+ callback(target);
134
+ return;
135
+ }
136
+
137
+ if (rafId) {
138
+ return;
139
+ }
140
+
141
+ rafId = defaultView.requestAnimationFrame(() => {
142
+ rafId = 0;
143
+ defaultView = null;
144
+
145
+ if (target?.isConnected) {
146
+ callback(target);
147
+ }
148
+ });
149
+ };
150
+ };
151
+
152
+ /**
153
+ * Observes resizes on the target and its direct children.
154
+ *
155
+ * @param {Element} target Target element.
156
+ * @param {(target: Element) => void} callback Callback to run.
157
+ * @param {{ getValue?: (entry: ResizeObserverEntry) => unknown, delay?: number, onChildList?: (entries: MutationRecord[]) => void }} [options]
158
+ * @returns {{ resizeObserver: ResizeObserver, mutationObserver: MutationObserver }}
159
+ */
160
+ const createDirectChildrenResizeObserver = (
161
+ target,
162
+ callback,
163
+ { getValue, delay = 250, onChildList } = {},
164
+ ) => {
165
+ const prevValues = new WeakMap();
166
+ const observedChildren = new Set();
167
+
168
+ const observer = new ResizeObserver(
169
+ debounce((entries) => {
170
+ let shouldApply = false;
171
+
172
+ for (const entry of entries) {
173
+ const currentValue = getValue?.(entry);
174
+ if (!prevValues.has(entry.target)) {
175
+ prevValues.set(entry.target, currentValue);
176
+ continue;
177
+ }
178
+
179
+ if (undefined === currentValue || currentValue !== prevValues.get(entry.target)) {
180
+ shouldApply = true;
181
+ }
182
+
183
+ prevValues.set(entry.target, currentValue);
184
+ }
185
+
186
+ if (shouldApply) {
187
+ callback(target);
188
+ }
189
+ }, delay),
190
+ );
191
+
192
+ const syncObservedChildren = () => {
193
+ Array.from(observedChildren).forEach((child) => {
194
+ if (child.parentElement !== target) {
195
+ observer.unobserve(child);
196
+ observedChildren.delete(child);
197
+ prevValues.delete(child);
198
+ }
199
+ });
200
+
201
+ Array.from(target?.children ?? []).forEach((child) => {
202
+ if (observedChildren.has(child)) {
203
+ return;
204
+ }
205
+
206
+ observer.observe(child);
207
+ observedChildren.add(child);
208
+ });
209
+ };
210
+
211
+ observer.observe(target);
212
+ syncObservedChildren();
213
+
214
+ const mutationObserver = createMutationObserver(target, { childList: true }, (entries) => {
215
+ if (!entries.some((entry) => 'childList' === entry.type)) {
216
+ return;
217
+ }
218
+
219
+ syncObservedChildren();
220
+ onChildList?.(entries);
221
+ callback(target);
222
+ });
223
+
224
+ return {
225
+ resizeObserver: observer,
226
+ mutationObserver,
227
+ };
228
+ };
229
+
230
+ /**
231
+ * Observes attribute changes on direct children.
232
+ *
233
+ * @param {Element} target Target element.
234
+ * @param {(target: Element) => void} callback Callback to run.
235
+ * @param {{ attributeFilter: string[], shouldApply: (entry: MutationRecord) => boolean, attributeOldValue?: boolean }} options
236
+ * @returns {{ observer: MutationObserver, syncObservedChildren: () => void }}
237
+ */
238
+ const createDirectChildrenAttributeObserver = (
239
+ target,
240
+ callback,
241
+ { attributeFilter, shouldApply, attributeOldValue = true } = {},
242
+ ) => {
243
+ const observer = new MutationObserver((entries) => {
244
+ requestAnimationFrame(() => {
245
+ if (!target?.isConnected) {
246
+ return;
247
+ }
248
+
249
+ if (
250
+ entries.some(
251
+ (entry) =>
252
+ 'attributes' === entry.type &&
253
+ entry.target.parentElement === target &&
254
+ shouldApply(entry),
255
+ )
256
+ ) {
257
+ callback(target);
258
+ }
259
+ });
260
+ });
261
+
262
+ const syncObservedChildren = () => {
263
+ observer.disconnect();
264
+ Array.from(target?.children ?? []).forEach((child) => {
265
+ observer.observe(child, {
266
+ attributes: true,
267
+ attributeFilter,
268
+ attributeOldValue,
269
+ });
270
+ });
271
+ };
272
+
273
+ syncObservedChildren();
274
+
275
+ return {
276
+ observer,
277
+ syncObservedChildren,
278
+ };
279
+ };
280
+
281
+ /**
282
+ * Creates a bundled observer setup for layout re-application.
283
+ *
284
+ * @param {Element} target Target element.
285
+ * @param {(target: Element) => void} apply Apply function.
286
+ * @param {{
287
+ * getResizeValue?: (entry: ResizeObserverEntry) => unknown,
288
+ * delay?: number,
289
+ * observeResize?: boolean,
290
+ * observeIntersection?: boolean,
291
+ * observeDirectChildrenResize?: boolean,
292
+ * targetMutation?: { options: MutationObserverInit, shouldApply?: (entries: MutationRecord[]) => boolean },
293
+ * directChildMutation?: { attributeFilter: string[], shouldApply: (entry: MutationRecord) => boolean, attributeOldValue?: boolean }
294
+ * }} [options]
295
+ * @returns {void}
296
+ */
297
+ const createLayoutObserver = (
298
+ target,
299
+ apply,
300
+ {
301
+ getResizeValue,
302
+ delay = 250,
303
+ observeResize = true,
304
+ observeIntersection = false,
305
+ observeDirectChildrenResize = false,
306
+ targetMutation,
307
+ directChildMutation,
308
+ } = {},
309
+ ) => {
310
+ const shouldObserveIntersection =
311
+ observeIntersection && 'undefined' !== typeof IntersectionObserver;
312
+ let isIntersecting = !shouldObserveIntersection || isNearViewport(target);
313
+ let needsApply = shouldObserveIntersection && !isIntersecting;
314
+
315
+ const runApply = (element = target) => {
316
+ if (!element?.isConnected) {
317
+ return;
318
+ }
319
+
320
+ if (shouldObserveIntersection && !isIntersecting) {
321
+ needsApply = true;
322
+ return;
323
+ }
324
+
325
+ needsApply = false;
326
+ apply(element);
327
+ };
328
+
329
+ const schedule = createScheduledTargetCallback(target, runApply);
330
+ const scheduleApply = () => {
331
+ if (!target?.isConnected) {
332
+ return;
333
+ }
334
+
335
+ if (shouldObserveIntersection && !isIntersecting) {
336
+ needsApply = true;
337
+ return;
338
+ }
339
+
340
+ needsApply = true;
341
+ schedule();
342
+ };
343
+
344
+ let syncDirectChildAttributes = () => {};
345
+
346
+ const resizeBundle = !observeResize
347
+ ? {
348
+ resizeObserver: null,
349
+ mutationObserver: null,
350
+ }
351
+ : observeDirectChildrenResize
352
+ ? createDirectChildrenResizeObserver(target, scheduleApply, {
353
+ getValue: getResizeValue,
354
+ delay,
355
+ onChildList: () => {
356
+ syncDirectChildAttributes();
357
+ },
358
+ })
359
+ : {
360
+ resizeObserver: createResizeObserver(target, scheduleApply, {
361
+ getValue: getResizeValue,
362
+ delay,
363
+ }),
364
+ };
365
+
366
+ if (shouldObserveIntersection) {
367
+ createIntersectionObserver(target, (entry) => {
368
+ isIntersecting = entry.isIntersecting;
369
+ if (isIntersecting && needsApply) {
370
+ scheduleApply();
371
+ }
372
+ });
373
+ }
374
+
375
+ if (targetMutation?.options) {
376
+ createMutationObserver(target, targetMutation.options, (entries) => {
377
+ if (targetMutation.shouldApply?.(entries) ?? 0 < entries.length) {
378
+ scheduleApply();
379
+ }
380
+ });
381
+ }
382
+
383
+ const directChildAttributeBundle =
384
+ directChildMutation?.attributeFilter && directChildMutation.shouldApply
385
+ ? createDirectChildrenAttributeObserver(target, scheduleApply, {
386
+ attributeFilter: directChildMutation.attributeFilter,
387
+ shouldApply: directChildMutation.shouldApply,
388
+ attributeOldValue: directChildMutation.attributeOldValue,
389
+ })
390
+ : null;
391
+
392
+ if (directChildAttributeBundle) {
393
+ syncDirectChildAttributes = directChildAttributeBundle.syncObservedChildren;
394
+ }
395
+
396
+ if (!resizeBundle.mutationObserver && directChildAttributeBundle) {
397
+ createMutationObserver(target, { childList: true }, (entries) => {
398
+ if (!entries.some((entry) => 'childList' === entry.type)) {
399
+ return;
400
+ }
401
+
402
+ syncDirectChildAttributes();
403
+ scheduleApply();
404
+ });
405
+ }
406
+
407
+ if (!shouldObserveIntersection || isIntersecting) {
408
+ runApply(target);
409
+ }
410
+ };
411
+
412
+ const getBorderBoxInlineSize = (entry) => entry.borderBoxSize?.[0].inlineSize;
413
+
414
+ const getContentRectWidth = (entry) => parseInt(entry.contentRect?.width);
415
+
416
+ const hasLayoutBox = (element) => !!element?.isConnected && 0 < element.getClientRects().length;
417
+
418
+ const isNearViewport = (element) => {
419
+ if (!hasLayoutBox(element)) {
420
+ return false;
421
+ }
422
+
423
+ const defaultView = element?.ownerDocument?.defaultView;
424
+ if (!defaultView) {
425
+ return true;
426
+ }
427
+
428
+ const rect = element.getBoundingClientRect();
429
+ const viewportWidth = defaultView.innerWidth;
430
+ const viewportHeight = defaultView.innerHeight;
431
+
432
+ return (
433
+ rect.bottom > -layoutIntersectionMargin &&
434
+ rect.right > 0 &&
435
+ rect.top < viewportHeight + layoutIntersectionMargin &&
436
+ rect.left < viewportWidth
437
+ );
438
+ };
439
+
440
+ /**
441
+ * Returns whether the mutation list contains a matching attributes record.
442
+ *
443
+ * @param {MutationRecord[]} entries Mutation records.
444
+ * @param {(entry: MutationRecord) => boolean} predicate Match predicate.
445
+ * @returns {boolean} Whether a matching attributes mutation exists.
446
+ */
447
+ const hasAttributeMutation = (entries, predicate) =>
448
+ entries.some((entry) => 'attributes' === entry.type && predicate(entry));
449
+
450
+ /**
451
+ * Coalesces repeated calls into the final call within the delay window.
452
+ *
453
+ * @param {Function} fn Function to wrap.
454
+ * @param {number} delay Delay in milliseconds.
455
+ * @returns {Function} Debounced function.
456
+ */
1
457
  export function debounce(fn, delay) {
2
458
  let timer;
3
459
 
@@ -10,167 +466,178 @@ export function debounce(fn, delay) {
10
466
  };
11
467
  }
12
468
 
469
+ /**
470
+ * Recalculates line wrapping state for divider layouts.
471
+ *
472
+ * @param {Element} target Target element.
473
+ * @returns {void}
474
+ */
13
475
  export const setDividerLinewrap = (target) => {
14
- const firstChild = [].slice.call(target?.children ?? [])?.[0];
476
+ const children = Array.from(target?.children ?? []);
477
+ const firstChild = children[0];
15
478
  if (!firstChild) {
16
479
  return;
17
480
  }
18
481
 
19
- let prevChild;
20
- const baseRect = firstChild.getBoundingClientRect();
482
+ const currentLayoutArray = withoutLayoutTokens(getLayoutTokens(target), [
483
+ 'divider:initialized',
484
+ '-stack',
485
+ ]);
486
+ setLayoutTokens(target, currentLayoutArray);
21
487
 
22
- let currentLayoutArray = (target.getAttribute('data-unitone-layout') ?? '').split(/\s+/);
23
- if (currentLayoutArray.some((value) => ['divider:initialized', '-stack'].includes(value))) {
24
- currentLayoutArray = currentLayoutArray.filter(
25
- (value) => !['divider:initialized', '-stack'].includes(value),
26
- );
27
- target.setAttribute('data-unitone-layout', currentLayoutArray.join(' '));
488
+ const childLayoutMap = new Map();
489
+ children.forEach((child) => {
490
+ const layoutTokens = withoutLayoutTokens(getLayoutTokens(child), ['-bol', '-linewrap']);
491
+ childLayoutMap.set(child, layoutTokens);
492
+ setLayoutTokens(child, layoutTokens);
493
+ });
494
+
495
+ if (!hasLayoutBox(target)) {
496
+ return;
28
497
  }
29
498
 
30
- const targetChildren = [].slice.call(target.children).filter((child) => {
499
+ const baseRect = firstChild.getBoundingClientRect();
500
+ const targetChildren = children.reduce((accumulator, child) => {
31
501
  const position = window.getComputedStyle(child).getPropertyValue('position');
32
502
  const display = window.getComputedStyle(child).getPropertyValue('display');
33
- return 'absolute' !== position && 'fixed' !== position && 'none' !== display;
34
- });
35
-
36
- targetChildren.forEach((child, index) => {
37
- let childCurrentLayoutArray = (child.getAttribute('data-unitone-layout') ?? '').split(/\s+/);
38
- if (childCurrentLayoutArray.some((value) => ['-bol', '-linewrap'].includes(value))) {
39
- childCurrentLayoutArray = childCurrentLayoutArray.filter(
40
- (value) => !['-bol', '-linewrap'].includes(value),
41
- );
42
- child.setAttribute('data-unitone-layout', childCurrentLayoutArray.join(' '));
503
+ if ('absolute' !== position && 'fixed' !== position && 'none' !== display) {
504
+ accumulator.push({
505
+ child,
506
+ layoutTokens: childLayoutMap.get(child) ?? [],
507
+ rect: child.getBoundingClientRect(),
508
+ });
43
509
  }
510
+ return accumulator;
511
+ }, []);
44
512
 
45
- const prevRect = prevChild?.getBoundingClientRect();
46
- const childRect = child.getBoundingClientRect();
513
+ let prevRect;
514
+ const nextChildLayouts = targetChildren.map(({ child, layoutTokens, rect }, index) => {
515
+ const nextLayoutTokens = [...layoutTokens];
47
516
 
48
- let shouldUpdate = false;
49
-
50
- if (0 === index || (prevRect?.top < childRect.top && prevRect?.left >= childRect.left)) {
51
- childCurrentLayoutArray.push('-bol');
52
- shouldUpdate = true;
517
+ if (0 === index || (prevRect?.top < rect.top && prevRect?.left >= rect.left)) {
518
+ nextLayoutTokens.push('-bol');
53
519
  }
54
520
 
55
- if (0 < index && baseRect.top < childRect.top) {
56
- childCurrentLayoutArray.push('-linewrap');
57
- shouldUpdate = true;
521
+ if (0 < index && baseRect.top < rect.top) {
522
+ nextLayoutTokens.push('-linewrap');
58
523
  }
59
524
 
60
- if (shouldUpdate) {
61
- child.setAttribute('data-unitone-layout', childCurrentLayoutArray.join(' '));
62
- }
525
+ prevRect = rect;
526
+ return { child, layoutTokens: nextLayoutTokens };
527
+ });
63
528
 
64
- prevChild = child;
529
+ nextChildLayouts.forEach(({ child, layoutTokens }) => {
530
+ setLayoutTokens(child, layoutTokens);
65
531
  });
66
532
 
67
- const isStack = [].slice
68
- .call(targetChildren)
69
- .every((child) => child.getBoundingClientRect().left === baseRect.left);
533
+ const isStack = targetChildren.every(({ rect }) => rect.left === baseRect.left);
534
+ const nextTargetLayout = [...currentLayoutArray];
70
535
  if (isStack) {
71
- currentLayoutArray.push('-stack');
536
+ nextTargetLayout.push('-stack');
72
537
  }
73
538
 
74
- currentLayoutArray.push('divider:initialized');
75
- target.setAttribute('data-unitone-layout', currentLayoutArray.join(' '));
539
+ nextTargetLayout.push('divider:initialized');
540
+ setLayoutTokens(target, nextTargetLayout);
76
541
  };
77
542
 
543
+ /**
544
+ * Creates the observer bundle for divider layouts.
545
+ *
546
+ * @param {Element} target Target element.
547
+ * @param {{ ignore?: { layout?: string[], className?: string[] } }} [args]
548
+ * @returns {void}
549
+ */
78
550
  export const dividersResizeObserver = (target, args = {}) => {
79
- let prevWidth = 0;
551
+ const shouldRecalculateByAttributeMutation = (entry) => {
552
+ if ('data-unitone-layout' === entry.attributeName) {
553
+ const ignoreUnitoneLayouts = [
554
+ ...(args?.ignore?.layout ?? []),
555
+ ...['divider:initialized', '-bol', '-linewrap', '-stack'],
556
+ ];
557
+
558
+ const current = (entry.target.getAttribute(entry.attributeName) ?? '')
559
+ .split(' ')
560
+ .filter((v) => !ignoreUnitoneLayouts.includes(v))
561
+ .join(' ');
562
+
563
+ const old = (entry.oldValue ?? '')
564
+ .split(' ')
565
+ .filter((v) => !ignoreUnitoneLayouts.includes(v))
566
+ .join(' ');
567
+
568
+ return current !== old;
569
+ }
80
570
 
81
- const observer = new ResizeObserver(
82
- debounce((entries) => {
83
- for (const entry of entries) {
84
- const width = entry.borderBoxSize?.[0].inlineSize;
85
- if (width !== prevWidth) {
86
- setDividerLinewrap(entry.target);
87
- prevWidth = width;
88
- }
89
- }
90
- }, 250),
91
- );
571
+ if ('class' === entry.attributeName) {
572
+ const ignoreClassNames = [...(args?.ignore?.className ?? [])];
92
573
 
93
- const mObserverArgs = {
94
- attributes: true,
95
- attributeFilter: ['style', 'data-unitone-layout', 'class'],
96
- attributeOldValue: true,
97
- subtree: true,
98
- characterData: true,
99
- };
574
+ const current = (entry.target.getAttribute(entry.attributeName) ?? '')
575
+ .split(' ')
576
+ .filter((v) => !ignoreClassNames.includes(v))
577
+ .join(' ');
100
578
 
101
- const mObserver = new MutationObserver((entries) => {
102
- requestAnimationFrame(() => {
103
- for (const entry of entries) {
104
- if ('attributes' === entry.type && 'data-unitone-layout' === entry.attributeName) {
105
- const ignoreUnitoneLayouts = [
106
- ...(args?.ignore?.layout ?? []),
107
- ...['divider:initialized', '-bol', '-linewrap', '-stack'],
108
- ];
109
-
110
- const current = (entry.target.getAttribute(entry.attributeName) ?? '')
111
- .split(' ')
112
- .filter((v) => !ignoreUnitoneLayouts.includes(v))
113
- .join(' ');
114
-
115
- const old = (entry.oldValue ?? '')
116
- .split(' ')
117
- .filter((v) => !ignoreUnitoneLayouts.includes(v))
118
- .join(' ');
119
-
120
- if (current !== old) {
121
- setDividerLinewrap(target);
122
- }
123
- } else if ('attributes' === entry.type && 'class' === entry.attributeName) {
124
- const ignoreClassNames = [...(args?.ignore?.className ?? [])];
125
-
126
- const current = (entry.target.getAttribute(entry.attributeName) ?? '')
127
- .split(' ')
128
- .filter((v) => !ignoreClassNames.includes(v))
129
- .join(' ');
130
-
131
- const old = (entry.oldValue ?? '')
132
- .split(' ')
133
- .filter((v) => !ignoreClassNames.includes(v))
134
- .join(' ');
135
-
136
- if (current !== old) {
137
- setDividerLinewrap(target);
138
- }
139
- } else if ('attributes' === entry.type && 'style' === entry.attributeName) {
140
- setDividerLinewrap(target);
141
- }
142
- }
143
- });
144
- });
579
+ const old = (entry.oldValue ?? '')
580
+ .split(' ')
581
+ .filter((v) => !ignoreClassNames.includes(v))
582
+ .join(' ');
145
583
 
146
- observer.observe(target);
147
- mObserver.observe(target, mObserverArgs);
584
+ return current !== old;
585
+ }
148
586
 
149
- return {
150
- resizeObserver: observer,
151
- mutationObserver: mObserver,
587
+ return 'style' === entry.attributeName;
152
588
  };
589
+
590
+ createLayoutObserver(target, setDividerLinewrap, {
591
+ getResizeValue: getBorderBoxInlineSize,
592
+ observeIntersection: true,
593
+ observeDirectChildrenResize: true,
594
+ targetMutation: {
595
+ options: {
596
+ attributes: true,
597
+ attributeFilter: ['style', 'data-unitone-layout', 'class'],
598
+ attributeOldValue: true,
599
+ },
600
+ shouldApply: (entries) =>
601
+ hasAttributeMutation(
602
+ entries,
603
+ (entry) => entry.target === target && shouldRecalculateByAttributeMutation(entry),
604
+ ),
605
+ },
606
+ directChildMutation: {
607
+ attributeFilter: ['style', 'data-unitone-layout', 'class'],
608
+ attributeOldValue: true,
609
+ shouldApply: shouldRecalculateByAttributeMutation,
610
+ },
611
+ });
153
612
  };
154
613
 
614
+ /**
615
+ * Recalculates step values for stairs layouts.
616
+ *
617
+ * @param {Element} target Target element.
618
+ * @returns {void}
619
+ */
155
620
  export const setStairsStep = (target) => {
156
- const children = [].slice.call(target.children);
157
- const firstChild = children?.[0];
621
+ const children = Array.from(target.children);
622
+ const currentLayoutArray = withoutLayoutTokens(getLayoutTokens(target), ['stairs:initialized']);
623
+ setLayoutTokens(target, currentLayoutArray);
624
+
625
+ const firstChild = children[0];
158
626
  if (!firstChild) {
627
+ setLayoutTokens(target, [...currentLayoutArray, 'stairs:initialized']);
159
628
  return;
160
629
  }
161
630
 
162
631
  // Reset
163
632
  target.style.removeProperty('--unitone--stairs-step-overflow-volume');
164
633
  target.style.removeProperty('--unitone--max-stairs-step');
165
- [].slice.call(target?.children ?? []).forEach((child) => {
634
+ children.forEach((child) => {
166
635
  child.style.removeProperty('--unitone--stairs-step');
167
636
  });
168
637
 
169
- const filteredChildren = [];
170
-
171
- let prevChild;
172
- let stairsStep = 0;
173
- let maxStairsStep = stairsStep;
638
+ if (!hasLayoutBox(target)) {
639
+ return;
640
+ }
174
641
 
175
642
  const stairsUp = (target.getAttribute('data-unitone-layout') ?? '')
176
643
  .split(/\s+/)
@@ -180,22 +647,30 @@ export const setStairsStep = (target) => {
180
647
  const isAlternatingStairs = ['up-down', 'down-up'].includes(stairsUp);
181
648
 
182
649
  const direction = window.getComputedStyle(target).getPropertyValue('flex-direction');
183
-
184
- children.forEach((child) => {
650
+ const targetBottom = target.getBoundingClientRect().bottom;
651
+ const filteredChildren = children.reduce((accumulator, child) => {
185
652
  const position = window.getComputedStyle(child).getPropertyValue('position');
186
653
  const display = window.getComputedStyle(child).getPropertyValue('display');
187
654
  if ('absolute' === position || 'fixed' === position || 'none' === display) {
188
- return;
655
+ return accumulator;
189
656
  }
190
657
 
191
- filteredChildren.push(child);
658
+ accumulator.push({
659
+ child,
660
+ rect: child.getBoundingClientRect(),
661
+ });
662
+ return accumulator;
663
+ }, []);
664
+
665
+ let prevRect;
666
+ let stairsStep = 0;
667
+ let maxStairsStep = stairsStep;
192
668
 
669
+ const nextSteps = filteredChildren.map(({ child, rect }, index) => {
193
670
  const isBol =
194
- 'row-reverse' === direction
195
- ? prevChild?.getBoundingClientRect()?.left <= child.getBoundingClientRect().left
196
- : prevChild?.getBoundingClientRect()?.left >= child.getBoundingClientRect().left;
671
+ 'row-reverse' === direction ? prevRect?.left <= rect.left : prevRect?.left >= rect.left;
197
672
 
198
- if (firstChild === child || isBol) {
673
+ if (0 === index || (firstChild === child && !prevRect) || isBol) {
199
674
  stairsStep = 0;
200
675
  } else if (isAlternatingStairs) {
201
676
  stairsStep = 0 === stairsStep ? 1 : 0;
@@ -203,64 +678,106 @@ export const setStairsStep = (target) => {
203
678
  stairsStep++;
204
679
  }
205
680
 
206
- child.style.setProperty('--unitone--stairs-step', stairsStep);
207
-
208
- prevChild = child;
681
+ prevRect = rect;
209
682
 
210
683
  if (stairsStep > maxStairsStep) {
211
684
  maxStairsStep = stairsStep;
212
685
  }
686
+
687
+ return {
688
+ child,
689
+ stairsStep,
690
+ };
691
+ });
692
+
693
+ nextSteps.forEach(({ child, stairsStep }) => {
694
+ child.style.setProperty('--unitone--stairs-step', stairsStep);
213
695
  });
214
696
 
215
697
  target.style.setProperty('--unitone--max-stairs-step', maxStairsStep);
216
698
 
217
- const targetBottom = target.getBoundingClientRect().bottom;
218
- const overflowVolume = filteredChildren.reduce((accumulator, current) => {
219
- const childBottom = current.getBoundingClientRect().bottom;
220
- const overflow = childBottom - targetBottom;
699
+ const overflowVolume = filteredChildren.reduce((accumulator, { child }) => {
700
+ const overflow = child.getBoundingClientRect().bottom - targetBottom;
221
701
  return accumulator > overflow ? accumulator : overflow;
222
702
  }, 0);
223
703
 
224
704
  target.style.setProperty('--unitone--stairs-step-overflow-volume', overflowVolume);
705
+ setLayoutTokens(target, [...currentLayoutArray, 'stairs:initialized']);
225
706
  };
226
707
 
708
+ /**
709
+ * Creates the observer bundle for stairs layouts.
710
+ *
711
+ * @param {Element} target Target element.
712
+ * @returns {void}
713
+ */
227
714
  export const stairsResizeObserver = (target) => {
228
- const observer = new ResizeObserver(
229
- debounce((entries) => {
230
- for (const entry of entries) {
231
- setStairsStep(entry.target);
232
- }
233
- }, 250),
234
- );
715
+ createLayoutObserver(target, setStairsStep, {
716
+ observeIntersection: true,
717
+ observeDirectChildrenResize: true,
718
+ });
719
+ };
235
720
 
236
- observer.observe(target);
721
+ /**
722
+ * Returns whether the node should be ignored by vertical-writing mutation handling.
723
+ *
724
+ * @param {Node | null | undefined} node Target node.
725
+ * @returns {boolean} Whether the node should be ignored.
726
+ */
727
+ const isIgnoredVerticalWritingMutationNode = (node) =>
728
+ node?.nodeType === Node.ELEMENT_NODE &&
729
+ [
730
+ 'vertical-writing__thresholder',
731
+ 'vertical-writing:initialized',
732
+ 'vertical-writing:safari',
733
+ ].includes(node.getAttribute('data-unitone-layout'));
734
+
735
+ /**
736
+ * Returns whether vertical-writing mutations require re-application.
737
+ *
738
+ * @param {MutationRecord[]} entries Mutation records.
739
+ * @returns {boolean} Whether re-application is required.
740
+ */
741
+ const shouldApplyVerticalWritingMutation = (entries) =>
742
+ entries.some((entry) => {
743
+ if ('attributes' === entry.type) {
744
+ return true;
745
+ }
237
746
 
238
- return observer;
239
- };
747
+ if ('childList' !== entry.type) {
748
+ return false;
749
+ }
240
750
 
751
+ return [...entry.addedNodes, ...entry.removedNodes].some(
752
+ (node) => !isIgnoredVerticalWritingMutationNode(node),
753
+ );
754
+ });
755
+
756
+ /**
757
+ * Recalculates column count and height for vertical-writing layouts.
758
+ *
759
+ * @param {Element} target Target element.
760
+ * @returns {void}
761
+ */
241
762
  export const setColumnCountForVertical = (target) => {
242
763
  if (!target) {
243
764
  return;
244
765
  }
245
766
 
246
- let currentLayoutArray = (target.getAttribute('data-unitone-layout') ?? '').split(/\s+/);
247
- if (
248
- currentLayoutArray.some((value) =>
249
- ['vertical-writing:initialized', 'vertical-writing:safari', '-force-switch'].includes(value),
250
- )
251
- ) {
252
- currentLayoutArray = currentLayoutArray.filter(
253
- (value) =>
254
- !['vertical-writing:initialized', 'vertical-writing:safari', '-force-switch'].includes(
255
- value,
256
- ),
257
- );
258
- target.setAttribute('data-unitone-layout', currentLayoutArray.join(' '));
259
- }
767
+ const currentLayoutTokens = getLayoutTokens(target);
768
+ const baseLayoutTokens = withoutLayoutTokens(currentLayoutTokens, [
769
+ 'vertical-writing:safari',
770
+ '-force-switch',
771
+ ]);
772
+ const nextLayoutTokens = withoutLayoutTokens(currentLayoutTokens, [
773
+ 'vertical-writing:initialized',
774
+ 'vertical-writing:safari',
775
+ '-force-switch',
776
+ ]);
777
+ setLayoutTokens(target, baseLayoutTokens);
260
778
 
261
779
  let lastChild;
262
- [].slice
263
- .call(target.children)
780
+ Array.from(target.children)
264
781
  .reverse()
265
782
  .some((child) => {
266
783
  if (
@@ -273,130 +790,92 @@ export const setColumnCountForVertical = (target) => {
273
790
  });
274
791
 
275
792
  if (!lastChild) {
276
- currentLayoutArray.push('vertical-writing:initialized');
277
- target.setAttribute('data-unitone-layout', currentLayoutArray.join(' '));
793
+ setLayoutTokens(target, [...nextLayoutTokens, 'vertical-writing:initialized']);
278
794
  return;
279
795
  }
280
796
 
281
- // Process of the threshold
282
797
  const computedStyle = getComputedStyle(target);
283
798
  const threshold = String(computedStyle.getPropertyValue('--unitone--threshold')).trim();
284
799
  let forceSwitch = false;
285
800
 
286
- setTimeout(() => {
287
- if (!target?.isConnected) {
288
- return;
801
+ if (threshold) {
802
+ const thresholder = target.ownerDocument.createElement('div');
803
+ thresholder.setAttribute(layoutAttributeName, 'vertical-writing__thresholder');
804
+ target.appendChild(thresholder);
805
+ forceSwitch = thresholder.offsetWidth >= target.offsetWidth;
806
+ thresholder.remove();
807
+ }
808
+
809
+ if (forceSwitch) {
810
+ nextLayoutTokens.push('-force-switch');
811
+ } else {
812
+ const maybeSafari =
813
+ target.getBoundingClientRect().left > lastChild.getBoundingClientRect().left;
814
+ if (maybeSafari) {
815
+ nextLayoutTokens.push('vertical-writing:safari');
289
816
  }
817
+ }
290
818
 
291
- if (!!threshold) {
292
- const thresholder = document.createElement('div');
293
- thresholder.setAttribute('data-unitone-layout', 'vertical-writing__thresholder');
294
- target.appendChild(thresholder);
295
- forceSwitch = thresholder.offsetWidth >= target.offsetWidth;
296
- if (thresholder.parentNode) {
297
- thresholder.parentNode.removeChild(thresholder);
298
- }
819
+ setLayoutTokens(target, [...nextLayoutTokens, 'vertical-writing:initialized']);
820
+
821
+ requestAnimationFrame(() => {
822
+ if (!target?.isConnected) {
823
+ return;
299
824
  }
300
825
 
301
- if (forceSwitch) {
302
- currentLayoutArray.push('-force-switch');
303
- if (target.parentNode?.style) {
826
+ if (target.parentNode?.style) {
827
+ if (forceSwitch) {
304
828
  target.parentNode.style.height = '';
305
- }
306
- } else {
307
- // For Safari
308
- const maybeSafari =
309
- target.getBoundingClientRect().left > lastChild.getBoundingClientRect().left;
310
- if (maybeSafari) {
311
- currentLayoutArray.push('vertical-writing:safari');
829
+ return;
312
830
  }
313
831
 
314
- target.setAttribute('data-unitone-layout', currentLayoutArray.join(' '));
832
+ const targetRect = target.getBoundingClientRect();
833
+ const lastChildRect = lastChild.getBoundingClientRect();
315
834
 
316
- requestAnimationFrame(() => {
317
- if (!target?.isConnected) {
318
- return;
319
- }
320
-
321
- const targetRect = target.getBoundingClientRect();
322
- const lastChildRect = lastChild.getBoundingClientRect();
323
-
324
- const targetY = targetRect.top + targetRect.height;
325
- const lastChildY = lastChildRect.top + lastChildRect.height;
326
- if (targetY !== lastChildY) {
327
- if (target.parentNode?.style) {
328
- target.parentNode.style.height = `${Math.ceil(lastChildY - targetRect.top)}px`;
329
- }
330
- }
331
- });
835
+ const targetY = targetRect.top + targetRect.height;
836
+ const lastChildY = lastChildRect.top + lastChildRect.height;
837
+ target.parentNode.style.height =
838
+ targetY !== lastChildY ? `${Math.ceil(lastChildY - targetRect.top)}px` : '';
332
839
  }
333
-
334
- currentLayoutArray.push('vertical-writing:initialized');
335
- target.setAttribute('data-unitone-layout', currentLayoutArray.join(' '));
336
- }, 250);
840
+ });
337
841
  };
338
842
 
843
+ /**
844
+ * Creates the observer bundle for vertical-writing layouts.
845
+ *
846
+ * @param {Element} target Target element.
847
+ * @returns {void}
848
+ */
339
849
  export const verticalsResizeObserver = (target) => {
340
- let prevWidth = 0;
341
-
342
- const observer = new ResizeObserver(
343
- debounce((entries) => {
344
- for (const entry of entries) {
345
- const width = entry.contentRect?.width;
346
- if (parseInt(width) !== parseInt(prevWidth)) {
347
- prevWidth = width;
348
- entry.target.parentNode.style.height = '';
349
- setColumnCountForVertical(entry.target);
350
- }
351
- }
352
- }, 250),
353
- );
354
-
355
- observer.observe(target);
850
+ const applyVerticalColumns = (element) => {
851
+ if (element.parentNode?.style) {
852
+ element.parentNode.style.height = '';
853
+ }
356
854
 
357
- const mObserverArgs = {
358
- attributes: true,
359
- attributeFilter: ['style'],
360
- childList: true,
361
- subtree: true,
855
+ setColumnCountForVertical(element);
362
856
  };
363
857
 
364
- const mObserver = new MutationObserver((entries) => {
365
- requestAnimationFrame(() => {
366
- if (0 < entries.length) {
367
- const entry = entries[0];
368
- const addedNode = entry.addedNodes?.[0];
369
- const removedNode = entry.removedNodes?.[0];
370
- if (
371
- (addedNode?.nodeType === Node.ELEMENT_NODE &&
372
- 'vertical-writing__thresholder' === addedNode.getAttribute('data-unitone-layout')) ||
373
- (removedNode?.nodeType === Node.ELEMENT_NODE &&
374
- 'vertical-writing__thresholder' === removedNode.getAttribute('data-unitone-layout')) ||
375
- (addedNode?.nodeType === Node.ELEMENT_NODE &&
376
- 'vertical-writing:initialized' === addedNode.getAttribute('data-unitone-layout')) ||
377
- (removedNode?.nodeType === Node.ELEMENT_NODE &&
378
- 'vertical-writing:initialized' === removedNode.getAttribute('data-unitone-layout')) ||
379
- (addedNode?.nodeType === Node.ELEMENT_NODE &&
380
- 'vertical-writing:safari' === addedNode.getAttribute('data-unitone-layout')) ||
381
- (removedNode?.nodeType === Node.ELEMENT_NODE &&
382
- 'vertical-writing:safari' === removedNode.getAttribute('data-unitone-layout'))
383
- ) {
384
- return;
385
- }
386
-
387
- setColumnCountForVertical(target);
388
- }
389
- });
858
+ createLayoutObserver(target, applyVerticalColumns, {
859
+ getResizeValue: getContentRectWidth,
860
+ observeIntersection: true,
861
+ targetMutation: {
862
+ options: {
863
+ attributes: true,
864
+ attributeFilter: ['style'],
865
+ childList: true,
866
+ subtree: true,
867
+ },
868
+ shouldApply: shouldApplyVerticalWritingMutation,
869
+ },
390
870
  });
391
-
392
- mObserver.observe(target, mObserverArgs);
393
-
394
- return {
395
- resizeObserver: observer,
396
- mutationObserver: mObserver,
397
- };
398
871
  };
399
872
 
873
+ /**
874
+ * Writes the computed 1em value to a CSS custom property for Firefox.
875
+ *
876
+ * @param {HTMLElement} target Target element.
877
+ * @returns {void}
878
+ */
400
879
  export const setResult1emPxForFireFox = (target) => {
401
880
  const ownerDocument = target.ownerDocument;
402
881
  const defaultView = ownerDocument.defaultView;
@@ -411,6 +890,12 @@ export const setResult1emPxForFireFox = (target) => {
411
890
  target.style.setProperty('--unitone--result--1em-px', fontSize);
412
891
  };
413
892
 
893
+ /**
894
+ * Creates the observer bundle for the Firefox 1em measurement workaround.
895
+ *
896
+ * @param {HTMLElement} target Target element.
897
+ * @returns {void}
898
+ */
414
899
  export const result1emPxForFireFoxObserver = (target) => {
415
900
  const ownerDocument = target.ownerDocument;
416
901
  const defaultView = ownerDocument.defaultView;
@@ -420,42 +905,28 @@ export const result1emPxForFireFoxObserver = (target) => {
420
905
  return;
421
906
  }
422
907
 
423
- let prevWidth = 0;
424
-
425
- const observer = new ResizeObserver(
426
- debounce((entries) => {
427
- for (const entry of entries) {
428
- const width = entry.borderBoxSize?.[0].inlineSize;
429
- if (width !== prevWidth) {
430
- setResult1emPxForFireFox(entry.target);
431
- prevWidth = width;
432
- }
433
- }
434
- }, 250),
435
- );
436
-
437
- const mObserverArgs = {
438
- attributes: true,
439
- attributeFilter: ['style', 'data-unitone-layout', 'class'],
440
- characterData: true,
441
- };
442
-
443
- const mObserver = new MutationObserver(() => {
444
- requestAnimationFrame(() => {
445
- setResult1emPxForFireFox(target);
446
- });
908
+ createLayoutObserver(target, setResult1emPxForFireFox, {
909
+ getResizeValue: getBorderBoxInlineSize,
910
+ targetMutation: {
911
+ options: {
912
+ attributes: true,
913
+ attributeFilter: ['style', 'data-unitone-layout', 'class'],
914
+ characterData: true,
915
+ },
916
+ shouldApply: () => true,
917
+ },
447
918
  });
448
-
449
- observer.observe(target);
450
- mObserver.observe(target, mObserverArgs);
451
-
452
- return {
453
- resizeObserver: observer,
454
- mutationObserver: mObserver,
455
- };
456
919
  };
457
920
 
921
+ /**
922
+ * Creates duplicated marquee content and refreshes initialization markers.
923
+ *
924
+ * @param {Element} target Target element.
925
+ * @returns {Element | undefined} The duplicated marquee element, if created.
926
+ */
458
927
  export const setMarquee = (target) => {
928
+ let clonedMarquee;
929
+
459
930
  const addInitializedToken = (element) => {
460
931
  const layout = element.getAttribute('data-unitone-layout') ?? '';
461
932
  if (layout.split(/\s+/).includes('marquee:initialized')) {
@@ -479,14 +950,26 @@ export const setMarquee = (target) => {
479
950
  return;
480
951
  }
481
952
 
953
+ const shouldRestartAnimation = Array.from(marquees).some((marquee) =>
954
+ (marquee.getAttribute(layoutAttributeName) ?? '').split(/\s+/).includes('marquee:initialized'),
955
+ );
956
+
482
957
  if (1 === target.childElementCount && 1 === marquees.length) {
483
958
  const marquee = marquees[0];
484
- const clonedMarquee = marquee.cloneNode(true);
959
+ clonedMarquee = marquee.cloneNode(true);
485
960
  clonedMarquee.setAttribute('aria-hidden', 'true');
486
961
  marquee.after(clonedMarquee);
487
962
  }
488
963
 
489
964
  marquees = getMarquees();
965
+
966
+ if (!shouldRestartAnimation) {
967
+ marquees.forEach((marquee) => {
968
+ addInitializedToken(marquee);
969
+ });
970
+ return clonedMarquee;
971
+ }
972
+
490
973
  marquees.forEach((marquee) => {
491
974
  removeInitializedToken(marquee);
492
975
  });
@@ -499,44 +982,47 @@ export const setMarquee = (target) => {
499
982
  addInitializedToken(marquee);
500
983
  });
501
984
  });
985
+
986
+ return clonedMarquee;
502
987
  };
503
988
 
989
+ /**
990
+ * Creates the observer bundle for marquee layouts.
991
+ *
992
+ * @param {Element} target Target element.
993
+ * @returns {void}
994
+ */
504
995
  export const marqueeResizeObserver = (target) => {
505
- let prevWidth = 0;
506
-
507
- setMarquee(target);
508
-
509
- const observer = new ResizeObserver(
510
- debounce((entries) => {
511
- for (const entry of entries) {
512
- const width = entry.contentRect?.width;
513
- if (parseInt(width) !== parseInt(prevWidth)) {
514
- prevWidth = width;
515
- setMarquee(entry.target);
516
- }
517
- }
518
- }, 250),
519
- );
520
-
521
- observer.observe(target);
996
+ let clonedMarquee;
522
997
 
523
- const mObserverArgs = {
524
- childList: true,
998
+ const applyMarquee = (element) => {
999
+ clonedMarquee = setMarquee(element);
525
1000
  };
526
1001
 
527
- const mObserver = new MutationObserver(() => {
528
- requestAnimationFrame(() => {
529
- if (!target?.isConnected) {
530
- return;
531
- }
532
- setMarquee(target);
533
- });
534
- });
1002
+ createLayoutObserver(target, applyMarquee, {
1003
+ observeResize: false,
1004
+ observeIntersection: true,
1005
+ targetMutation: {
1006
+ options: {
1007
+ childList: true,
1008
+ },
1009
+ shouldApply: (entries) => {
1010
+ const addedNodes = entries.flatMap((entry) => Array.from(entry.addedNodes ?? []));
1011
+ const removedNodes = entries.flatMap((entry) => Array.from(entry.removedNodes ?? []));
535
1012
 
536
- mObserver.observe(target, mObserverArgs);
1013
+ if (
1014
+ clonedMarquee?.isConnected &&
1015
+ 1 === addedNodes.length &&
1016
+ 0 === removedNodes.length &&
1017
+ addedNodes[0] === clonedMarquee
1018
+ ) {
1019
+ clonedMarquee = null;
1020
+ return false;
1021
+ }
537
1022
 
538
- return {
539
- resizeObserver: observer,
540
- mutationObserver: mObserver,
541
- };
1023
+ clonedMarquee = null;
1024
+ return true;
1025
+ },
1026
+ },
1027
+ });
542
1028
  };