@inc2734/unitone-css 1.4.0 → 1.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/app.css +1 -1
- package/dist/app.js +1 -1
- package/dist/behaviors/dividers.js +1 -1
- package/dist/behaviors/index.js +1 -1
- package/dist/behaviors/stairs.js +1 -1
- package/dist/layout-primitives/cluster/react.js +1 -1
- package/dist/layout-primitives/index.js +1 -1
- package/dist/layout-primitives/marquee/behavior.js +1 -1
- package/dist/layout-primitives/marquee/react.js +1 -1
- package/dist/layout-primitives/responsive-grid/react.js +1 -1
- package/dist/layout-primitives/stack/react.js +1 -1
- package/dist/layout-primitives/switcher/react.js +1 -1
- package/dist/layout-primitives/vertical-writing/behavior.js +1 -1
- package/dist/layout-primitives/vertical-writing/react.js +1 -1
- package/dist/layout-primitives/with-sidebar/react.js +1 -1
- package/dist/library.js +1 -1
- package/package.json +2 -2
- package/src/layout-primitives/marquee/_index.scss +2 -3
- package/src/library.js +498 -425
- package/src/observer-scope.js +53 -0
- package/src/register-layout-initializer.js +35 -3
package/src/library.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { createObserverScope } from './observer-scope';
|
|
2
|
+
|
|
1
3
|
const layoutAttributeName = 'data-unitone-layout';
|
|
2
4
|
const layoutIntersectionMargin = 200;
|
|
3
5
|
const layoutIntersectionRootMargin = `${layoutIntersectionMargin}px 0px`;
|
|
@@ -40,31 +42,38 @@ const setLayoutTokens = (element, tokens) => {
|
|
|
40
42
|
* Observes target resizes and invokes the callback when a relevant change is detected.
|
|
41
43
|
*
|
|
42
44
|
* @param {Element} target Target element.
|
|
43
|
-
* @param {(target: Element
|
|
44
|
-
* @param {
|
|
45
|
+
* @param {(target: Element) => void} callback Callback to run.
|
|
46
|
+
* @param {ReturnType<typeof createObserverScope>} scope Observer lifetime.
|
|
47
|
+
* @param {(entry: ResizeObserverEntry) => unknown} [getValue] Value to compare.
|
|
45
48
|
* @returns {ResizeObserver} ResizeObserver instance.
|
|
46
49
|
*/
|
|
47
|
-
const createResizeObserver = (target, callback,
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
const currentValue = getValue?.(entry);
|
|
55
|
-
if (isFirstEntry) {
|
|
56
|
-
prevValue = currentValue;
|
|
57
|
-
isFirstEntry = false;
|
|
58
|
-
continue;
|
|
59
|
-
}
|
|
50
|
+
const createResizeObserver = (target, callback, scope, getValue) => {
|
|
51
|
+
const prevValues = new WeakMap();
|
|
52
|
+
const onResize = debounce(() => callback(target), 250);
|
|
53
|
+
const observer = new ResizeObserver((entries) => {
|
|
54
|
+
if (scope.disposed) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
60
57
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
58
|
+
let changed = false;
|
|
59
|
+
for (const entry of entries) {
|
|
60
|
+
const currentValue = getValue?.(entry);
|
|
61
|
+
if (
|
|
62
|
+
!prevValues.has(entry.target) ||
|
|
63
|
+
undefined === currentValue ||
|
|
64
|
+
currentValue !== prevValues.get(entry.target)
|
|
65
|
+
) {
|
|
66
|
+
changed = true;
|
|
65
67
|
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
+
prevValues.set(entry.target, currentValue);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (changed) {
|
|
72
|
+
onResize();
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
scope.addCleanup(onResize.cancel);
|
|
76
|
+
scope.addCleanup(() => observer.disconnect());
|
|
68
77
|
|
|
69
78
|
observer.observe(target);
|
|
70
79
|
|
|
@@ -77,22 +86,18 @@ const createResizeObserver = (target, callback, { getValue, delay = 250 } = {})
|
|
|
77
86
|
* @param {Node} target Target node.
|
|
78
87
|
* @param {MutationObserverInit} options Observer options.
|
|
79
88
|
* @param {(entries: MutationRecord[]) => void} callback Callback to run.
|
|
80
|
-
* @
|
|
89
|
+
* @param {ReturnType<typeof createObserverScope>} scope Observer lifetime.
|
|
90
|
+
* @returns {void}
|
|
81
91
|
*/
|
|
82
|
-
const createMutationObserver = (target, options, callback) => {
|
|
92
|
+
const createMutationObserver = (target, options, callback, scope) => {
|
|
83
93
|
const observer = new MutationObserver((entries) => {
|
|
84
|
-
|
|
85
|
-
if (!target?.isConnected) {
|
|
86
|
-
return;
|
|
87
|
-
}
|
|
88
|
-
|
|
94
|
+
if (!scope.disposed && target.isConnected) {
|
|
89
95
|
callback(entries);
|
|
90
|
-
}
|
|
96
|
+
}
|
|
91
97
|
});
|
|
92
98
|
|
|
93
99
|
observer.observe(target, options);
|
|
94
|
-
|
|
95
|
-
return observer;
|
|
100
|
+
scope.addCleanup(() => observer.disconnect());
|
|
96
101
|
};
|
|
97
102
|
|
|
98
103
|
/**
|
|
@@ -100,12 +105,13 @@ const createMutationObserver = (target, options, callback) => {
|
|
|
100
105
|
*
|
|
101
106
|
* @param {Element} target Target element.
|
|
102
107
|
* @param {(entry: IntersectionObserverEntry) => void} callback Callback to run.
|
|
103
|
-
* @
|
|
108
|
+
* @param {ReturnType<typeof createObserverScope>} scope Observer lifetime.
|
|
109
|
+
* @returns {void}
|
|
104
110
|
*/
|
|
105
|
-
const createIntersectionObserver = (target, callback) => {
|
|
111
|
+
const createIntersectionObserver = (target, callback, scope) => {
|
|
106
112
|
const observer = new IntersectionObserver(
|
|
107
113
|
([entry]) => {
|
|
108
|
-
if (entry) {
|
|
114
|
+
if (entry && !scope.disposed) {
|
|
109
115
|
callback(entry);
|
|
110
116
|
}
|
|
111
117
|
},
|
|
@@ -113,8 +119,7 @@ const createIntersectionObserver = (target, callback) => {
|
|
|
113
119
|
);
|
|
114
120
|
|
|
115
121
|
observer.observe(target);
|
|
116
|
-
|
|
117
|
-
return observer;
|
|
122
|
+
scope.addCleanup(() => observer.disconnect());
|
|
118
123
|
};
|
|
119
124
|
|
|
120
125
|
/**
|
|
@@ -122,27 +127,19 @@ const createIntersectionObserver = (target, callback) => {
|
|
|
122
127
|
*
|
|
123
128
|
* @param {Element} target Target element.
|
|
124
129
|
* @param {(target: Element) => void} callback Callback to run.
|
|
130
|
+
* @param {ReturnType<typeof createObserverScope>} scope Observer lifetime.
|
|
125
131
|
* @returns {() => void} Schedule function.
|
|
126
132
|
*/
|
|
127
|
-
const createScheduledTargetCallback = (target, callback) => {
|
|
133
|
+
const createScheduledTargetCallback = (target, callback, scope) => {
|
|
128
134
|
let rafId = 0;
|
|
129
|
-
let defaultView;
|
|
130
135
|
|
|
131
136
|
return () => {
|
|
132
|
-
|
|
133
|
-
if (!defaultView?.requestAnimationFrame) {
|
|
134
|
-
callback(target);
|
|
137
|
+
if (scope.disposed || rafId) {
|
|
135
138
|
return;
|
|
136
139
|
}
|
|
137
140
|
|
|
138
|
-
|
|
139
|
-
return;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
rafId = defaultView.requestAnimationFrame(() => {
|
|
141
|
+
rafId = scope.requestAnimationFrame(() => {
|
|
143
142
|
rafId = 0;
|
|
144
|
-
defaultView = null;
|
|
145
|
-
|
|
146
143
|
if (target?.isConnected) {
|
|
147
144
|
callback(target);
|
|
148
145
|
}
|
|
@@ -151,269 +148,169 @@ const createScheduledTargetCallback = (target, callback) => {
|
|
|
151
148
|
};
|
|
152
149
|
|
|
153
150
|
/**
|
|
154
|
-
*
|
|
151
|
+
* Keeps resize and optional attribute observation in sync with direct children.
|
|
155
152
|
*
|
|
156
153
|
* @param {Element} target Target element.
|
|
154
|
+
* @param {ResizeObserver} resizeObserver Shared size observer.
|
|
157
155
|
* @param {(target: Element) => void} callback Callback to run.
|
|
158
|
-
* @param {
|
|
159
|
-
* @
|
|
156
|
+
* @param {ReturnType<typeof createObserverScope>} scope Observer lifetime.
|
|
157
|
+
* @param {{ attributeFilter: string[], shouldApply: (entry: MutationRecord) => boolean }} [attributes]
|
|
158
|
+
* @returns {void}
|
|
160
159
|
*/
|
|
161
|
-
const
|
|
162
|
-
target,
|
|
163
|
-
callback,
|
|
164
|
-
{ getValue, delay = 250, onChildList } = {},
|
|
165
|
-
) => {
|
|
166
|
-
const prevValues = new WeakMap();
|
|
160
|
+
const observeLayoutChildren = (target, resizeObserver, callback, scope, attributes) => {
|
|
167
161
|
const observedChildren = new Set();
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
if (undefined === currentValue || currentValue !== prevValues.get(entry.target)) {
|
|
181
|
-
shouldApply = true;
|
|
162
|
+
const attributeObserver = attributes
|
|
163
|
+
? new MutationObserver((entries) => {
|
|
164
|
+
if (
|
|
165
|
+
!scope.disposed &&
|
|
166
|
+
target.isConnected &&
|
|
167
|
+
hasAttributeMutation(
|
|
168
|
+
entries,
|
|
169
|
+
(entry) => entry.target.parentElement === target && attributes.shouldApply(entry),
|
|
170
|
+
)
|
|
171
|
+
) {
|
|
172
|
+
callback(target);
|
|
182
173
|
}
|
|
174
|
+
})
|
|
175
|
+
: null;
|
|
176
|
+
scope.addCleanup(() => {
|
|
177
|
+
observedChildren.clear();
|
|
178
|
+
attributeObserver?.disconnect();
|
|
179
|
+
});
|
|
183
180
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
if (shouldApply) {
|
|
188
|
-
callback(target);
|
|
189
|
-
}
|
|
190
|
-
}, delay),
|
|
191
|
-
);
|
|
192
|
-
|
|
193
|
-
const syncObservedChildren = () => {
|
|
194
|
-
Array.from(observedChildren).forEach((child) => {
|
|
181
|
+
const syncChildren = () => {
|
|
182
|
+
for (const child of observedChildren) {
|
|
195
183
|
if (child.parentElement !== target) {
|
|
196
|
-
|
|
184
|
+
resizeObserver.unobserve(child);
|
|
197
185
|
observedChildren.delete(child);
|
|
198
|
-
prevValues.delete(child);
|
|
199
|
-
}
|
|
200
|
-
});
|
|
201
|
-
|
|
202
|
-
Array.from(target?.children ?? []).forEach((child) => {
|
|
203
|
-
if (observedChildren.has(child)) {
|
|
204
|
-
return;
|
|
205
186
|
}
|
|
206
|
-
|
|
207
|
-
observer.observe(child);
|
|
208
|
-
observedChildren.add(child);
|
|
209
|
-
});
|
|
210
|
-
};
|
|
211
|
-
|
|
212
|
-
observer.observe(target);
|
|
213
|
-
syncObservedChildren();
|
|
214
|
-
|
|
215
|
-
const mutationObserver = createMutationObserver(target, { childList: true }, (entries) => {
|
|
216
|
-
if (!entries.some((entry) => 'childList' === entry.type)) {
|
|
217
|
-
return;
|
|
218
187
|
}
|
|
219
188
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
return {
|
|
226
|
-
resizeObserver: observer,
|
|
227
|
-
mutationObserver,
|
|
228
|
-
};
|
|
229
|
-
};
|
|
230
|
-
|
|
231
|
-
/**
|
|
232
|
-
* Observes attribute changes on direct children.
|
|
233
|
-
*
|
|
234
|
-
* @param {Element} target Target element.
|
|
235
|
-
* @param {(target: Element) => void} callback Callback to run.
|
|
236
|
-
* @param {{ attributeFilter: string[], shouldApply: (entry: MutationRecord) => boolean, attributeOldValue?: boolean }} options
|
|
237
|
-
* @returns {{ observer: MutationObserver, syncObservedChildren: () => void }}
|
|
238
|
-
*/
|
|
239
|
-
const createDirectChildrenAttributeObserver = (
|
|
240
|
-
target,
|
|
241
|
-
callback,
|
|
242
|
-
{ attributeFilter, shouldApply, attributeOldValue = true } = {},
|
|
243
|
-
) => {
|
|
244
|
-
const observer = new MutationObserver((entries) => {
|
|
245
|
-
requestAnimationFrame(() => {
|
|
246
|
-
if (!target?.isConnected) {
|
|
247
|
-
return;
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
if (
|
|
251
|
-
entries.some(
|
|
252
|
-
(entry) =>
|
|
253
|
-
'attributes' === entry.type &&
|
|
254
|
-
entry.target.parentElement === target &&
|
|
255
|
-
shouldApply(entry),
|
|
256
|
-
)
|
|
257
|
-
) {
|
|
258
|
-
callback(target);
|
|
189
|
+
attributeObserver?.disconnect();
|
|
190
|
+
for (const child of target.children) {
|
|
191
|
+
if (!observedChildren.has(child)) {
|
|
192
|
+
resizeObserver.observe(child);
|
|
193
|
+
observedChildren.add(child);
|
|
259
194
|
}
|
|
260
|
-
|
|
261
|
-
});
|
|
262
|
-
|
|
263
|
-
const syncObservedChildren = () => {
|
|
264
|
-
observer.disconnect();
|
|
265
|
-
Array.from(target?.children ?? []).forEach((child) => {
|
|
266
|
-
observer.observe(child, {
|
|
195
|
+
attributeObserver?.observe(child, {
|
|
267
196
|
attributes: true,
|
|
268
|
-
attributeFilter,
|
|
269
|
-
attributeOldValue,
|
|
197
|
+
attributeFilter: attributes.attributeFilter,
|
|
198
|
+
attributeOldValue: true,
|
|
270
199
|
});
|
|
271
|
-
}
|
|
200
|
+
}
|
|
272
201
|
};
|
|
273
202
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
203
|
+
syncChildren();
|
|
204
|
+
createMutationObserver(
|
|
205
|
+
target,
|
|
206
|
+
{ childList: true },
|
|
207
|
+
() => {
|
|
208
|
+
syncChildren();
|
|
209
|
+
callback(target);
|
|
210
|
+
},
|
|
211
|
+
scope,
|
|
212
|
+
);
|
|
280
213
|
};
|
|
281
214
|
|
|
282
215
|
/**
|
|
283
216
|
* Creates a bundled observer setup for layout re-application.
|
|
284
217
|
*
|
|
285
218
|
* @param {Element} target Target element.
|
|
286
|
-
* @param {(target: Element) => void} apply Apply function.
|
|
219
|
+
* @param {(target: Element, scope: ReturnType<typeof createObserverScope>) => void} apply Apply function.
|
|
287
220
|
* @param {{
|
|
288
221
|
* getResizeValue?: (entry: ResizeObserverEntry) => unknown,
|
|
289
|
-
* delay?: number,
|
|
290
|
-
* observeResize?: boolean,
|
|
291
|
-
* observeIntersection?: boolean,
|
|
292
222
|
* observeDirectChildrenResize?: boolean,
|
|
293
|
-
* targetMutation?: { options: MutationObserverInit, shouldApply
|
|
294
|
-
* directChildMutation?: { attributeFilter: string[], shouldApply: (entry: MutationRecord) => boolean
|
|
223
|
+
* targetMutation?: { options: MutationObserverInit, shouldApply: (entries: MutationRecord[]) => boolean },
|
|
224
|
+
* directChildMutation?: { attributeFilter: string[], shouldApply: (entry: MutationRecord) => boolean }
|
|
295
225
|
* }} [options]
|
|
296
|
-
* @returns {void}
|
|
226
|
+
* @returns {() => void} Stops observation and cancels queued work.
|
|
297
227
|
*/
|
|
298
228
|
const createLayoutObserver = (
|
|
299
229
|
target,
|
|
300
230
|
apply,
|
|
301
|
-
{
|
|
302
|
-
getResizeValue,
|
|
303
|
-
delay = 250,
|
|
304
|
-
observeResize = true,
|
|
305
|
-
observeIntersection = false,
|
|
306
|
-
observeDirectChildrenResize = false,
|
|
307
|
-
targetMutation,
|
|
308
|
-
directChildMutation,
|
|
309
|
-
} = {},
|
|
231
|
+
{ getResizeValue, observeDirectChildrenResize = false, targetMutation, directChildMutation } = {},
|
|
310
232
|
) => {
|
|
311
|
-
const
|
|
312
|
-
|
|
233
|
+
const scope = createObserverScope(target);
|
|
234
|
+
const shouldObserveIntersection = 'undefined' !== typeof IntersectionObserver;
|
|
313
235
|
let isIntersecting = !shouldObserveIntersection || isNearViewport(target);
|
|
314
|
-
let needsApply =
|
|
236
|
+
let needsApply = !isIntersecting;
|
|
315
237
|
|
|
316
|
-
const runApply = (
|
|
317
|
-
if (!
|
|
238
|
+
const runApply = () => {
|
|
239
|
+
if (scope.disposed || !target.isConnected) {
|
|
318
240
|
return;
|
|
319
241
|
}
|
|
320
242
|
|
|
321
|
-
if (
|
|
243
|
+
if (!isIntersecting) {
|
|
322
244
|
needsApply = true;
|
|
323
245
|
return;
|
|
324
246
|
}
|
|
325
247
|
|
|
326
248
|
needsApply = false;
|
|
327
|
-
apply(
|
|
249
|
+
apply(target, scope);
|
|
328
250
|
};
|
|
329
251
|
|
|
330
|
-
const schedule = createScheduledTargetCallback(target, runApply);
|
|
252
|
+
const schedule = createScheduledTargetCallback(target, runApply, scope);
|
|
331
253
|
const scheduleApply = () => {
|
|
332
|
-
if (!target?.isConnected) {
|
|
333
|
-
return;
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
if (shouldObserveIntersection && !isIntersecting) {
|
|
337
|
-
needsApply = true;
|
|
254
|
+
if (scope.disposed || !target?.isConnected) {
|
|
338
255
|
return;
|
|
339
256
|
}
|
|
340
257
|
|
|
341
258
|
needsApply = true;
|
|
342
|
-
|
|
259
|
+
if (isIntersecting) {
|
|
260
|
+
schedule();
|
|
261
|
+
}
|
|
343
262
|
};
|
|
344
263
|
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
? {
|
|
349
|
-
resizeObserver: null,
|
|
350
|
-
mutationObserver: null,
|
|
351
|
-
}
|
|
352
|
-
: observeDirectChildrenResize
|
|
353
|
-
? createDirectChildrenResizeObserver(target, scheduleApply, {
|
|
354
|
-
getValue: getResizeValue,
|
|
355
|
-
delay,
|
|
356
|
-
onChildList: () => {
|
|
357
|
-
syncDirectChildAttributes();
|
|
358
|
-
},
|
|
359
|
-
})
|
|
360
|
-
: {
|
|
361
|
-
resizeObserver: createResizeObserver(target, scheduleApply, {
|
|
362
|
-
getValue: getResizeValue,
|
|
363
|
-
delay,
|
|
364
|
-
}),
|
|
365
|
-
};
|
|
366
|
-
|
|
367
|
-
if (shouldObserveIntersection) {
|
|
368
|
-
createIntersectionObserver(target, (entry) => {
|
|
369
|
-
isIntersecting = entry.isIntersecting;
|
|
370
|
-
if (isIntersecting && needsApply) {
|
|
371
|
-
scheduleApply();
|
|
372
|
-
}
|
|
373
|
-
});
|
|
264
|
+
const resizeObserver = createResizeObserver(target, scheduleApply, scope, getResizeValue);
|
|
265
|
+
if (observeDirectChildrenResize) {
|
|
266
|
+
observeLayoutChildren(target, resizeObserver, scheduleApply, scope, directChildMutation);
|
|
374
267
|
}
|
|
375
268
|
|
|
376
|
-
if (
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
269
|
+
if (shouldObserveIntersection) {
|
|
270
|
+
createIntersectionObserver(
|
|
271
|
+
target,
|
|
272
|
+
(entry) => {
|
|
273
|
+
isIntersecting = entry.isIntersecting;
|
|
274
|
+
if (isIntersecting && needsApply) {
|
|
275
|
+
scheduleApply();
|
|
276
|
+
}
|
|
277
|
+
},
|
|
278
|
+
scope,
|
|
279
|
+
);
|
|
382
280
|
}
|
|
383
281
|
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
282
|
+
if (targetMutation) {
|
|
283
|
+
createMutationObserver(
|
|
284
|
+
target,
|
|
285
|
+
targetMutation.options,
|
|
286
|
+
(entries) => {
|
|
287
|
+
if (targetMutation.shouldApply(entries)) {
|
|
288
|
+
scheduleApply();
|
|
289
|
+
}
|
|
290
|
+
},
|
|
291
|
+
scope,
|
|
292
|
+
);
|
|
395
293
|
}
|
|
396
294
|
|
|
397
|
-
if (
|
|
398
|
-
|
|
399
|
-
if (!entries.some((entry) => 'childList' === entry.type)) {
|
|
400
|
-
return;
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
syncDirectChildAttributes();
|
|
404
|
-
scheduleApply();
|
|
405
|
-
});
|
|
295
|
+
if (isIntersecting) {
|
|
296
|
+
runApply();
|
|
406
297
|
}
|
|
407
298
|
|
|
408
|
-
|
|
409
|
-
runApply(target);
|
|
410
|
-
}
|
|
299
|
+
return scope.dispose;
|
|
411
300
|
};
|
|
412
301
|
|
|
413
302
|
const getBorderBoxInlineSize = (entry) => entry.borderBoxSize?.[0].inlineSize;
|
|
414
303
|
|
|
415
304
|
const getContentRectWidth = (entry) => parseInt(entry.contentRect?.width);
|
|
416
305
|
|
|
306
|
+
const getElementStyle = (element) =>
|
|
307
|
+
(element.ownerDocument.defaultView ?? window).getComputedStyle(element);
|
|
308
|
+
|
|
309
|
+
const isInFlow = (element) => {
|
|
310
|
+
const { position, display } = getElementStyle(element);
|
|
311
|
+
return !['absolute', 'fixed'].includes(position) && 'none' !== display;
|
|
312
|
+
};
|
|
313
|
+
|
|
417
314
|
const hasLayoutBox = (element) => !!element?.isConnected && 0 < element.getClientRects().length;
|
|
418
315
|
|
|
419
316
|
const isNearViewport = (element) => {
|
|
@@ -468,32 +365,51 @@ const getNormalizedInlineRect = (
|
|
|
468
365
|
};
|
|
469
366
|
|
|
470
367
|
/**
|
|
471
|
-
*
|
|
368
|
+
* Compares each attribute before the batch with its final value, ignoring intermediate writes.
|
|
472
369
|
*
|
|
473
370
|
* @param {MutationRecord[]} entries Mutation records.
|
|
474
371
|
* @param {(entry: MutationRecord) => boolean} predicate Match predicate.
|
|
475
372
|
* @returns {boolean} Whether a matching attributes mutation exists.
|
|
476
373
|
*/
|
|
477
|
-
const hasAttributeMutation = (entries, predicate) =>
|
|
478
|
-
|
|
374
|
+
const hasAttributeMutation = (entries, predicate) => {
|
|
375
|
+
const seen = new Map();
|
|
376
|
+
return entries.some((entry) => {
|
|
377
|
+
if ('attributes' !== entry.type) {
|
|
378
|
+
return false;
|
|
379
|
+
}
|
|
380
|
+
const attributes = seen.get(entry.target) ?? new Set();
|
|
381
|
+
if (attributes.has(entry.attributeName)) {
|
|
382
|
+
return false;
|
|
383
|
+
}
|
|
384
|
+
attributes.add(entry.attributeName);
|
|
385
|
+
seen.set(entry.target, attributes);
|
|
386
|
+
return predicate(entry);
|
|
387
|
+
});
|
|
388
|
+
};
|
|
479
389
|
|
|
480
390
|
/**
|
|
481
391
|
* Coalesces repeated calls into the final call within the delay window.
|
|
482
392
|
*
|
|
483
393
|
* @param {Function} fn Function to wrap.
|
|
484
394
|
* @param {number} delay Delay in milliseconds.
|
|
485
|
-
* @returns {Function} Debounced function.
|
|
395
|
+
* @returns {Function & { cancel: () => void }} Debounced function with cancellation.
|
|
486
396
|
*/
|
|
487
397
|
export function debounce(fn, delay) {
|
|
488
398
|
let timer;
|
|
489
399
|
|
|
490
|
-
|
|
400
|
+
const debounced = function (...args) {
|
|
491
401
|
const context = this;
|
|
492
402
|
clearTimeout(timer);
|
|
493
403
|
timer = setTimeout(() => {
|
|
404
|
+
timer = undefined;
|
|
494
405
|
fn.apply(context, args);
|
|
495
406
|
}, delay);
|
|
496
407
|
};
|
|
408
|
+
debounced.cancel = () => {
|
|
409
|
+
clearTimeout(timer);
|
|
410
|
+
timer = undefined;
|
|
411
|
+
};
|
|
412
|
+
return debounced;
|
|
497
413
|
}
|
|
498
414
|
|
|
499
415
|
/**
|
|
@@ -515,14 +431,14 @@ export const setDividerLinewrap = (target) => {
|
|
|
515
431
|
layoutTokens: withoutLayoutTokens(getLayoutTokens(child), ['-bol', '-linewrap']),
|
|
516
432
|
}));
|
|
517
433
|
|
|
518
|
-
const
|
|
434
|
+
const applyChildLayouts = () => {
|
|
519
435
|
childLayouts.forEach(({ child, layoutTokens }) => {
|
|
520
436
|
setLayoutTokens(child, layoutTokens);
|
|
521
437
|
});
|
|
522
438
|
};
|
|
523
439
|
|
|
524
440
|
if (!currentLayoutArray.some((value) => value.startsWith('-divider:'))) {
|
|
525
|
-
|
|
441
|
+
applyChildLayouts();
|
|
526
442
|
return;
|
|
527
443
|
}
|
|
528
444
|
|
|
@@ -532,41 +448,34 @@ export const setDividerLinewrap = (target) => {
|
|
|
532
448
|
}
|
|
533
449
|
|
|
534
450
|
if (!hasLayoutBox(target)) {
|
|
535
|
-
|
|
451
|
+
applyChildLayouts();
|
|
536
452
|
return;
|
|
537
453
|
}
|
|
538
454
|
|
|
539
455
|
const defaultView =
|
|
540
456
|
target?.ownerDocument?.defaultView ?? ('undefined' !== typeof window ? window : undefined);
|
|
541
457
|
if (!defaultView?.getComputedStyle) {
|
|
542
|
-
|
|
458
|
+
applyChildLayouts();
|
|
543
459
|
return;
|
|
544
460
|
}
|
|
545
461
|
|
|
546
|
-
const targetStyle =
|
|
462
|
+
const targetStyle = getElementStyle(target);
|
|
547
463
|
const flow = {
|
|
548
464
|
direction: targetStyle.getPropertyValue('direction'),
|
|
549
465
|
flexDirection: targetStyle.getPropertyValue('flex-direction'),
|
|
550
466
|
writingMode: targetStyle.getPropertyValue('writing-mode'),
|
|
551
467
|
};
|
|
552
468
|
|
|
553
|
-
const targetChildren =
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
const rect = child.getBoundingClientRect();
|
|
559
|
-
accumulator.push({
|
|
560
|
-
child,
|
|
561
|
-
layoutTokens,
|
|
562
|
-
inlineRect: getNormalizedInlineRect(rect, flow),
|
|
563
|
-
});
|
|
469
|
+
const targetChildren = [];
|
|
470
|
+
for (const entry of childLayouts) {
|
|
471
|
+
if (isInFlow(entry.child)) {
|
|
472
|
+
entry.inlineRect = getNormalizedInlineRect(entry.child.getBoundingClientRect(), flow);
|
|
473
|
+
targetChildren.push(entry);
|
|
564
474
|
}
|
|
565
|
-
|
|
566
|
-
}, []);
|
|
475
|
+
}
|
|
567
476
|
|
|
568
477
|
if (0 === targetChildren.length) {
|
|
569
|
-
|
|
478
|
+
applyChildLayouts();
|
|
570
479
|
setLayoutTokens(target, [...currentLayoutArray, 'divider:initialized']);
|
|
571
480
|
return;
|
|
572
481
|
}
|
|
@@ -574,15 +483,14 @@ export const setDividerLinewrap = (target) => {
|
|
|
574
483
|
let prevInlineRect;
|
|
575
484
|
let hasWrapped = false;
|
|
576
485
|
let isStack = true;
|
|
577
|
-
|
|
578
|
-
const nextLayoutTokens = [...layoutTokens];
|
|
486
|
+
targetChildren.forEach(({ layoutTokens, inlineRect }, index) => {
|
|
579
487
|
const isBeginningOfLine =
|
|
580
488
|
0 === index ||
|
|
581
489
|
inlineRect.start < prevInlineRect.end - layoutPositionTolerance ||
|
|
582
490
|
inlineRect.start <= prevInlineRect.start + layoutPositionTolerance;
|
|
583
491
|
|
|
584
492
|
if (isBeginningOfLine) {
|
|
585
|
-
|
|
493
|
+
layoutTokens.push('-bol');
|
|
586
494
|
if (0 < index) {
|
|
587
495
|
hasWrapped = true;
|
|
588
496
|
}
|
|
@@ -591,19 +499,12 @@ export const setDividerLinewrap = (target) => {
|
|
|
591
499
|
}
|
|
592
500
|
|
|
593
501
|
if (hasWrapped) {
|
|
594
|
-
|
|
502
|
+
layoutTokens.push('-linewrap');
|
|
595
503
|
}
|
|
596
504
|
|
|
597
505
|
prevInlineRect = inlineRect;
|
|
598
|
-
return { child, layoutTokens: nextLayoutTokens };
|
|
599
|
-
});
|
|
600
|
-
|
|
601
|
-
const nextChildLayoutMap = new Map(
|
|
602
|
-
nextChildLayouts.map(({ child, layoutTokens }) => [child, layoutTokens]),
|
|
603
|
-
);
|
|
604
|
-
childLayouts.forEach(({ child, layoutTokens }) => {
|
|
605
|
-
setLayoutTokens(child, nextChildLayoutMap.get(child) ?? layoutTokens);
|
|
606
506
|
});
|
|
507
|
+
applyChildLayouts();
|
|
607
508
|
|
|
608
509
|
const nextTargetLayout = [...currentLayoutArray];
|
|
609
510
|
if (isStack) {
|
|
@@ -614,56 +515,39 @@ export const setDividerLinewrap = (target) => {
|
|
|
614
515
|
setLayoutTokens(target, nextTargetLayout);
|
|
615
516
|
};
|
|
616
517
|
|
|
518
|
+
const withoutAttributeTokens = (value, ignoredTokens) =>
|
|
519
|
+
withoutLayoutTokens((value ?? '').split(' '), ignoredTokens).join(' ');
|
|
520
|
+
|
|
617
521
|
/**
|
|
618
522
|
* Creates the observer bundle for divider layouts.
|
|
619
523
|
*
|
|
620
524
|
* @param {Element} target Target element.
|
|
621
525
|
* @param {{ ignore?: { layout?: string[], className?: string[] } }} [args]
|
|
622
|
-
* @returns {void}
|
|
526
|
+
* @returns {() => void} Stops observation and cancels queued work.
|
|
623
527
|
*/
|
|
624
528
|
export const dividersResizeObserver = (target, args = {}) => {
|
|
625
529
|
const shouldRecalculateByAttributeMutation = (entry) => {
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
.join(' ');
|
|
641
|
-
|
|
642
|
-
return current !== old;
|
|
643
|
-
}
|
|
644
|
-
|
|
645
|
-
if ('class' === entry.attributeName) {
|
|
646
|
-
const ignoreClassNames = [...(args?.ignore?.className ?? [])];
|
|
647
|
-
|
|
648
|
-
const current = (entry.target.getAttribute(entry.attributeName) ?? '')
|
|
649
|
-
.split(' ')
|
|
650
|
-
.filter((v) => !ignoreClassNames.includes(v))
|
|
651
|
-
.join(' ');
|
|
652
|
-
|
|
653
|
-
const old = (entry.oldValue ?? '')
|
|
654
|
-
.split(' ')
|
|
655
|
-
.filter((v) => !ignoreClassNames.includes(v))
|
|
656
|
-
.join(' ');
|
|
657
|
-
|
|
658
|
-
return current !== old;
|
|
530
|
+
const { attributeName, oldValue } = entry;
|
|
531
|
+
const currentValue = entry.target.getAttribute(attributeName);
|
|
532
|
+
const ignoredTokens =
|
|
533
|
+
layoutAttributeName === attributeName
|
|
534
|
+
? [...(args?.ignore?.layout ?? []), 'divider:initialized', '-bol', '-linewrap', '-stack']
|
|
535
|
+
: 'class' === attributeName
|
|
536
|
+
? [...(args?.ignore?.className ?? [])]
|
|
537
|
+
: null;
|
|
538
|
+
|
|
539
|
+
if (ignoredTokens) {
|
|
540
|
+
return (
|
|
541
|
+
withoutAttributeTokens(currentValue, ignoredTokens) !==
|
|
542
|
+
withoutAttributeTokens(oldValue, ignoredTokens)
|
|
543
|
+
);
|
|
659
544
|
}
|
|
660
545
|
|
|
661
|
-
return ['style', 'dir'].includes(
|
|
546
|
+
return ['style', 'dir'].includes(attributeName) && (currentValue ?? '') !== (oldValue ?? '');
|
|
662
547
|
};
|
|
663
548
|
|
|
664
|
-
createLayoutObserver(target, setDividerLinewrap, {
|
|
549
|
+
return createLayoutObserver(target, setDividerLinewrap, {
|
|
665
550
|
getResizeValue: getBorderBoxInlineSize,
|
|
666
|
-
observeIntersection: true,
|
|
667
551
|
observeDirectChildrenResize: true,
|
|
668
552
|
targetMutation: {
|
|
669
553
|
options: {
|
|
@@ -679,7 +563,6 @@ export const dividersResizeObserver = (target, args = {}) => {
|
|
|
679
563
|
},
|
|
680
564
|
directChildMutation: {
|
|
681
565
|
attributeFilter: ['style', 'data-unitone-layout', 'class'],
|
|
682
|
-
attributeOldValue: true,
|
|
683
566
|
shouldApply: shouldRecalculateByAttributeMutation,
|
|
684
567
|
},
|
|
685
568
|
});
|
|
@@ -696,13 +579,12 @@ export const setStairsStep = (target) => {
|
|
|
696
579
|
const currentLayoutArray = withoutLayoutTokens(getLayoutTokens(target), ['stairs:initialized']);
|
|
697
580
|
setLayoutTokens(target, currentLayoutArray);
|
|
698
581
|
|
|
699
|
-
|
|
700
|
-
if (!firstChild) {
|
|
582
|
+
if (0 === children.length) {
|
|
701
583
|
setLayoutTokens(target, [...currentLayoutArray, 'stairs:initialized']);
|
|
702
584
|
return;
|
|
703
585
|
}
|
|
704
586
|
|
|
705
|
-
//
|
|
587
|
+
// Measure the unshifted layout before applying steps and measuring their overflow.
|
|
706
588
|
target.style.removeProperty('--unitone--stairs-step-overflow-volume');
|
|
707
589
|
target.style.removeProperty('--unitone--max-stairs-step');
|
|
708
590
|
children.forEach((child) => {
|
|
@@ -720,12 +602,10 @@ export const setStairsStep = (target) => {
|
|
|
720
602
|
|
|
721
603
|
const isAlternatingStairs = ['up-down', 'down-up'].includes(stairsUp);
|
|
722
604
|
|
|
723
|
-
const direction =
|
|
605
|
+
const direction = getElementStyle(target).getPropertyValue('flex-direction');
|
|
724
606
|
const targetBottom = target.getBoundingClientRect().bottom;
|
|
725
607
|
const filteredChildren = children.reduce((accumulator, child) => {
|
|
726
|
-
|
|
727
|
-
const display = window.getComputedStyle(child).getPropertyValue('display');
|
|
728
|
-
if ('absolute' === position || 'fixed' === position || 'none' === display) {
|
|
608
|
+
if (!isInFlow(child)) {
|
|
729
609
|
return accumulator;
|
|
730
610
|
}
|
|
731
611
|
|
|
@@ -744,7 +624,7 @@ export const setStairsStep = (target) => {
|
|
|
744
624
|
const isBol =
|
|
745
625
|
'row-reverse' === direction ? prevRect?.left <= rect.left : prevRect?.left >= rect.left;
|
|
746
626
|
|
|
747
|
-
if (0 === index ||
|
|
627
|
+
if (0 === index || isBol) {
|
|
748
628
|
stairsStep = 0;
|
|
749
629
|
} else if (isAlternatingStairs) {
|
|
750
630
|
stairsStep = 0 === stairsStep ? 1 : 0;
|
|
@@ -783,11 +663,10 @@ export const setStairsStep = (target) => {
|
|
|
783
663
|
* Creates the observer bundle for stairs layouts.
|
|
784
664
|
*
|
|
785
665
|
* @param {Element} target Target element.
|
|
786
|
-
* @returns {void}
|
|
666
|
+
* @returns {() => void} Stops observation and cancels queued work.
|
|
787
667
|
*/
|
|
788
668
|
export const stairsResizeObserver = (target) => {
|
|
789
|
-
createLayoutObserver(target, setStairsStep, {
|
|
790
|
-
observeIntersection: true,
|
|
669
|
+
return createLayoutObserver(target, setStairsStep, {
|
|
791
670
|
observeDirectChildrenResize: true,
|
|
792
671
|
});
|
|
793
672
|
};
|
|
@@ -800,11 +679,7 @@ export const stairsResizeObserver = (target) => {
|
|
|
800
679
|
*/
|
|
801
680
|
const isIgnoredVerticalWritingMutationNode = (node) =>
|
|
802
681
|
node?.nodeType === Node.ELEMENT_NODE &&
|
|
803
|
-
|
|
804
|
-
'vertical-writing__thresholder',
|
|
805
|
-
'vertical-writing:initialized',
|
|
806
|
-
'vertical-writing:safari',
|
|
807
|
-
].includes(node.getAttribute('data-unitone-layout'));
|
|
682
|
+
'vertical-writing__thresholder' === node.getAttribute(layoutAttributeName);
|
|
808
683
|
|
|
809
684
|
/**
|
|
810
685
|
* Returns whether vertical-writing mutations require re-application.
|
|
@@ -831,9 +706,10 @@ const shouldApplyVerticalWritingMutation = (entries) =>
|
|
|
831
706
|
* Recalculates column count and height for vertical-writing layouts.
|
|
832
707
|
*
|
|
833
708
|
* @param {Element} target Target element.
|
|
709
|
+
* @param {ReturnType<typeof createObserverScope>} [scope] Observer lifetime.
|
|
834
710
|
* @returns {void}
|
|
835
711
|
*/
|
|
836
|
-
|
|
712
|
+
const updateColumnCountForVertical = (target, scope) => {
|
|
837
713
|
if (!target) {
|
|
838
714
|
return;
|
|
839
715
|
}
|
|
@@ -854,10 +730,7 @@ export const setColumnCountForVertical = (target) => {
|
|
|
854
730
|
Array.from(target.children)
|
|
855
731
|
.reverse()
|
|
856
732
|
.some((child) => {
|
|
857
|
-
if (
|
|
858
|
-
!['absolute', 'fixed'].includes(getComputedStyle(child).position) &&
|
|
859
|
-
'none' !== getComputedStyle(child).display
|
|
860
|
-
) {
|
|
733
|
+
if (isInFlow(child)) {
|
|
861
734
|
lastChild = child;
|
|
862
735
|
return true;
|
|
863
736
|
}
|
|
@@ -868,11 +741,11 @@ export const setColumnCountForVertical = (target) => {
|
|
|
868
741
|
return;
|
|
869
742
|
}
|
|
870
743
|
|
|
871
|
-
const computedStyle =
|
|
744
|
+
const computedStyle = getElementStyle(target);
|
|
872
745
|
const threshold = String(computedStyle.getPropertyValue('--unitone--threshold')).trim();
|
|
873
746
|
let forceSwitch = false;
|
|
874
747
|
|
|
875
|
-
if (threshold) {
|
|
748
|
+
if (threshold && !/^[+-]?(?:0+\.?0*|\.0+)(?:[a-z]+|%)?$/i.test(threshold)) {
|
|
876
749
|
const thresholder = target.ownerDocument.createElement('div');
|
|
877
750
|
thresholder.setAttribute(layoutAttributeName, 'vertical-writing__thresholder');
|
|
878
751
|
target.appendChild(thresholder);
|
|
@@ -892,7 +765,10 @@ export const setColumnCountForVertical = (target) => {
|
|
|
892
765
|
|
|
893
766
|
setLayoutTokens(target, [...nextLayoutTokens, 'vertical-writing:initialized']);
|
|
894
767
|
|
|
895
|
-
|
|
768
|
+
const schedule = scope
|
|
769
|
+
? (callback) => scope.requestAnimationFrame(callback)
|
|
770
|
+
: (callback) => target.ownerDocument.defaultView.requestAnimationFrame(callback);
|
|
771
|
+
schedule(() => {
|
|
896
772
|
if (!target?.isConnected) {
|
|
897
773
|
return;
|
|
898
774
|
}
|
|
@@ -915,23 +791,30 @@ export const setColumnCountForVertical = (target) => {
|
|
|
915
791
|
};
|
|
916
792
|
|
|
917
793
|
/**
|
|
918
|
-
*
|
|
794
|
+
* Recalculates vertical-writing columns without creating persistent observers.
|
|
919
795
|
*
|
|
920
796
|
* @param {Element} target Target element.
|
|
921
797
|
* @returns {void}
|
|
922
798
|
*/
|
|
799
|
+
export const setColumnCountForVertical = (target) => updateColumnCountForVertical(target);
|
|
800
|
+
|
|
801
|
+
/**
|
|
802
|
+
* Creates the observer bundle for vertical-writing layouts.
|
|
803
|
+
*
|
|
804
|
+
* @param {Element} target Target element.
|
|
805
|
+
* @returns {() => void} Stops observation and cancels queued work.
|
|
806
|
+
*/
|
|
923
807
|
export const verticalsResizeObserver = (target) => {
|
|
924
|
-
const applyVerticalColumns = (element) => {
|
|
808
|
+
const applyVerticalColumns = (element, scope) => {
|
|
925
809
|
if (element.parentNode?.style) {
|
|
926
810
|
element.parentNode.style.height = '';
|
|
927
811
|
}
|
|
928
812
|
|
|
929
|
-
|
|
813
|
+
updateColumnCountForVertical(element, scope);
|
|
930
814
|
};
|
|
931
815
|
|
|
932
|
-
createLayoutObserver(target, applyVerticalColumns, {
|
|
816
|
+
return createLayoutObserver(target, applyVerticalColumns, {
|
|
933
817
|
getResizeValue: getContentRectWidth,
|
|
934
|
-
observeIntersection: true,
|
|
935
818
|
targetMutation: {
|
|
936
819
|
options: {
|
|
937
820
|
attributes: true,
|
|
@@ -944,111 +827,301 @@ export const verticalsResizeObserver = (target) => {
|
|
|
944
827
|
});
|
|
945
828
|
};
|
|
946
829
|
|
|
830
|
+
const marqueeClones = new WeakSet();
|
|
831
|
+
const marqueeStates = new WeakMap();
|
|
832
|
+
|
|
833
|
+
const getMarquees = (target) =>
|
|
834
|
+
Array.from(target.querySelectorAll(':scope > [data-unitone-layout~="marquee"]'));
|
|
835
|
+
|
|
836
|
+
const getMarqueeAnimation = (element) =>
|
|
837
|
+
element
|
|
838
|
+
?.getAnimations()
|
|
839
|
+
.find(({ animationName }) => ['marquee', 'marquee-reverse'].includes(animationName));
|
|
840
|
+
|
|
947
841
|
/**
|
|
948
|
-
*
|
|
842
|
+
* Measures layout width without including transforms or rounding fractional pixels.
|
|
949
843
|
*
|
|
950
|
-
* @param {
|
|
951
|
-
* @
|
|
844
|
+
* @param {CSSStyleDeclaration} style Computed style.
|
|
845
|
+
* @param {boolean} borderBox Whether to include padding and borders.
|
|
846
|
+
* @returns {number} Width in CSS pixels.
|
|
952
847
|
*/
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
element.setAttribute('data-unitone-layout', `${layout} marquee:initialized`.trim());
|
|
962
|
-
};
|
|
848
|
+
const getMarqueeWidth = (style, borderBox) => {
|
|
849
|
+
const edges = ['paddingLeft', 'paddingRight', 'borderLeftWidth', 'borderRightWidth'].reduce(
|
|
850
|
+
(total, property) => total + (parseFloat(style[property]) || 0),
|
|
851
|
+
0,
|
|
852
|
+
);
|
|
853
|
+
const width = parseFloat(style.width) || 0;
|
|
854
|
+
return width + (borderBox ? edges : 0) - ('border-box' === style.boxSizing ? edges : 0);
|
|
855
|
+
};
|
|
963
856
|
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
857
|
+
/**
|
|
858
|
+
* Synchronizes existing animations without measuring or rebuilding copies.
|
|
859
|
+
*
|
|
860
|
+
* @param {Element[]} marquees Original and copied marquees.
|
|
861
|
+
* @param {ReturnType<typeof createObserverScope>} [scope] Observer lifetime.
|
|
862
|
+
* @returns {void}
|
|
863
|
+
*/
|
|
864
|
+
const syncMarqueeAnimations = ([original, ...copies], scope) => {
|
|
865
|
+
const animation = getMarqueeAnimation(original);
|
|
866
|
+
if (animation) {
|
|
867
|
+
const syncAnimations = () => {
|
|
868
|
+
if (scope?.disposed || !original.isConnected || getMarqueeAnimation(original) !== animation) {
|
|
869
|
+
return;
|
|
870
|
+
}
|
|
871
|
+
copies.forEach((element) => {
|
|
872
|
+
const copyAnimation = getMarqueeAnimation(element);
|
|
873
|
+
if (!element.isConnected || !copyAnimation) {
|
|
874
|
+
return;
|
|
875
|
+
}
|
|
972
876
|
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
877
|
+
const syncTime = () => {
|
|
878
|
+
if (
|
|
879
|
+
!scope?.disposed &&
|
|
880
|
+
element.isConnected &&
|
|
881
|
+
getMarqueeAnimation(original) === animation
|
|
882
|
+
) {
|
|
883
|
+
copyAnimation.currentTime = animation.currentTime;
|
|
884
|
+
}
|
|
885
|
+
};
|
|
886
|
+
syncTime();
|
|
887
|
+
// A pending play or pause task can otherwise apply an outdated hold time.
|
|
888
|
+
if (copyAnimation.pending) {
|
|
889
|
+
copyAnimation.ready.then(syncTime, () => {});
|
|
890
|
+
}
|
|
891
|
+
});
|
|
892
|
+
};
|
|
893
|
+
syncAnimations();
|
|
894
|
+
if (animation.pending) {
|
|
895
|
+
animation.ready.then(syncAnimations, () => {});
|
|
896
|
+
}
|
|
977
897
|
}
|
|
898
|
+
};
|
|
978
899
|
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
900
|
+
/**
|
|
901
|
+
* Updates generated copies and synchronizes them with the original animation.
|
|
902
|
+
*
|
|
903
|
+
* @param {Element} target Marquee wrapper.
|
|
904
|
+
* @param {boolean} [refreshClones] Whether source content has changed; omitted for manual detection.
|
|
905
|
+
* @param {ReturnType<typeof createObserverScope>} [scope] Observer lifetime.
|
|
906
|
+
* @returns {{ originals: Element[], firstClone?: Element }} Sources and the first new copy.
|
|
907
|
+
*/
|
|
908
|
+
const updateMarquee = (target, refreshClones, scope) => {
|
|
909
|
+
const originals = [];
|
|
910
|
+
const clones = [];
|
|
911
|
+
for (const child of target.children) {
|
|
912
|
+
if (marqueeClones.has(child)) {
|
|
913
|
+
clones.push(child);
|
|
914
|
+
} else if (child.matches('[data-unitone-layout~="marquee"]')) {
|
|
915
|
+
originals.push(child);
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
const original = originals[0];
|
|
919
|
+
const state = marqueeStates.get(target);
|
|
920
|
+
// Observer calls know whether content changed; manual calls compare the saved markup.
|
|
921
|
+
// Form control state can change without changing its HTML attributes.
|
|
922
|
+
refreshClones ??=
|
|
923
|
+
state?.source !== original?.outerHTML || !!original?.querySelector('input, textarea, select');
|
|
924
|
+
refreshClones ||= state?.original !== original;
|
|
925
|
+
|
|
926
|
+
if (refreshClones || 1 !== originals.length) {
|
|
927
|
+
clones.forEach((clone) => clone.remove());
|
|
928
|
+
clones.length = 0;
|
|
929
|
+
}
|
|
982
930
|
|
|
983
|
-
if (
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
clonedMarquee.setAttribute('aria-hidden', 'true');
|
|
987
|
-
marquee.after(clonedMarquee);
|
|
931
|
+
if (!original || !hasLayoutBox(target)) {
|
|
932
|
+
marqueeStates.delete(target);
|
|
933
|
+
return { originals };
|
|
988
934
|
}
|
|
989
935
|
|
|
990
|
-
|
|
936
|
+
const wrapperStyle = getElementStyle(target);
|
|
937
|
+
const originalStyle = getElementStyle(original);
|
|
938
|
+
const wrapperWidth = getMarqueeWidth(wrapperStyle, false);
|
|
939
|
+
const width = getMarqueeWidth(originalStyle, true);
|
|
940
|
+
const columnGap = wrapperStyle.columnGap;
|
|
941
|
+
const gap = (parseFloat(columnGap) || 0) * (columnGap.endsWith('%') ? wrapperWidth / 100 : 1);
|
|
942
|
+
const gapValue = `${gap}px`;
|
|
943
|
+
if (target.style.getPropertyValue('--unitone--marquee-gap') !== gapValue) {
|
|
944
|
+
target.style.setProperty('--unitone--marquee-gap', gapValue);
|
|
945
|
+
}
|
|
991
946
|
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
947
|
+
// Keep author-provided sibling content intact instead of treating it as a generated copy.
|
|
948
|
+
const canClone = 1 === originals.length && target.childElementCount - clones.length === 1;
|
|
949
|
+
const count =
|
|
950
|
+
canClone && 0 < wrapperWidth && 0 < width ? Math.ceil((wrapperWidth + gap) / (width + gap)) : 0;
|
|
951
|
+
|
|
952
|
+
clones.splice(count).forEach((clone) => clone.remove());
|
|
953
|
+
|
|
954
|
+
const lastMarquee = clones.at(-1) ?? original;
|
|
955
|
+
let firstClone;
|
|
956
|
+
const fragment = target.ownerDocument.createDocumentFragment();
|
|
957
|
+
while (clones.length < count) {
|
|
958
|
+
const clone = original.cloneNode(true);
|
|
959
|
+
clone.setAttribute('aria-hidden', 'true');
|
|
960
|
+
clone.setAttribute('inert', '');
|
|
961
|
+
marqueeClones.add(clone);
|
|
962
|
+
clones.push(clone);
|
|
963
|
+
fragment.append(clone);
|
|
964
|
+
firstClone ??= clone;
|
|
965
|
+
}
|
|
966
|
+
if (firstClone) {
|
|
967
|
+
lastMarquee.after(fragment);
|
|
997
968
|
}
|
|
998
969
|
|
|
999
|
-
|
|
1000
|
-
|
|
970
|
+
[...originals, ...clones].forEach((element) => {
|
|
971
|
+
const tokens = getLayoutTokens(element);
|
|
972
|
+
if (!tokens.includes('marquee:initialized')) {
|
|
973
|
+
setLayoutTokens(element, [...tokens, 'marquee:initialized']);
|
|
974
|
+
}
|
|
1001
975
|
});
|
|
1002
976
|
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
});
|
|
977
|
+
marqueeStates.set(target, {
|
|
978
|
+
original,
|
|
979
|
+
wrapperWidth,
|
|
980
|
+
width,
|
|
981
|
+
vertical: !originalStyle.writingMode.startsWith('horizontal'),
|
|
982
|
+
source: refreshClones || !state ? original.outerHTML : state.source,
|
|
1010
983
|
});
|
|
984
|
+
syncMarqueeAnimations([...originals, ...clones], scope);
|
|
1011
985
|
|
|
1012
|
-
return
|
|
986
|
+
return { originals, firstClone };
|
|
1013
987
|
};
|
|
1014
988
|
|
|
1015
989
|
/**
|
|
1016
|
-
*
|
|
990
|
+
* Refreshes marquee copies while retaining the original animation's progress.
|
|
1017
991
|
*
|
|
1018
992
|
* @param {Element} target Target element.
|
|
1019
|
-
* @returns {
|
|
993
|
+
* @returns {Element | undefined} The first newly created copy, if any.
|
|
994
|
+
*/
|
|
995
|
+
export const setMarquee = (target) => updateMarquee(target).firstClone;
|
|
996
|
+
|
|
997
|
+
/**
|
|
998
|
+
* Observes marquee size and content changes without delaying resize updates.
|
|
999
|
+
*
|
|
1000
|
+
* @param {Element} target Target element.
|
|
1001
|
+
* @returns {() => void} Stops observation and cancels queued work.
|
|
1020
1002
|
*/
|
|
1021
1003
|
export const marqueeResizeObserver = (target) => {
|
|
1022
|
-
|
|
1004
|
+
const scope = createObserverScope(target);
|
|
1005
|
+
let isIntersecting = 'undefined' === typeof IntersectionObserver || isNearViewport(target);
|
|
1006
|
+
let refreshClones = true;
|
|
1007
|
+
const observedOriginals = new Set();
|
|
1008
|
+
scope.addCleanup(() => {
|
|
1009
|
+
observedOriginals.clear();
|
|
1010
|
+
marqueeStates.delete(target);
|
|
1011
|
+
});
|
|
1023
1012
|
|
|
1024
|
-
const
|
|
1025
|
-
|
|
1013
|
+
const observeMutations = () => {
|
|
1014
|
+
mutationObserver.observe(target, { attributes: true, childList: true });
|
|
1015
|
+
observedOriginals.forEach((element) => {
|
|
1016
|
+
mutationObserver.observe(element, {
|
|
1017
|
+
attributes: true,
|
|
1018
|
+
childList: true,
|
|
1019
|
+
characterData: true,
|
|
1020
|
+
subtree: true,
|
|
1021
|
+
});
|
|
1022
|
+
});
|
|
1026
1023
|
};
|
|
1027
1024
|
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
options: {
|
|
1033
|
-
childList: true,
|
|
1034
|
-
},
|
|
1035
|
-
shouldApply: (entries) => {
|
|
1036
|
-
const addedNodes = entries.flatMap((entry) => Array.from(entry.addedNodes ?? []));
|
|
1037
|
-
const removedNodes = entries.flatMap((entry) => Array.from(entry.removedNodes ?? []));
|
|
1025
|
+
const changesSource = (entry) =>
|
|
1026
|
+
entry.target !== target ||
|
|
1027
|
+
('childList' === entry.type &&
|
|
1028
|
+
[...entry.addedNodes, ...entry.removedNodes].some((node) => !marqueeClones.has(node)));
|
|
1038
1029
|
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
addedNodes[0] === clonedMarquee
|
|
1044
|
-
) {
|
|
1045
|
-
clonedMarquee = null;
|
|
1046
|
-
return false;
|
|
1047
|
-
}
|
|
1030
|
+
const apply = () => {
|
|
1031
|
+
if (scope.disposed || !target.isConnected || !isIntersecting) {
|
|
1032
|
+
return;
|
|
1033
|
+
}
|
|
1048
1034
|
|
|
1049
|
-
|
|
1050
|
-
|
|
1035
|
+
// Preserve pending author edits, then exclude our own DOM writes from observation.
|
|
1036
|
+
refreshClones ||= mutationObserver.takeRecords().some(changesSource);
|
|
1037
|
+
mutationObserver.disconnect();
|
|
1038
|
+
const { originals } = updateMarquee(target, refreshClones, scope);
|
|
1039
|
+
refreshClones = false;
|
|
1040
|
+
|
|
1041
|
+
observedOriginals.forEach((element) => {
|
|
1042
|
+
if (!originals.includes(element)) {
|
|
1043
|
+
resizeObserver.unobserve(element);
|
|
1044
|
+
observedOriginals.delete(element);
|
|
1045
|
+
}
|
|
1046
|
+
});
|
|
1047
|
+
originals.forEach((element) => {
|
|
1048
|
+
if (!observedOriginals.has(element)) {
|
|
1049
|
+
resizeObserver.observe(element, { box: 'border-box' });
|
|
1050
|
+
observedOriginals.add(element);
|
|
1051
|
+
}
|
|
1052
|
+
});
|
|
1053
|
+
observeMutations();
|
|
1054
|
+
};
|
|
1055
|
+
|
|
1056
|
+
const scheduleApply = createScheduledTargetCallback(target, apply, scope);
|
|
1057
|
+
const mutationObserver = new MutationObserver((entries) => {
|
|
1058
|
+
if (!entries.some((entry) => 'childList' !== entry.type || changesSource(entry))) {
|
|
1059
|
+
return;
|
|
1060
|
+
}
|
|
1061
|
+
refreshClones ||= entries.some(changesSource);
|
|
1062
|
+
scheduleApply();
|
|
1063
|
+
});
|
|
1064
|
+
const resizeObserver = new ResizeObserver((entries) => {
|
|
1065
|
+
// Measurements already applied by a mutation callback need no second update.
|
|
1066
|
+
const state = marqueeStates.get(target);
|
|
1067
|
+
if (
|
|
1068
|
+
!state ||
|
|
1069
|
+
entries.some((entry) => {
|
|
1070
|
+
if (entry.target === target) {
|
|
1071
|
+
return Math.abs(entry.contentRect.width - state.wrapperWidth) > 0.01;
|
|
1072
|
+
}
|
|
1073
|
+
const box = entry.borderBoxSize?.[0];
|
|
1074
|
+
const width = state.vertical ? box?.blockSize : box?.inlineSize;
|
|
1075
|
+
return (
|
|
1076
|
+
entry.target !== state.original ||
|
|
1077
|
+
undefined === width ||
|
|
1078
|
+
Math.abs(width - state.width) > 0.01
|
|
1079
|
+
);
|
|
1080
|
+
})
|
|
1081
|
+
) {
|
|
1082
|
+
apply();
|
|
1083
|
+
}
|
|
1084
|
+
});
|
|
1085
|
+
scope.addCleanup(() => mutationObserver.disconnect());
|
|
1086
|
+
scope.addCleanup(() => resizeObserver.disconnect());
|
|
1087
|
+
resizeObserver.observe(target);
|
|
1088
|
+
observeMutations();
|
|
1089
|
+
|
|
1090
|
+
if ('undefined' !== typeof IntersectionObserver) {
|
|
1091
|
+
createIntersectionObserver(
|
|
1092
|
+
target,
|
|
1093
|
+
(entry) => {
|
|
1094
|
+
const wasIntersecting = isIntersecting;
|
|
1095
|
+
isIntersecting = entry.isIntersecting;
|
|
1096
|
+
if (isIntersecting && !wasIntersecting) {
|
|
1097
|
+
apply();
|
|
1098
|
+
}
|
|
1051
1099
|
},
|
|
1052
|
-
|
|
1100
|
+
scope,
|
|
1101
|
+
);
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
// Re-sync copies created while CSS has paused the original animation.
|
|
1105
|
+
const scheduleSync = createScheduledTargetCallback(
|
|
1106
|
+
target,
|
|
1107
|
+
() => syncMarqueeAnimations(getMarquees(target), scope),
|
|
1108
|
+
scope,
|
|
1109
|
+
);
|
|
1110
|
+
const onPauseChange = () => {
|
|
1111
|
+
if (getLayoutTokens(target).includes('-pause-on-hover')) {
|
|
1112
|
+
scheduleSync();
|
|
1113
|
+
}
|
|
1114
|
+
};
|
|
1115
|
+
['pointerenter', 'pointerleave', 'focusin', 'focusout'].forEach((eventName) => {
|
|
1116
|
+
target.addEventListener(eventName, onPauseChange);
|
|
1117
|
+
scope.addCleanup(() => target.removeEventListener(eventName, onPauseChange));
|
|
1053
1118
|
});
|
|
1119
|
+
|
|
1120
|
+
// Viewport media queries can change only the gap, without resizing either marquee box.
|
|
1121
|
+
const defaultView = target.ownerDocument.defaultView;
|
|
1122
|
+
defaultView.addEventListener('resize', scheduleApply);
|
|
1123
|
+
scope.addCleanup(() => defaultView.removeEventListener('resize', scheduleApply));
|
|
1124
|
+
apply();
|
|
1125
|
+
|
|
1126
|
+
return scope.dispose;
|
|
1054
1127
|
};
|