@schukai/monster 4.137.2 → 4.137.4

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/package.json CHANGED
@@ -1 +1 @@
1
- {"author":"Volker Schukai","dependencies":{"@floating-ui/dom":"^1.7.6"},"description":"Monster is a simple library for creating fast, robust and lightweight websites.","homepage":"https://monsterjs.org/","keywords":["framework","web","dom","css","sass","mobile-first","app","front-end","templates","schukai","core","shopcloud","alvine","monster","buildmap","stack","observer","observable","uuid","node","nodelist","css-in-js","logger","log","theme"],"license":"AGPL 3.0","main":"source/monster.mjs","module":"source/monster.mjs","name":"@schukai/monster","repository":{"type":"git","url":"https://gitlab.schukai.com/oss/libraries/javascript/monster.git"},"type":"module","version":"4.137.2"}
1
+ {"author":"Volker Schukai","dependencies":{"@floating-ui/dom":"^1.7.6"},"description":"Monster is a simple library for creating fast, robust and lightweight websites.","homepage":"https://monsterjs.org/","keywords":["framework","web","dom","css","sass","mobile-first","app","front-end","templates","schukai","core","shopcloud","alvine","monster","buildmap","stack","observer","observable","uuid","node","nodelist","css-in-js","logger","log","theme"],"license":"AGPL 3.0","main":"source/monster.mjs","module":"source/monster.mjs","name":"@schukai/monster","repository":{"type":"git","url":"https://gitlab.schukai.com/oss/libraries/javascript/monster.git"},"type":"module","version":"4.137.4"}
@@ -25,6 +25,7 @@ import { getLocaleOfDocument } from "../../dom/locale.mjs";
25
25
  import { hasObjectLink } from "../../dom/attributes.mjs";
26
26
  import { customElementUpdaterLinkSymbol } from "../../dom/constants.mjs";
27
27
  import { getGlobalObject } from "../../types/global.mjs";
28
+ import { UniqueQueue } from "../../types/uniquequeue.mjs";
28
29
  import {
29
30
  createFloatingPopper,
30
31
  popperInstanceSymbol,
@@ -83,6 +84,23 @@ const dotsUpdateInProgressSymbol = Symbol("dotsUpdateInProgress");
83
84
  * @type {symbol}
84
85
  */
85
86
  const dotsFrameRequestSymbol = Symbol("dotsFrameRequest");
87
+ /**
88
+ * @private
89
+ * @type {symbol}
90
+ */
91
+ const dotsLayoutCacheSymbol = Symbol("dotsLayoutCache");
92
+
93
+ /**
94
+ * @private
95
+ * @type {UniqueQueue}
96
+ */
97
+ const scheduledDotsUpdates = new UniqueQueue();
98
+
99
+ /**
100
+ * @private
101
+ * @type {number|null}
102
+ */
103
+ let scheduledDotsFrameRequest = null;
86
104
 
87
105
  /**
88
106
  * @private
@@ -222,14 +240,8 @@ class ColumnBar extends CustomElement {
222
240
  this[dotsMutationObserverSymbol] = null;
223
241
  }
224
242
 
225
- if (typeof this[dotsFrameRequestSymbol] === "number") {
226
- const cancelFrame =
227
- typeof globalThis.cancelAnimationFrame === "function"
228
- ? globalThis.cancelAnimationFrame.bind(globalThis)
229
- : globalThis.clearTimeout.bind(globalThis);
230
- cancelFrame(this[dotsFrameRequestSymbol]);
231
- this[dotsFrameRequestSymbol] = null;
232
- }
243
+ removeScheduledDotsUpdate(this);
244
+ this[dotsFrameRequestSymbol] = null;
233
245
 
234
246
  if (this[closeEventHandlerSymbol]) {
235
247
  getGlobalObject("document").removeEventListener(
@@ -455,7 +467,11 @@ function initDotsObservers() {
455
467
  const parentElement = this.parentElement;
456
468
 
457
469
  if (controlElement && !this[resizeObserverSymbol]) {
458
- this[resizeObserverSymbol] = new ResizeObserver(() => {
470
+ this[resizeObserverSymbol] = new ResizeObserver((entries) => {
471
+ updateDotsLayoutResizeCache.call(this, entries, {
472
+ controlElement,
473
+ parentElement,
474
+ });
459
475
  scheduleDotsVisibilityUpdate.call(this);
460
476
  });
461
477
  this[resizeObserverSymbol].observe(controlElement);
@@ -465,10 +481,17 @@ function initDotsObservers() {
465
481
  }
466
482
 
467
483
  if (this[dotsContainerElementSymbol] && !this[dotsMutationObserverSymbol]) {
468
- this[dotsMutationObserverSymbol] = new MutationObserver(() => {
469
- if (this[dotsUpdateInProgressSymbol]) {
484
+ this[dotsMutationObserverSymbol] = new MutationObserver((mutations) => {
485
+ if (
486
+ this[dotsUpdateInProgressSymbol] ||
487
+ mutations.every(isOverflowIndicatorMutation)
488
+ ) {
470
489
  return;
471
490
  }
491
+ invalidateDotsLayoutCache.call(this, {
492
+ dots: true,
493
+ visibility: true,
494
+ });
472
495
  scheduleDotsVisibilityUpdate.call(this);
473
496
  });
474
497
  this[dotsMutationObserverSymbol].observe(this[dotsContainerElementSymbol], {
@@ -481,29 +504,43 @@ function initDotsObservers() {
481
504
  * @private
482
505
  */
483
506
  function scheduleDotsVisibilityUpdate() {
484
- if (
485
- this[dotsFrameRequestSymbol] !== null &&
486
- this[dotsFrameRequestSymbol] !== undefined
487
- ) {
488
- return;
489
- }
507
+ scheduledDotsUpdates.add(this);
508
+ this[dotsFrameRequestSymbol] = true;
490
509
 
491
- this[dotsFrameRequestSymbol] = requestAnimationFrame(() => {
492
- this[dotsFrameRequestSymbol] = null;
493
- updateDotsVisibility.call(this);
510
+ if (scheduledDotsFrameRequest !== null) return;
511
+
512
+ scheduledDotsFrameRequest = requestAnimationFrame(() => {
513
+ const pending = [];
514
+ while (!scheduledDotsUpdates.isEmpty()) {
515
+ pending.push(scheduledDotsUpdates.poll());
516
+ }
517
+ scheduledDotsFrameRequest = null;
518
+
519
+ const layouts = [];
520
+ for (const element of pending) {
521
+ element[dotsFrameRequestSymbol] = null;
522
+ const layout = measureDotsLayout.call(element);
523
+ if (layout) {
524
+ layouts.push(layout);
525
+ }
526
+ }
527
+
528
+ for (const layout of layouts) {
529
+ applyDotsLayout(layout);
530
+ }
494
531
  });
495
532
  }
496
533
 
497
534
  /**
498
535
  * @private
499
536
  */
500
- function updateDotsVisibility() {
537
+ function measureDotsLayout() {
501
538
  if (this[dotsUpdateInProgressSymbol]) {
502
- return;
539
+ return null;
503
540
  }
504
541
 
505
542
  if (!this.shadowRoot || !this[dotsContainerElementSymbol]) {
506
- return;
543
+ return null;
507
544
  }
508
545
 
509
546
  const controlElement = this.shadowRoot.querySelector(
@@ -513,81 +550,318 @@ function updateDotsVisibility() {
513
550
  const parentElement = this.parentElement;
514
551
 
515
552
  if (!controlElement || !settingsButton) {
516
- return;
553
+ return null;
517
554
  }
518
555
 
519
556
  const dotsContainer = this[dotsContainerElementSymbol];
520
- dotsContainer.classList.remove("dots-hidden");
557
+ const cache = getDotsLayoutCache.call(this);
558
+ const dots = Array.from(
559
+ dotsContainer.querySelectorAll("li:not(.dots-overflow-indicator)"),
560
+ );
521
561
 
522
- this[dotsUpdateInProgressSymbol] = true;
523
- try {
524
- const dots = Array.from(dotsContainer.querySelectorAll("li"));
525
- if (dots.length === 0) {
526
- return;
527
- }
562
+ if (dots.length === 0) {
563
+ return {
564
+ hostElement: this,
565
+ dots,
566
+ dotsContainer,
567
+ hiddenCount: 0,
568
+ hideContainer: true,
569
+ visibleDots: 0,
570
+ };
571
+ }
528
572
 
529
- for (const dot of dots) {
530
- dot.classList.remove("dots-overflow-hidden");
531
- }
573
+ if (cache.dotsCount !== dots.length) {
574
+ cache.dotsCount = dots.length;
575
+ cache.dotSlotWidth = null;
576
+ cache.lastVisibleDots = null;
577
+ cache.lastHiddenCount = null;
578
+ }
532
579
 
533
- const indicator = dotsContainer.querySelector(".dots-overflow-indicator");
534
- if (indicator) {
535
- indicator.remove();
536
- }
580
+ if (cache.controlWidth === null || cache.controlWidth <= 0) {
581
+ cache.controlWidth = controlElement.getBoundingClientRect().width;
582
+ }
583
+ if (cache.settingsWidth === null || cache.settingsWidth <= 0) {
584
+ cache.settingsWidth = settingsButton.getBoundingClientRect().width;
585
+ }
537
586
 
538
- const controlWidth = controlElement.getBoundingClientRect().width;
539
- const settingsWidth = settingsButton.getBoundingClientRect().width;
540
- const availableWidth = Math.max(
541
- 0,
542
- getAvailableWidth(parentElement, settingsWidth, this) ??
543
- controlWidth - settingsWidth - 12,
544
- );
587
+ const availableWidth = Math.max(
588
+ 0,
589
+ getAvailableWidth(parentElement, cache.settingsWidth, this, cache) ??
590
+ cache.controlWidth - cache.settingsWidth - 12,
591
+ );
545
592
 
546
- const dotSlotWidth = getDotSlotWidth(dots[0]);
547
- const maxDots =
548
- dotSlotWidth > 0
549
- ? Math.floor(availableWidth / dotSlotWidth)
550
- : dots.length;
551
- const configuredMaxVisible = parseInt(
552
- this.getOption("dots.maxVisible"),
553
- 10,
593
+ if (cache.dotSlotWidth === null || cache.dotSlotWidth <= 0) {
594
+ cache.dotSlotWidth = getDotSlotWidth(dots[0]);
595
+ }
596
+
597
+ const maxDots =
598
+ cache.dotSlotWidth > 0
599
+ ? Math.floor(availableWidth / cache.dotSlotWidth)
600
+ : dots.length;
601
+ const configuredMaxVisible = parseInt(this.getOption("dots.maxVisible"), 10);
602
+ const enforceMaxVisible =
603
+ Number.isFinite(configuredMaxVisible) && configuredMaxVisible > 0;
604
+
605
+ if (maxDots <= 1) {
606
+ return {
607
+ hostElement: this,
608
+ dots,
609
+ dotsContainer,
610
+ hiddenCount: 0,
611
+ hideContainer: true,
612
+ visibleDots: 0,
613
+ };
614
+ }
615
+
616
+ const configLimit = enforceMaxVisible
617
+ ? configuredMaxVisible
618
+ : Number.POSITIVE_INFINITY;
619
+ const baseLimit = Math.min(maxDots, configLimit, dots.length);
620
+ const needsIndicator = baseLimit < dots.length;
621
+
622
+ let visibleDots = baseLimit;
623
+ if (needsIndicator) {
624
+ visibleDots = Math.min(
625
+ Math.max(1, maxDots - 1),
626
+ configLimit,
627
+ dots.length,
554
628
  );
555
- const enforceMaxVisible =
556
- Number.isFinite(configuredMaxVisible) && configuredMaxVisible > 0;
629
+ }
557
630
 
558
- if (maxDots <= 1) {
559
- dotsContainer.classList.add("dots-hidden");
560
- return;
561
- }
631
+ return {
632
+ hostElement: this,
633
+ dots,
634
+ dotsContainer,
635
+ hiddenCount: dots.length - visibleDots,
636
+ hideContainer: false,
637
+ visibleDots,
638
+ };
639
+ }
640
+
641
+ /**
642
+ * @private
643
+ * @param {object} layout
644
+ * @return {void}
645
+ */
646
+ function applyDotsLayout(layout) {
647
+ const cache = getDotsLayoutCache.call(layout.hostElement);
562
648
 
563
- const configLimit = enforceMaxVisible
564
- ? configuredMaxVisible
565
- : Number.POSITIVE_INFINITY;
566
- const baseLimit = Math.min(maxDots, configLimit, dots.length);
567
- const needsIndicator = baseLimit < dots.length;
568
-
569
- let visibleDots = baseLimit;
570
- if (needsIndicator) {
571
- visibleDots = Math.min(
572
- Math.max(1, maxDots - 1),
573
- configLimit,
574
- dots.length,
649
+ layout.hostElement[dotsUpdateInProgressSymbol] = true;
650
+ try {
651
+ if (cache.lastContainerHidden !== layout.hideContainer) {
652
+ layout.dotsContainer.classList.toggle(
653
+ "dots-hidden",
654
+ layout.hideContainer,
575
655
  );
656
+ cache.lastContainerHidden = layout.hideContainer;
576
657
  }
577
- for (let i = visibleDots; i < dots.length; i++) {
578
- dots[i].classList.add("dots-overflow-hidden");
658
+
659
+ if (
660
+ cache.lastVisibleDots !== layout.visibleDots ||
661
+ cache.lastDotsCount !== layout.dots.length
662
+ ) {
663
+ for (let i = 0; i < layout.dots.length; i++) {
664
+ const shouldHide = i >= layout.visibleDots;
665
+ if (
666
+ layout.dots[i].classList.contains("dots-overflow-hidden") !==
667
+ shouldHide
668
+ ) {
669
+ layout.dots[i].classList.toggle("dots-overflow-hidden", shouldHide);
670
+ }
671
+ }
672
+ cache.lastVisibleDots = layout.visibleDots;
673
+ cache.lastDotsCount = layout.dots.length;
579
674
  }
580
675
 
581
- const hiddenCount = dots.length - visibleDots;
582
- if (hiddenCount > 0) {
583
- const overflowIndicator = document.createElement("li");
584
- overflowIndicator.className = "dots-overflow-indicator";
585
- overflowIndicator.textContent = `+${hiddenCount}`;
586
- dotsContainer.appendChild(overflowIndicator);
676
+ if (cache.lastHiddenCount !== layout.hiddenCount) {
677
+ updateOverflowIndicator(layout.dotsContainer, layout.hiddenCount);
678
+ cache.lastHiddenCount = layout.hiddenCount;
587
679
  }
588
680
  } finally {
589
- this[dotsUpdateInProgressSymbol] = false;
681
+ layout.hostElement[dotsUpdateInProgressSymbol] = false;
682
+ }
683
+ }
684
+
685
+ /**
686
+ * @private
687
+ * @param {MutationRecord} mutation
688
+ * @return {boolean}
689
+ */
690
+ function isOverflowIndicatorMutation(mutation) {
691
+ const changedNodes = Array.from(mutation.addedNodes).concat(
692
+ Array.from(mutation.removedNodes),
693
+ );
694
+
695
+ return (
696
+ changedNodes.length > 0 &&
697
+ changedNodes.every((node) => {
698
+ return (
699
+ node instanceof HTMLElement &&
700
+ node.classList.contains("dots-overflow-indicator")
701
+ );
702
+ })
703
+ );
704
+ }
705
+
706
+ /**
707
+ * @private
708
+ * @param {HTMLElement} dotsContainer
709
+ * @param {number} hiddenCount
710
+ * @return {void}
711
+ */
712
+ function updateOverflowIndicator(dotsContainer, hiddenCount) {
713
+ let overflowIndicator = dotsContainer.querySelector(
714
+ ".dots-overflow-indicator",
715
+ );
716
+
717
+ if (hiddenCount <= 0) {
718
+ overflowIndicator?.remove();
719
+ return;
720
+ }
721
+
722
+ const label = `+${hiddenCount}`;
723
+ if (!overflowIndicator) {
724
+ overflowIndicator = document.createElement("li");
725
+ overflowIndicator.className = "dots-overflow-indicator";
726
+ dotsContainer.appendChild(overflowIndicator);
727
+ }
728
+
729
+ if (overflowIndicator.textContent !== label) {
730
+ overflowIndicator.textContent = label;
731
+ }
732
+ }
733
+
734
+ /**
735
+ * @private
736
+ * @param {ColumnBar} element
737
+ * @return {void}
738
+ */
739
+ function removeScheduledDotsUpdate(element) {
740
+ const pending = [];
741
+ while (!scheduledDotsUpdates.isEmpty()) {
742
+ const item = scheduledDotsUpdates.poll();
743
+ if (item !== element) {
744
+ pending.push(item);
745
+ }
746
+ }
747
+ for (const item of pending) {
748
+ scheduledDotsUpdates.add(item);
749
+ }
750
+ }
751
+
752
+ /**
753
+ * @private
754
+ * @return {object}
755
+ */
756
+ function getDotsLayoutCache() {
757
+ if (!this[dotsLayoutCacheSymbol]) {
758
+ this[dotsLayoutCacheSymbol] = {
759
+ controlWidth: null,
760
+ dotSlotWidth: null,
761
+ dotsCount: null,
762
+ lastContainerHidden: null,
763
+ lastDotsCount: null,
764
+ lastHiddenCount: null,
765
+ lastVisibleDots: null,
766
+ parentGap: null,
767
+ parentWidth: null,
768
+ settingsWidth: null,
769
+ siblingsSignature: null,
770
+ siblingsWidth: null,
771
+ };
772
+ }
773
+
774
+ return this[dotsLayoutCacheSymbol];
775
+ }
776
+
777
+ /**
778
+ * @private
779
+ * @param {object} options
780
+ * @return {void}
781
+ */
782
+ function invalidateDotsLayoutCache(options = {}) {
783
+ const cache = getDotsLayoutCache.call(this);
784
+
785
+ if (options.dots === true) {
786
+ cache.dotSlotWidth = null;
787
+ cache.dotsCount = null;
788
+ }
789
+
790
+ if (options.visibility === true) {
791
+ cache.lastContainerHidden = null;
792
+ cache.lastDotsCount = null;
793
+ cache.lastHiddenCount = null;
794
+ cache.lastVisibleDots = null;
795
+ }
796
+
797
+ if (options.control === true) {
798
+ cache.controlWidth = null;
799
+ }
800
+
801
+ if (options.parent === true) {
802
+ cache.parentWidth = null;
803
+ }
804
+
805
+ if (options.settings === true) {
806
+ cache.settingsWidth = null;
807
+ }
808
+
809
+ if (options.siblings === true) {
810
+ cache.parentGap = null;
811
+ cache.siblingsSignature = null;
812
+ cache.siblingsWidth = null;
813
+ }
814
+ }
815
+
816
+ /**
817
+ * @private
818
+ * @param {ResizeObserverEntry[]} entries
819
+ * @param {object} elements
820
+ * @return {void}
821
+ */
822
+ function updateDotsLayoutResizeCache(entries, { controlElement, parentElement }) {
823
+ const cache = getDotsLayoutCache.call(this);
824
+
825
+ if (!Array.isArray(entries) || entries.length === 0) {
826
+ invalidateDotsLayoutCache.call(this, {
827
+ control: true,
828
+ parent: true,
829
+ siblings: true,
830
+ });
831
+ return;
590
832
  }
833
+
834
+ for (const entry of entries) {
835
+ const width = getResizeEntryWidth(entry);
836
+ if (width === null) continue;
837
+
838
+ if (entry.target === controlElement) {
839
+ cache.controlWidth = width;
840
+ }
841
+ if (entry.target === parentElement) {
842
+ cache.parentWidth = width;
843
+ cache.siblingsSignature = null;
844
+ cache.siblingsWidth = null;
845
+ }
846
+ }
847
+ }
848
+
849
+ /**
850
+ * @private
851
+ * @param {ResizeObserverEntry} entry
852
+ * @return {number|null}
853
+ */
854
+ function getResizeEntryWidth(entry) {
855
+ const borderSize = Array.isArray(entry.borderBoxSize)
856
+ ? entry.borderBoxSize[0]
857
+ : entry.borderBoxSize;
858
+ const contentSize = Array.isArray(entry.contentBoxSize)
859
+ ? entry.contentBoxSize[0]
860
+ : entry.contentBoxSize;
861
+ const width =
862
+ borderSize?.inlineSize ?? contentSize?.inlineSize ?? entry.contentRect?.width;
863
+
864
+ return Number.isFinite(width) ? width : null;
591
865
  }
592
866
 
593
867
  /**
@@ -607,32 +881,50 @@ function getDotSlotWidth(dot) {
607
881
  * @private
608
882
  * @param {HTMLElement|null} parent
609
883
  * @param {number} settingsWidth
884
+ * @param {ColumnBar} hostElement
885
+ * @param {object} cache
610
886
  * @return {number|null}
611
887
  */
612
- function getAvailableWidth(parent, settingsWidth, hostElement) {
888
+ function getAvailableWidth(parent, settingsWidth, hostElement, cache) {
613
889
  if (!parent) return null;
614
890
 
615
- const parentWidth = parent.getBoundingClientRect().width;
891
+ const parentWidth =
892
+ cache.parentWidth ?? parent.getBoundingClientRect().width ?? null;
616
893
  if (!parentWidth) return null;
617
-
618
- const styles = getComputedStyle(parent);
619
- const gapValue = styles.columnGap || styles.gap || "0";
620
- const gap = parseFloat(gapValue) || 0;
894
+ cache.parentWidth = parentWidth;
621
895
 
622
896
  const siblings = Array.from(parent.children).filter(
623
897
  (el) => el !== hostElement,
624
898
  );
625
- let siblingsWidth = 0;
626
- for (const sibling of siblings) {
627
- const siblingStyles = getComputedStyle(sibling);
628
- if (siblingStyles.display === "none") {
629
- continue;
899
+ const siblingsSignature = siblings
900
+ .map((sibling) => sibling.getAttribute("data-monster-role") || sibling.tagName)
901
+ .join("|");
902
+
903
+ if (
904
+ cache.siblingsSignature !== siblingsSignature ||
905
+ cache.siblingsWidth === null ||
906
+ cache.parentGap === null
907
+ ) {
908
+ const styles = getComputedStyle(parent);
909
+ const gapValue = styles.columnGap || styles.gap || "0";
910
+ cache.parentGap = parseFloat(gapValue) || 0;
911
+ cache.siblingsSignature = siblingsSignature;
912
+ cache.siblingsWidth = 0;
913
+
914
+ for (const sibling of siblings) {
915
+ const siblingStyles = getComputedStyle(sibling);
916
+ if (siblingStyles.display === "none") {
917
+ continue;
918
+ }
919
+ cache.siblingsWidth += sibling.getBoundingClientRect().width;
630
920
  }
631
- siblingsWidth += sibling.getBoundingClientRect().width;
632
921
  }
633
922
 
634
923
  const gaps = siblings.length > 0 ? siblings.length : 0;
635
- return Math.max(0, parentWidth - siblingsWidth - gap * gaps - settingsWidth);
924
+ return Math.max(
925
+ 0,
926
+ parentWidth - cache.siblingsWidth - cache.parentGap * gaps - settingsWidth,
927
+ );
636
928
  }
637
929
 
638
930
  /**
@@ -580,6 +580,7 @@ function initPopperSwitch() {
580
580
  }
581
581
 
582
582
  this[switchElementSymbol] = switchButton;
583
+ updatePopperSwitchVisibility.call(this);
583
584
  }
584
585
 
585
586
  /**
@@ -1180,25 +1181,36 @@ function adjustButtonVisibility() {
1180
1181
  const self = this;
1181
1182
 
1182
1183
  return new Promise((resolve) => {
1183
- const observer = new MutationObserver(function (mutations) {
1184
+ let resolved = false;
1185
+ let observer;
1186
+ const runIfRendered = () => {
1187
+ if (resolved === true) return;
1188
+
1184
1189
  const defCount = self.getOption("buttons.standard").length;
1185
1190
  const domCount = self[navElementSymbol].querySelectorAll(
1186
- 'button[data-monster-role="button"]',
1191
+ 'button[data-monster-role="button"][data-monster-tab-reference]',
1187
1192
  ).length;
1188
1193
 
1189
- // in drawing
1190
1194
  if (defCount !== domCount) return;
1191
1195
 
1192
- observer.disconnect();
1193
-
1196
+ resolved = true;
1197
+ observer?.disconnect();
1194
1198
  checkAndRearrangeButtons.call(self);
1195
-
1196
1199
  resolve();
1200
+ };
1201
+
1202
+ observer = new MutationObserver(function () {
1203
+ runIfRendered();
1197
1204
  });
1198
1205
 
1199
1206
  observer.observe(self[navElementSymbol], {
1200
1207
  attributes: true,
1208
+ childList: true,
1209
+ subtree: true,
1201
1210
  });
1211
+
1212
+ runIfRendered();
1213
+ getWindow().requestAnimationFrame(runIfRendered);
1202
1214
  });
1203
1215
  }
1204
1216
 
@@ -1273,16 +1285,25 @@ function rearrangeButtons() {
1273
1285
 
1274
1286
  setButtonCollections.call(this, standardButtons, popperButtons);
1275
1287
 
1276
- if (this[switchElementSymbol]) {
1277
- if (popperButtons.length > 0) {
1278
- this[switchElementSymbol].classList.remove("hidden");
1279
- } else {
1280
- this[switchElementSymbol].classList.add("hidden");
1281
- }
1282
- }
1288
+ updatePopperSwitchVisibility.call(this);
1283
1289
  });
1284
1290
  }
1285
1291
 
1292
+ /**
1293
+ * @private
1294
+ */
1295
+ function updatePopperSwitchVisibility() {
1296
+ if (!this[switchElementSymbol]) {
1297
+ return;
1298
+ }
1299
+
1300
+ if (this.getOption("buttons.popper").length > 0) {
1301
+ this[switchElementSymbol].classList.remove("hidden");
1302
+ } else {
1303
+ this[switchElementSymbol].classList.add("hidden");
1304
+ }
1305
+ }
1306
+
1286
1307
  /**
1287
1308
  * @private
1288
1309
  * @param {string} ref
@@ -557,4 +557,122 @@ describe("Datatable drag scroll", function () {
557
557
  globalThis.requestAnimationFrame = originalGlobalRequestAnimationFrame;
558
558
  }
559
559
  });
560
+
561
+ it("coalesces column bar dot updates and ignores overflow indicator mutations", async function () {
562
+ const OriginalResizeObserver = window.ResizeObserver;
563
+ const originalGlobalResizeObserver = globalThis.ResizeObserver;
564
+ const originalRequestAnimationFrame = window.requestAnimationFrame;
565
+ const originalGlobalRequestAnimationFrame = globalThis.requestAnimationFrame;
566
+
567
+ class TrackingResizeObserver extends ResizeObserverMock {
568
+ static instances = [];
569
+
570
+ constructor(callback) {
571
+ super(callback);
572
+ TrackingResizeObserver.instances.push(this);
573
+ }
574
+ }
575
+
576
+ try {
577
+ window.ResizeObserver = TrackingResizeObserver;
578
+ globalThis.ResizeObserver = TrackingResizeObserver;
579
+
580
+ const mocks = document.getElementById("mocks");
581
+ const wrapper = document.createElement("div");
582
+ let parentWidth = 80;
583
+
584
+ Object.defineProperty(wrapper, "getBoundingClientRect", {
585
+ configurable: true,
586
+ value: () => ({ width: parentWidth }),
587
+ });
588
+
589
+ mocks.appendChild(wrapper);
590
+
591
+ const createColumnBar = async () => {
592
+ const columnBar = document.createElement("monster-column-bar");
593
+ columnBar.setOption("columns", [
594
+ { index: 0, name: "One", visible: true },
595
+ { index: 1, name: "Two", visible: true },
596
+ { index: 2, name: "Three", visible: true },
597
+ { index: 3, name: "Four", visible: true },
598
+ { index: 4, name: "Five", visible: true },
599
+ { index: 5, name: "Six", visible: true },
600
+ ]);
601
+ wrapper.appendChild(columnBar);
602
+
603
+ await new Promise((resolve) => setTimeout(resolve, 30));
604
+
605
+ const control = columnBar.shadowRoot.querySelector(
606
+ "[data-monster-role=control]",
607
+ );
608
+ const settingsButton = columnBar.shadowRoot.querySelector(
609
+ "[data-monster-role=settings-button]",
610
+ );
611
+ const dotsContainer = columnBar.shadowRoot.querySelector(
612
+ "[data-monster-role=dots]",
613
+ );
614
+ const dots = Array.from(dotsContainer.querySelectorAll("li"));
615
+
616
+ Object.defineProperty(control, "getBoundingClientRect", {
617
+ configurable: true,
618
+ value: () => ({ width: parentWidth }),
619
+ });
620
+ Object.defineProperty(settingsButton, "getBoundingClientRect", {
621
+ configurable: true,
622
+ value: () => ({ width: 20 }),
623
+ });
624
+ dots.forEach((dot) => {
625
+ Object.defineProperty(dot, "getBoundingClientRect", {
626
+ configurable: true,
627
+ value: () => ({ width: 20 }),
628
+ });
629
+ });
630
+
631
+ return { columnBar, control, dotsContainer };
632
+ };
633
+
634
+ const first = await createColumnBar();
635
+ const second = await createColumnBar();
636
+
637
+ const firstResizeObserver = TrackingResizeObserver.instances.find(
638
+ (observer) => observer.elements.includes(first.control),
639
+ );
640
+ const secondResizeObserver = TrackingResizeObserver.instances.find(
641
+ (observer) => observer.elements.includes(second.control),
642
+ );
643
+
644
+ expect(firstResizeObserver).to.exist;
645
+ expect(secondResizeObserver).to.exist;
646
+
647
+ const scheduledCallbacks = [];
648
+ window.requestAnimationFrame = (callback) => {
649
+ scheduledCallbacks.push(callback);
650
+ return scheduledCallbacks.length;
651
+ };
652
+ globalThis.requestAnimationFrame = window.requestAnimationFrame;
653
+
654
+ firstResizeObserver.triggerResize([]);
655
+ secondResizeObserver.triggerResize([]);
656
+
657
+ expect(scheduledCallbacks.length).to.equal(1);
658
+
659
+ scheduledCallbacks.shift()();
660
+
661
+ expect(
662
+ first.dotsContainer.querySelector(".dots-overflow-indicator"),
663
+ ).to.exist;
664
+ expect(
665
+ second.dotsContainer.querySelector(".dots-overflow-indicator"),
666
+ ).to.exist;
667
+
668
+ await new Promise((resolve) => setTimeout(resolve, 0));
669
+
670
+ expect(scheduledCallbacks.length).to.equal(0);
671
+ } finally {
672
+ window.ResizeObserver = OriginalResizeObserver;
673
+ globalThis.ResizeObserver = originalGlobalResizeObserver;
674
+ window.requestAnimationFrame = originalRequestAnimationFrame;
675
+ globalThis.requestAnimationFrame = originalGlobalRequestAnimationFrame;
676
+ }
677
+ });
560
678
  });