@inc2734/unitone-css 1.4.1 → 1.4.2
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 +1 -1
- package/src/behaviors/dividers.js +3 -1
- package/src/behaviors/stairs.js +3 -1
- package/src/layout-behavior-state.js +41 -0
- package/src/layout-primitives/cluster/index.jsx +25 -16
- package/src/layout-primitives/marquee/_index.scss +15 -7
- package/src/layout-primitives/marquee/behavior.js +1 -1
- package/src/layout-primitives/marquee/index.jsx +69 -7
- package/src/layout-primitives/marquee/layout.js +163 -0
- package/src/layout-primitives/responsive-grid/index.jsx +29 -17
- package/src/layout-primitives/stack/index.jsx +25 -16
- package/src/layout-primitives/switcher/index.jsx +26 -17
- package/src/layout-primitives/vertical-writing/behavior.js +3 -1
- package/src/layout-primitives/vertical-writing/index.jsx +11 -11
- package/src/layout-primitives/with-sidebar/index.jsx +27 -18
- package/src/library.js +162 -298
- package/src/register-layout-initializer.js +34 -24
- package/src/use-layout-behaviors.js +60 -0
package/src/library.js
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
|
+
import { requestReactLayoutRefresh } from './layout-behavior-state';
|
|
1
2
|
import { createObserverScope } from './observer-scope';
|
|
2
3
|
|
|
4
|
+
import {
|
|
5
|
+
getMarqueeParts,
|
|
6
|
+
isReactMarquee,
|
|
7
|
+
markMarqueeCopy,
|
|
8
|
+
measureMarquee,
|
|
9
|
+
observeMarquee,
|
|
10
|
+
requestMarqueeRefresh,
|
|
11
|
+
} from './layout-primitives/marquee/layout';
|
|
12
|
+
|
|
3
13
|
const layoutAttributeName = 'data-unitone-layout';
|
|
4
14
|
const layoutIntersectionMargin = 200;
|
|
5
15
|
const layoutIntersectionRootMargin = `${layoutIntersectionMargin}px 0px`;
|
|
@@ -155,9 +165,17 @@ const createScheduledTargetCallback = (target, callback, scope) => {
|
|
|
155
165
|
* @param {(target: Element) => void} callback Callback to run.
|
|
156
166
|
* @param {ReturnType<typeof createObserverScope>} scope Observer lifetime.
|
|
157
167
|
* @param {{ attributeFilter: string[], shouldApply: (entry: MutationRecord) => boolean }} [attributes]
|
|
168
|
+
* @param {(entries: MutationRecord[]) => boolean} [shouldApplyChildList] Filters temporary children.
|
|
158
169
|
* @returns {void}
|
|
159
170
|
*/
|
|
160
|
-
const observeLayoutChildren = (
|
|
171
|
+
const observeLayoutChildren = (
|
|
172
|
+
target,
|
|
173
|
+
resizeObserver,
|
|
174
|
+
callback,
|
|
175
|
+
scope,
|
|
176
|
+
attributes,
|
|
177
|
+
shouldApplyChildList,
|
|
178
|
+
) => {
|
|
161
179
|
const observedChildren = new Set();
|
|
162
180
|
const attributeObserver = attributes
|
|
163
181
|
? new MutationObserver((entries) => {
|
|
@@ -204,9 +222,9 @@ const observeLayoutChildren = (target, resizeObserver, callback, scope, attribut
|
|
|
204
222
|
createMutationObserver(
|
|
205
223
|
target,
|
|
206
224
|
{ childList: true },
|
|
207
|
-
() => {
|
|
225
|
+
(entries) => {
|
|
208
226
|
syncChildren();
|
|
209
|
-
callback(target);
|
|
227
|
+
if (!shouldApplyChildList || shouldApplyChildList(entries)) callback(target);
|
|
210
228
|
},
|
|
211
229
|
scope,
|
|
212
230
|
);
|
|
@@ -221,14 +239,21 @@ const observeLayoutChildren = (target, resizeObserver, callback, scope, attribut
|
|
|
221
239
|
* getResizeValue?: (entry: ResizeObserverEntry) => unknown,
|
|
222
240
|
* observeDirectChildrenResize?: boolean,
|
|
223
241
|
* targetMutation?: { options: MutationObserverInit, shouldApply: (entries: MutationRecord[]) => boolean },
|
|
224
|
-
* directChildMutation?: { attributeFilter: string[], shouldApply: (entry: MutationRecord) => boolean }
|
|
242
|
+
* directChildMutation?: { attributeFilter: string[], shouldApply: (entry: MutationRecord) => boolean },
|
|
243
|
+
* shouldApplyChildList?: (entries: MutationRecord[]) => boolean
|
|
225
244
|
* }} [options]
|
|
226
245
|
* @returns {() => void} Stops observation and cancels queued work.
|
|
227
246
|
*/
|
|
228
247
|
const createLayoutObserver = (
|
|
229
248
|
target,
|
|
230
249
|
apply,
|
|
231
|
-
{
|
|
250
|
+
{
|
|
251
|
+
getResizeValue,
|
|
252
|
+
observeDirectChildrenResize = false,
|
|
253
|
+
targetMutation,
|
|
254
|
+
directChildMutation,
|
|
255
|
+
shouldApplyChildList,
|
|
256
|
+
} = {},
|
|
232
257
|
) => {
|
|
233
258
|
const scope = createObserverScope(target);
|
|
234
259
|
const shouldObserveIntersection = 'undefined' !== typeof IntersectionObserver;
|
|
@@ -263,7 +288,14 @@ const createLayoutObserver = (
|
|
|
263
288
|
|
|
264
289
|
const resizeObserver = createResizeObserver(target, scheduleApply, scope, getResizeValue);
|
|
265
290
|
if (observeDirectChildrenResize) {
|
|
266
|
-
observeLayoutChildren(
|
|
291
|
+
observeLayoutChildren(
|
|
292
|
+
target,
|
|
293
|
+
resizeObserver,
|
|
294
|
+
scheduleApply,
|
|
295
|
+
scope,
|
|
296
|
+
directChildMutation,
|
|
297
|
+
shouldApplyChildList,
|
|
298
|
+
);
|
|
267
299
|
}
|
|
268
300
|
|
|
269
301
|
if (shouldObserveIntersection) {
|
|
@@ -296,13 +328,12 @@ const createLayoutObserver = (
|
|
|
296
328
|
runApply();
|
|
297
329
|
}
|
|
298
330
|
|
|
299
|
-
|
|
331
|
+
// Keep the public callable disposer; React also uses its internal refresh operation.
|
|
332
|
+
return Object.assign(scope.dispose, { refresh: runApply });
|
|
300
333
|
};
|
|
301
334
|
|
|
302
335
|
const getBorderBoxInlineSize = (entry) => entry.borderBoxSize?.[0].inlineSize;
|
|
303
336
|
|
|
304
|
-
const getContentRectWidth = (entry) => parseInt(entry.contentRect?.width);
|
|
305
|
-
|
|
306
337
|
const getElementStyle = (element) =>
|
|
307
338
|
(element.ownerDocument.defaultView ?? window).getComputedStyle(element);
|
|
308
339
|
|
|
@@ -418,7 +449,7 @@ export function debounce(fn, delay) {
|
|
|
418
449
|
* @param {Element} target Target element.
|
|
419
450
|
* @returns {void}
|
|
420
451
|
*/
|
|
421
|
-
|
|
452
|
+
const updateDividerLinewrap = (target) => {
|
|
422
453
|
const children = Array.from(target?.children ?? []);
|
|
423
454
|
const currentLayoutArray = withoutLayoutTokens(getLayoutTokens(target), [
|
|
424
455
|
'divider:initialized',
|
|
@@ -516,7 +547,23 @@ export const setDividerLinewrap = (target) => {
|
|
|
516
547
|
};
|
|
517
548
|
|
|
518
549
|
const withoutAttributeTokens = (value, ignoredTokens) =>
|
|
519
|
-
withoutLayoutTokens((value ?? '').split(
|
|
550
|
+
withoutLayoutTokens((value ?? '').split(/\s+/).filter(Boolean), ignoredTokens)
|
|
551
|
+
.sort()
|
|
552
|
+
.join(' ');
|
|
553
|
+
|
|
554
|
+
const hasChangedAttribute = (entry) => {
|
|
555
|
+
const current = entry.target.getAttribute(entry.attributeName);
|
|
556
|
+
// Behaviors may append their tokens in different orders on the same element.
|
|
557
|
+
if (['data-unitone-layout', 'class'].includes(entry.attributeName)) {
|
|
558
|
+
return withoutAttributeTokens(current, []) !== withoutAttributeTokens(entry.oldValue, []);
|
|
559
|
+
}
|
|
560
|
+
return (current ?? '') !== (entry.oldValue ?? '');
|
|
561
|
+
};
|
|
562
|
+
|
|
563
|
+
/** Refreshes the layout, deferring React-managed targets to their component. */
|
|
564
|
+
export const setDividerLinewrap = (target) => {
|
|
565
|
+
if (!requestReactLayoutRefresh(target)) updateDividerLinewrap(target);
|
|
566
|
+
};
|
|
520
567
|
|
|
521
568
|
/**
|
|
522
569
|
* Creates the observer bundle for divider layouts.
|
|
@@ -526,12 +573,13 @@ const withoutAttributeTokens = (value, ignoredTokens) =>
|
|
|
526
573
|
* @returns {() => void} Stops observation and cancels queued work.
|
|
527
574
|
*/
|
|
528
575
|
export const dividersResizeObserver = (target, args = {}) => {
|
|
576
|
+
if (!args.reactOwned && requestReactLayoutRefresh(target)) return () => {};
|
|
529
577
|
const shouldRecalculateByAttributeMutation = (entry) => {
|
|
530
578
|
const { attributeName, oldValue } = entry;
|
|
531
579
|
const currentValue = entry.target.getAttribute(attributeName);
|
|
532
580
|
const ignoredTokens =
|
|
533
581
|
layoutAttributeName === attributeName
|
|
534
|
-
? [...(args?.ignore?.layout ?? [])
|
|
582
|
+
? [...(args?.ignore?.layout ?? [])]
|
|
535
583
|
: 'class' === attributeName
|
|
536
584
|
? [...(args?.ignore?.className ?? [])]
|
|
537
585
|
: null;
|
|
@@ -546,7 +594,7 @@ export const dividersResizeObserver = (target, args = {}) => {
|
|
|
546
594
|
return ['style', 'dir'].includes(attributeName) && (currentValue ?? '') !== (oldValue ?? '');
|
|
547
595
|
};
|
|
548
596
|
|
|
549
|
-
return createLayoutObserver(target,
|
|
597
|
+
return createLayoutObserver(target, updateDividerLinewrap, {
|
|
550
598
|
getResizeValue: getBorderBoxInlineSize,
|
|
551
599
|
observeDirectChildrenResize: true,
|
|
552
600
|
targetMutation: {
|
|
@@ -574,7 +622,7 @@ export const dividersResizeObserver = (target, args = {}) => {
|
|
|
574
622
|
* @param {Element} target Target element.
|
|
575
623
|
* @returns {void}
|
|
576
624
|
*/
|
|
577
|
-
|
|
625
|
+
const updateStairsStep = (target) => {
|
|
578
626
|
const children = Array.from(target.children);
|
|
579
627
|
const currentLayoutArray = withoutLayoutTokens(getLayoutTokens(target), ['stairs:initialized']);
|
|
580
628
|
setLayoutTokens(target, currentLayoutArray);
|
|
@@ -659,15 +707,33 @@ export const setStairsStep = (target) => {
|
|
|
659
707
|
setLayoutTokens(target, [...currentLayoutArray, 'stairs:initialized']);
|
|
660
708
|
};
|
|
661
709
|
|
|
710
|
+
/** Refreshes the layout, deferring React-managed targets to their component. */
|
|
711
|
+
export const setStairsStep = (target) => {
|
|
712
|
+
if (!requestReactLayoutRefresh(target)) updateStairsStep(target);
|
|
713
|
+
};
|
|
714
|
+
|
|
662
715
|
/**
|
|
663
716
|
* Creates the observer bundle for stairs layouts.
|
|
664
717
|
*
|
|
665
718
|
* @param {Element} target Target element.
|
|
666
719
|
* @returns {() => void} Stops observation and cancels queued work.
|
|
667
720
|
*/
|
|
668
|
-
export const stairsResizeObserver = (target) => {
|
|
669
|
-
|
|
721
|
+
export const stairsResizeObserver = (target, args = {}) => {
|
|
722
|
+
if (!args.reactOwned && requestReactLayoutRefresh(target)) return () => {};
|
|
723
|
+
return createLayoutObserver(target, updateStairsStep, {
|
|
670
724
|
observeDirectChildrenResize: true,
|
|
725
|
+
targetMutation: {
|
|
726
|
+
options: {
|
|
727
|
+
attributes: true,
|
|
728
|
+
attributeFilter: ['style', 'class', 'dir', 'data-unitone-layout'],
|
|
729
|
+
attributeOldValue: true,
|
|
730
|
+
},
|
|
731
|
+
shouldApply: (entries) => hasAttributeMutation(entries, hasChangedAttribute),
|
|
732
|
+
},
|
|
733
|
+
directChildMutation: {
|
|
734
|
+
attributeFilter: ['style', 'class', 'dir', 'data-unitone-layout'],
|
|
735
|
+
shouldApply: hasChangedAttribute,
|
|
736
|
+
},
|
|
671
737
|
});
|
|
672
738
|
};
|
|
673
739
|
|
|
@@ -688,10 +754,9 @@ const isIgnoredVerticalWritingMutationNode = (node) =>
|
|
|
688
754
|
* @returns {boolean} Whether re-application is required.
|
|
689
755
|
*/
|
|
690
756
|
const shouldApplyVerticalWritingMutation = (entries) =>
|
|
757
|
+
hasAttributeMutation(entries, hasChangedAttribute) ||
|
|
691
758
|
entries.some((entry) => {
|
|
692
|
-
if ('
|
|
693
|
-
return true;
|
|
694
|
-
}
|
|
759
|
+
if ('characterData' === entry.type) return true;
|
|
695
760
|
|
|
696
761
|
if ('childList' !== entry.type) {
|
|
697
762
|
return false;
|
|
@@ -796,7 +861,9 @@ const updateColumnCountForVertical = (target, scope) => {
|
|
|
796
861
|
* @param {Element} target Target element.
|
|
797
862
|
* @returns {void}
|
|
798
863
|
*/
|
|
799
|
-
export const setColumnCountForVertical = (target) =>
|
|
864
|
+
export const setColumnCountForVertical = (target) => {
|
|
865
|
+
if (!requestReactLayoutRefresh(target)) updateColumnCountForVertical(target);
|
|
866
|
+
};
|
|
800
867
|
|
|
801
868
|
/**
|
|
802
869
|
* Creates the observer bundle for vertical-writing layouts.
|
|
@@ -804,7 +871,8 @@ export const setColumnCountForVertical = (target) => updateColumnCountForVertica
|
|
|
804
871
|
* @param {Element} target Target element.
|
|
805
872
|
* @returns {() => void} Stops observation and cancels queued work.
|
|
806
873
|
*/
|
|
807
|
-
export const verticalsResizeObserver = (target) => {
|
|
874
|
+
export const verticalsResizeObserver = (target, args = {}) => {
|
|
875
|
+
if (!args.reactOwned && requestReactLayoutRefresh(target)) return () => {};
|
|
808
876
|
const applyVerticalColumns = (element, scope) => {
|
|
809
877
|
if (element.parentNode?.style) {
|
|
810
878
|
element.parentNode.style.height = '';
|
|
@@ -814,11 +882,14 @@ export const verticalsResizeObserver = (target) => {
|
|
|
814
882
|
};
|
|
815
883
|
|
|
816
884
|
return createLayoutObserver(target, applyVerticalColumns, {
|
|
817
|
-
|
|
885
|
+
observeDirectChildrenResize: true,
|
|
886
|
+
shouldApplyChildList: shouldApplyVerticalWritingMutation,
|
|
818
887
|
targetMutation: {
|
|
819
888
|
options: {
|
|
820
889
|
attributes: true,
|
|
821
|
-
attributeFilter: ['style'],
|
|
890
|
+
attributeFilter: ['style', 'class', 'dir', 'data-unitone-layout'],
|
|
891
|
+
attributeOldValue: true,
|
|
892
|
+
characterData: true,
|
|
822
893
|
childList: true,
|
|
823
894
|
subtree: true,
|
|
824
895
|
},
|
|
@@ -827,301 +898,94 @@ export const verticalsResizeObserver = (target) => {
|
|
|
827
898
|
});
|
|
828
899
|
};
|
|
829
900
|
|
|
830
|
-
const marqueeClones = new WeakSet();
|
|
831
901
|
const marqueeStates = new WeakMap();
|
|
832
902
|
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
const
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
);
|
|
853
|
-
const width = parseFloat(style.width) || 0;
|
|
854
|
-
return width + (borderBox ? edges : 0) - ('border-box' === style.boxSizing ? edges : 0);
|
|
855
|
-
};
|
|
903
|
+
/** Updates copied items while keeping the original nodes and animation intact. */
|
|
904
|
+
const updateMarquee = (target) => {
|
|
905
|
+
let parts = getMarqueeParts(target);
|
|
906
|
+
const { marquee, originals } = parts;
|
|
907
|
+
if (!marquee) return;
|
|
908
|
+
const state = marqueeStates.get(target);
|
|
909
|
+
const source = originals.map((item) => item.outerHTML).join('');
|
|
910
|
+
// Snapshots exclude generated nodes, so author additions after copies are detected too.
|
|
911
|
+
if (
|
|
912
|
+
state?.marquee !== marquee ||
|
|
913
|
+
state?.source !== source ||
|
|
914
|
+
originals.some(
|
|
915
|
+
(item) =>
|
|
916
|
+
item.matches('input, textarea, select') || item.querySelector('input, textarea, select'),
|
|
917
|
+
)
|
|
918
|
+
) {
|
|
919
|
+
parts.copies.forEach((copy) => copy.remove());
|
|
920
|
+
parts.copies = [];
|
|
921
|
+
}
|
|
856
922
|
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
*
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
copies.forEach((element) => {
|
|
872
|
-
const copyAnimation = getMarqueeAnimation(element);
|
|
873
|
-
if (!element.isConnected || !copyAnimation) {
|
|
874
|
-
return;
|
|
875
|
-
}
|
|
923
|
+
let firstClone;
|
|
924
|
+
const renderCopies = (groups) => {
|
|
925
|
+
const count = groups * originals.length;
|
|
926
|
+
parts.copies.splice(count).forEach((copy) => copy.remove());
|
|
927
|
+
const fragment = target.ownerDocument.createDocumentFragment();
|
|
928
|
+
while (parts.copies.length < count) {
|
|
929
|
+
const copy = originals[parts.copies.length % originals.length].cloneNode(true);
|
|
930
|
+
markMarqueeCopy(copy);
|
|
931
|
+
parts.copies.push(copy);
|
|
932
|
+
fragment.append(copy);
|
|
933
|
+
firstClone ??= copy;
|
|
934
|
+
}
|
|
935
|
+
if (fragment.childNodes.length) marquee.append(fragment);
|
|
936
|
+
};
|
|
876
937
|
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
};
|
|
893
|
-
syncAnimations();
|
|
894
|
-
if (animation.pending) {
|
|
895
|
-
animation.ready.then(syncAnimations, () => {});
|
|
938
|
+
let layout = measureMarquee(target, parts);
|
|
939
|
+
if (layout.groups && !parts.copies.length) {
|
|
940
|
+
renderCopies(1);
|
|
941
|
+
layout = measureMarquee(target, parts);
|
|
942
|
+
}
|
|
943
|
+
renderCopies(layout.groups);
|
|
944
|
+
if (marquee) {
|
|
945
|
+
const travel = `${layout.rtl ? layout.distance : -layout.distance}px`;
|
|
946
|
+
if (marquee.style.getPropertyValue('--unitone--marquee-travel') !== travel) {
|
|
947
|
+
marquee.style.setProperty('--unitone--marquee-travel', travel);
|
|
948
|
+
}
|
|
949
|
+
if (layout.distance > 0) {
|
|
950
|
+
const tokens = getLayoutTokens(marquee);
|
|
951
|
+
if (!tokens.includes('marquee:initialized'))
|
|
952
|
+
setLayoutTokens(marquee, [...tokens, 'marquee:initialized']);
|
|
896
953
|
}
|
|
897
954
|
}
|
|
955
|
+
marqueeStates.set(target, { marquee, source });
|
|
956
|
+
return firstClone?.isConnected ? firstClone : undefined;
|
|
898
957
|
};
|
|
899
958
|
|
|
900
959
|
/**
|
|
901
|
-
*
|
|
960
|
+
* Refreshes copied items without restarting the animation.
|
|
961
|
+
* React-owned marquees receive a refresh request and render copies through React.
|
|
902
962
|
*
|
|
903
963
|
* @param {Element} target Marquee wrapper.
|
|
904
|
-
* @
|
|
905
|
-
* @param {ReturnType<typeof createObserverScope>} [scope] Observer lifetime.
|
|
906
|
-
* @returns {{ originals: Element[], firstClone?: Element }} Sources and the first new copy.
|
|
964
|
+
* @returns {Element | undefined} The first newly created HTML item copy, if any.
|
|
907
965
|
*/
|
|
908
|
-
const
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
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
|
-
}
|
|
930
|
-
|
|
931
|
-
if (!original || !hasLayoutBox(target)) {
|
|
932
|
-
marqueeStates.delete(target);
|
|
933
|
-
return { originals };
|
|
934
|
-
}
|
|
935
|
-
|
|
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
|
-
}
|
|
946
|
-
|
|
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);
|
|
966
|
+
export const setMarquee = (target) => {
|
|
967
|
+
if (isReactMarquee(target)) {
|
|
968
|
+
requestMarqueeRefresh(target);
|
|
969
|
+
return;
|
|
968
970
|
}
|
|
969
|
-
|
|
970
|
-
[...originals, ...clones].forEach((element) => {
|
|
971
|
-
const tokens = getLayoutTokens(element);
|
|
972
|
-
if (!tokens.includes('marquee:initialized')) {
|
|
973
|
-
setLayoutTokens(element, [...tokens, 'marquee:initialized']);
|
|
974
|
-
}
|
|
975
|
-
});
|
|
976
|
-
|
|
977
|
-
marqueeStates.set(target, {
|
|
978
|
-
original,
|
|
979
|
-
wrapperWidth,
|
|
980
|
-
width,
|
|
981
|
-
vertical: !originalStyle.writingMode.startsWith('horizontal'),
|
|
982
|
-
source: refreshClones || !state ? original.outerHTML : state.source,
|
|
983
|
-
});
|
|
984
|
-
syncMarqueeAnimations([...originals, ...clones], scope);
|
|
985
|
-
|
|
986
|
-
return { originals, firstClone };
|
|
971
|
+
return updateMarquee(target);
|
|
987
972
|
};
|
|
988
973
|
|
|
989
974
|
/**
|
|
990
|
-
*
|
|
975
|
+
* Observes marquee and original item sizes and content changes.
|
|
976
|
+
* React components own their observer lifetime and generated children.
|
|
991
977
|
*
|
|
992
|
-
* @param {Element} target
|
|
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.
|
|
978
|
+
* @param {Element} target Marquee wrapper.
|
|
1001
979
|
* @returns {() => void} Stops observation and cancels queued work.
|
|
1002
980
|
*/
|
|
1003
981
|
export const marqueeResizeObserver = (target) => {
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
const observedOriginals = new Set();
|
|
1008
|
-
scope.addCleanup(() => {
|
|
1009
|
-
observedOriginals.clear();
|
|
1010
|
-
marqueeStates.delete(target);
|
|
1011
|
-
});
|
|
1012
|
-
|
|
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
|
-
});
|
|
1023
|
-
};
|
|
1024
|
-
|
|
1025
|
-
const changesSource = (entry) =>
|
|
1026
|
-
entry.target !== target ||
|
|
1027
|
-
('childList' === entry.type &&
|
|
1028
|
-
[...entry.addedNodes, ...entry.removedNodes].some((node) => !marqueeClones.has(node)));
|
|
1029
|
-
|
|
1030
|
-
const apply = () => {
|
|
1031
|
-
if (scope.disposed || !target.isConnected || !isIntersecting) {
|
|
1032
|
-
return;
|
|
1033
|
-
}
|
|
1034
|
-
|
|
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
|
-
}
|
|
1099
|
-
},
|
|
1100
|
-
scope,
|
|
1101
|
-
);
|
|
982
|
+
if (isReactMarquee(target)) {
|
|
983
|
+
return observeMarquee(target, () => requestMarqueeRefresh(target), { listenForRefresh: false })
|
|
984
|
+
.dispose;
|
|
1102
985
|
}
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
target
|
|
1107
|
-
() => syncMarqueeAnimations(getMarquees(target), scope),
|
|
1108
|
-
scope,
|
|
1109
|
-
);
|
|
1110
|
-
const onPauseChange = () => {
|
|
1111
|
-
if (getLayoutTokens(target).includes('-pause-on-hover')) {
|
|
1112
|
-
scheduleSync();
|
|
1113
|
-
}
|
|
986
|
+
const controller = observeMarquee(target, () => updateMarquee(target));
|
|
987
|
+
return () => {
|
|
988
|
+
controller.dispose();
|
|
989
|
+
marqueeStates.delete(target);
|
|
1114
990
|
};
|
|
1115
|
-
['pointerenter', 'pointerleave', 'focusin', 'focusout'].forEach((eventName) => {
|
|
1116
|
-
target.addEventListener(eventName, onPauseChange);
|
|
1117
|
-
scope.addCleanup(() => target.removeEventListener(eventName, onPauseChange));
|
|
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;
|
|
1127
991
|
};
|