@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.
Files changed (34) hide show
  1. package/dist/app.css +1 -1
  2. package/dist/app.js +1 -1
  3. package/dist/behaviors/dividers.js +1 -1
  4. package/dist/behaviors/index.js +1 -1
  5. package/dist/behaviors/stairs.js +1 -1
  6. package/dist/layout-primitives/cluster/react.js +1 -1
  7. package/dist/layout-primitives/index.js +1 -1
  8. package/dist/layout-primitives/marquee/behavior.js +1 -1
  9. package/dist/layout-primitives/marquee/react.js +1 -1
  10. package/dist/layout-primitives/responsive-grid/react.js +1 -1
  11. package/dist/layout-primitives/stack/react.js +1 -1
  12. package/dist/layout-primitives/switcher/react.js +1 -1
  13. package/dist/layout-primitives/vertical-writing/behavior.js +1 -1
  14. package/dist/layout-primitives/vertical-writing/react.js +1 -1
  15. package/dist/layout-primitives/with-sidebar/react.js +1 -1
  16. package/dist/library.js +1 -1
  17. package/package.json +1 -1
  18. package/src/behaviors/dividers.js +3 -1
  19. package/src/behaviors/stairs.js +3 -1
  20. package/src/layout-behavior-state.js +41 -0
  21. package/src/layout-primitives/cluster/index.jsx +25 -16
  22. package/src/layout-primitives/marquee/_index.scss +15 -7
  23. package/src/layout-primitives/marquee/behavior.js +1 -1
  24. package/src/layout-primitives/marquee/index.jsx +69 -7
  25. package/src/layout-primitives/marquee/layout.js +163 -0
  26. package/src/layout-primitives/responsive-grid/index.jsx +29 -17
  27. package/src/layout-primitives/stack/index.jsx +25 -16
  28. package/src/layout-primitives/switcher/index.jsx +26 -17
  29. package/src/layout-primitives/vertical-writing/behavior.js +3 -1
  30. package/src/layout-primitives/vertical-writing/index.jsx +11 -11
  31. package/src/layout-primitives/with-sidebar/index.jsx +27 -18
  32. package/src/library.js +162 -298
  33. package/src/register-layout-initializer.js +34 -24
  34. 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 = (target, resizeObserver, callback, scope, attributes) => {
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
- { getResizeValue, observeDirectChildrenResize = false, targetMutation, directChildMutation } = {},
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(target, resizeObserver, scheduleApply, scope, directChildMutation);
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
- return scope.dispose;
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
- export const setDividerLinewrap = (target) => {
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(' '), ignoredTokens).join(' ');
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 ?? []), 'divider:initialized', '-bol', '-linewrap', '-stack']
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, setDividerLinewrap, {
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
- export const setStairsStep = (target) => {
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
- return createLayoutObserver(target, setStairsStep, {
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 ('attributes' === entry.type) {
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) => updateColumnCountForVertical(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
- getResizeValue: getContentRectWidth,
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
- 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
-
841
- /**
842
- * Measures layout width without including transforms or rounding fractional pixels.
843
- *
844
- * @param {CSSStyleDeclaration} style Computed style.
845
- * @param {boolean} borderBox Whether to include padding and borders.
846
- * @returns {number} Width in CSS pixels.
847
- */
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
- };
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
- * 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
- }
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
- 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, () => {});
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
- * Updates generated copies and synchronizes them with the original animation.
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
- * @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.
964
+ * @returns {Element | undefined} The first newly created HTML item copy, if any.
907
965
  */
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
- }
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
- * Refreshes marquee copies while retaining the original animation's progress.
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 Target element.
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
- 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
- });
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
- // 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
- }
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
  };