@schukai/monster 4.145.2 → 4.146.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -35,7 +35,11 @@ import {
35
35
  findTargetElementFromEvent,
36
36
  fireCustomEvent,
37
37
  } from "../../dom/events.mjs";
38
- import { getDocument, getWindow } from "../../dom/util.mjs";
38
+ import {
39
+ findElementWithSelectorUpwards,
40
+ getDocument,
41
+ getWindow,
42
+ } from "../../dom/util.mjs";
39
43
  import { random } from "../../math/random.mjs";
40
44
  import { getGlobal } from "../../types/global.mjs";
41
45
  import { ID } from "../../types/id.mjs";
@@ -60,6 +64,7 @@ import {
60
64
  setEventListenersModifiers,
61
65
  } from "../form/util/popper.mjs";
62
66
  import { getLocaleOfDocument } from "../../dom/locale.mjs";
67
+ import { generateComponentConfigKey } from "../host/util.mjs";
63
68
 
64
69
  export { Tabs };
65
70
 
@@ -86,6 +91,12 @@ const controlElementSymbol = Symbol("controlElement");
86
91
  * @type {symbol}
87
92
  */
88
93
  const navElementSymbol = Symbol("navElement");
94
+
95
+ /**
96
+ * @private
97
+ * @type {symbol}
98
+ */
99
+ const measurementNavElementSymbol = Symbol("measurementNavElement");
89
100
  /**
90
101
  * @private
91
102
  * @type {symbol}
@@ -103,6 +114,30 @@ const changeTabEventHandler = Symbol("changeTabEventHandler");
103
114
  */
104
115
  const removeTabEventHandler = Symbol("removeTabEventHandler");
105
116
 
117
+ /**
118
+ * @private
119
+ * @type {symbol}
120
+ */
121
+ const dragStartEventHandler = Symbol("dragStartEventHandler");
122
+
123
+ /**
124
+ * @private
125
+ * @type {symbol}
126
+ */
127
+ const dragOverEventHandler = Symbol("dragOverEventHandler");
128
+
129
+ /**
130
+ * @private
131
+ * @type {symbol}
132
+ */
133
+ const dropEventHandler = Symbol("dropEventHandler");
134
+
135
+ /**
136
+ * @private
137
+ * @type {symbol}
138
+ */
139
+ const dragEndEventHandler = Symbol("dragEndEventHandler");
140
+
106
141
  /**
107
142
  * @private
108
143
  * @type {symbol}
@@ -154,6 +189,42 @@ const resizeObserverSymbol = Symbol("resizeObserver");
154
189
  */
155
190
  const activeReferenceSymbol = Symbol("activeReference");
156
191
 
192
+ /**
193
+ * local symbol
194
+ * @private
195
+ * @type {symbol}
196
+ */
197
+ const draggingReferenceSymbol = Symbol("draggingReference");
198
+
199
+ /**
200
+ * local symbol
201
+ * @private
202
+ * @type {symbol}
203
+ */
204
+ const suppressOrderConfigSaveSymbol = Symbol("suppressOrderConfigSave");
205
+
206
+ /**
207
+ * @private
208
+ * @type {symbol}
209
+ */
210
+ const rearrangeFrameSymbol = Symbol("rearrangeFrame");
211
+
212
+ /**
213
+ * @private
214
+ * @type {symbol}
215
+ */
216
+ const buttonCollectionsSignatureSymbol = Symbol("buttonCollectionsSignature");
217
+
218
+ /**
219
+ * @private
220
+ * @type {symbol}
221
+ */
222
+ const layoutSignatureHistorySymbol = Symbol("layoutSignatureHistory");
223
+
224
+ const LAYOUT_SWITCH_HYSTERESIS = 16;
225
+ const LAYOUT_OSCILLATION_HISTORY_LIMIT = 4;
226
+ const LAYOUT_OSCILLATION_TIME_WINDOW_MS = 1000;
227
+
157
228
  /**
158
229
  * A Tabs Control
159
230
  *
@@ -197,6 +268,11 @@ class Tabs extends CustomElement {
197
268
  * @property {string} features.removeBehavior Remove behavior, auto, next, previous and none
198
269
  * @property {boolean} features.openFirst Open the first tab when no active tab is set
199
270
  * @property {boolean} features.deriveLabelFromContent=true Generate labels from panel text when no `data-monster-button-label` is set
271
+ * @property {Object} features.order Tab ordering features
272
+ * @property {boolean} features.order.drag=true Allow tab headers to be reordered by drag and drop
273
+ * @property {boolean} features.order.persist=false Persist reordered tabs through the host config manager
274
+ * @property {Object} config Host configuration options
275
+ * @property {string} config.instanceKey Stable config identity for persisted tab order
200
276
  *
201
277
  * Tab panels can use `data-monster-name` as stable public identity, while
202
278
  * DOM ids remain the internal button reference. `data-monster-tab-available`
@@ -250,6 +326,14 @@ class Tabs extends CustomElement {
250
326
  removeBehavior: "auto",
251
327
  openFirst: true,
252
328
  deriveLabelFromContent: true,
329
+ order: {
330
+ drag: true,
331
+ persist: false,
332
+ },
333
+ },
334
+
335
+ config: {
336
+ instanceKey: null,
253
337
  },
254
338
 
255
339
  classes: {
@@ -305,11 +389,22 @@ class Tabs extends CustomElement {
305
389
  attachTabChangeObserver.call(this);
306
390
 
307
391
  // setup structure
308
- initTabButtons.call(this).then(() => {
309
- initPopperSwitch.call(this);
310
- initPopper.call(this);
311
- attachResizeObserver.call(this);
312
- });
392
+ const initTabs =
393
+ isTabOrderPersistenceEnabled.call(this) === true
394
+ ? initTabOrderFromHostConfig
395
+ .call(this)
396
+ .then(() => initTabButtons.call(this))
397
+ : initTabButtons.call(this);
398
+
399
+ initTabs
400
+ .then(() => {
401
+ initPopperSwitch.call(this);
402
+ initPopper.call(this);
403
+ attachResizeObserver.call(this);
404
+ })
405
+ .catch((e) => {
406
+ addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, e.message);
407
+ });
313
408
  }
314
409
 
315
410
  /**
@@ -553,6 +648,26 @@ class Tabs extends CustomElement {
553
648
 
554
649
  this[popperInstanceSymbol]?.destroy();
555
650
  this[tabListMutationObserverSymbol]?.disconnect();
651
+ this[resizeObserverSymbol]?.disconnect();
652
+ this[navElementSymbol]?.removeEventListener(
653
+ "dragstart",
654
+ this[dragStartEventHandler],
655
+ );
656
+ this[navElementSymbol]?.removeEventListener(
657
+ "dragover",
658
+ this[dragOverEventHandler],
659
+ );
660
+ this[navElementSymbol]?.removeEventListener("drop", this[dropEventHandler]);
661
+ this[navElementSymbol]?.removeEventListener(
662
+ "dragend",
663
+ this[dragEndEventHandler],
664
+ );
665
+ if (this[rearrangeFrameSymbol] !== undefined) {
666
+ getWindow().cancelAnimationFrame(this[rearrangeFrameSymbol]);
667
+ delete this[rearrangeFrameSymbol];
668
+ }
669
+ this[measurementNavElementSymbol]?.remove();
670
+ delete this[measurementNavElementSymbol];
556
671
  }
557
672
  }
558
673
 
@@ -1233,6 +1348,93 @@ function initEventHandler() {
1233
1348
  this[navElementSymbol].addEventListener("touch", this[changeTabEventHandler]);
1234
1349
  this[navElementSymbol].addEventListener("click", this[changeTabEventHandler]);
1235
1350
 
1351
+ this[dragStartEventHandler] = (event) => {
1352
+ if (isTabDragEnabled.call(this) !== true) {
1353
+ return;
1354
+ }
1355
+
1356
+ const button = findTargetElementFromEvent(event, ATTRIBUTE_ROLE, "button");
1357
+ if (!(button instanceof HTMLButtonElement) || button.disabled === true) {
1358
+ return;
1359
+ }
1360
+
1361
+ const reference = button.getAttribute(`${ATTRIBUTE_PREFIX}tab-reference`);
1362
+ if (!reference) {
1363
+ return;
1364
+ }
1365
+
1366
+ this[draggingReferenceSymbol] = reference;
1367
+ button.setAttribute("data-monster-dragging", "true");
1368
+ event.dataTransfer?.setData("text/plain", reference);
1369
+ if (event.dataTransfer) {
1370
+ event.dataTransfer.effectAllowed = "move";
1371
+ }
1372
+ };
1373
+
1374
+ this[dragOverEventHandler] = (event) => {
1375
+ if (isTabDragEnabled.call(this) !== true) {
1376
+ return;
1377
+ }
1378
+
1379
+ const button = findTargetElementFromEvent(event, ATTRIBUTE_ROLE, "button");
1380
+ if (
1381
+ !(button instanceof HTMLButtonElement) ||
1382
+ button.disabled === true ||
1383
+ !this[draggingReferenceSymbol]
1384
+ ) {
1385
+ return;
1386
+ }
1387
+
1388
+ event.preventDefault();
1389
+ if (event.dataTransfer) {
1390
+ event.dataTransfer.dropEffect = "move";
1391
+ }
1392
+ };
1393
+
1394
+ this[dropEventHandler] = (event) => {
1395
+ if (isTabDragEnabled.call(this) !== true) {
1396
+ return;
1397
+ }
1398
+
1399
+ const button = findTargetElementFromEvent(event, ATTRIBUTE_ROLE, "button");
1400
+ if (!(button instanceof HTMLButtonElement) || button.disabled === true) {
1401
+ return;
1402
+ }
1403
+
1404
+ const sourceReference = this[draggingReferenceSymbol];
1405
+ const targetReference = button.getAttribute(
1406
+ `${ATTRIBUTE_PREFIX}tab-reference`,
1407
+ );
1408
+ if (
1409
+ !sourceReference ||
1410
+ !targetReference ||
1411
+ sourceReference === targetReference
1412
+ ) {
1413
+ return;
1414
+ }
1415
+
1416
+ event.preventDefault();
1417
+ reorderTabPanels.call(this, sourceReference, targetReference);
1418
+ clearDraggingButton.call(this);
1419
+ delete this[draggingReferenceSymbol];
1420
+ };
1421
+
1422
+ this[dragEndEventHandler] = () => {
1423
+ clearDraggingButton.call(this);
1424
+ delete this[draggingReferenceSymbol];
1425
+ };
1426
+
1427
+ this[navElementSymbol].addEventListener(
1428
+ "dragstart",
1429
+ this[dragStartEventHandler],
1430
+ );
1431
+ this[navElementSymbol].addEventListener(
1432
+ "dragover",
1433
+ this[dragOverEventHandler],
1434
+ );
1435
+ this[navElementSymbol].addEventListener("drop", this[dropEventHandler]);
1436
+ this[navElementSymbol].addEventListener("dragend", this[dragEndEventHandler]);
1437
+
1236
1438
  return this;
1237
1439
  }
1238
1440
 
@@ -1327,6 +1529,8 @@ function initTabButtons() {
1327
1529
  throw new Error("no shadow-root is defined");
1328
1530
  }
1329
1531
 
1532
+ resetLayoutSignatureHistory.call(this);
1533
+
1330
1534
  let activeReference;
1331
1535
  let invalidActive = false;
1332
1536
  const previousActiveReference = this[activeReferenceSymbol] || null;
@@ -1399,6 +1603,7 @@ function initTabButtons() {
1399
1603
  state,
1400
1604
  class: classes,
1401
1605
  disabled,
1606
+ draggable: isTabDragEnabled.call(this) === true && disabled !== true,
1402
1607
  title: disabled === true ? disabledReason : null,
1403
1608
  "aria-label": disabled === true ? disabledReason : null,
1404
1609
  remove,
@@ -1408,8 +1613,11 @@ function initTabButtons() {
1408
1613
  });
1409
1614
  }
1410
1615
 
1411
- setButtonCollections.call(this, buttons, []);
1616
+ const { standardButtons, popperButtons } =
1617
+ splitButtonsByCurrentPopperReferences.call(this, buttons);
1618
+ setButtonCollections.call(this, standardButtons, popperButtons);
1412
1619
  this.setOption("marker", random());
1620
+ this[dimensionsSymbol].setVia("data.calculated", false);
1413
1621
 
1414
1622
  return adjustButtonVisibility.call(this).then(() => {
1415
1623
  if (
@@ -1491,7 +1699,9 @@ function adjustButtonVisibility() {
1491
1699
  const runIfRendered = () => {
1492
1700
  if (resolved === true) return;
1493
1701
 
1494
- const defCount = self.getOption("buttons.standard").length;
1702
+ const defCount =
1703
+ self.getOption("buttons.standard").length +
1704
+ self.getOption("buttons.popper").length;
1495
1705
  const domCount = self[navElementSymbol].querySelectorAll(
1496
1706
  'button[data-monster-role="button"][data-monster-tab-reference]',
1497
1707
  ).length;
@@ -1529,7 +1739,7 @@ function getDimValue(value) {
1529
1739
  return 0;
1530
1740
  }
1531
1741
 
1532
- const valueAsInt = parseInt(value, 10);
1742
+ const valueAsInt = parseFloat(value);
1533
1743
 
1534
1744
  if (isNaN(valueAsInt)) {
1535
1745
  return 0;
@@ -1554,7 +1764,7 @@ function calcBoxWidth(node) {
1554
1764
  getDimValue(bounding["width"]) +
1555
1765
  getDimValue(dim["border-right-width"]) +
1556
1766
  getDimValue(dim["margin-right"]) +
1557
- getDimValue(dim["padding-left"])
1767
+ getDimValue(dim["padding-right"])
1558
1768
  );
1559
1769
  }
1560
1770
 
@@ -1563,10 +1773,13 @@ function calcBoxWidth(node) {
1563
1773
  * @return {Object}
1564
1774
  */
1565
1775
  function rearrangeButtons() {
1566
- getWindow().requestAnimationFrame(() => {
1567
- const standardButtons = [];
1568
- const popperButtons = [];
1569
- let sum = 0;
1776
+ if (this[rearrangeFrameSymbol] !== undefined) {
1777
+ return;
1778
+ }
1779
+
1780
+ this[rearrangeFrameSymbol] = getWindow().requestAnimationFrame(() => {
1781
+ delete this[rearrangeFrameSymbol];
1782
+
1570
1783
  const space = this[dimensionsSymbol].getVia("data.space");
1571
1784
 
1572
1785
  if (space <= 0) {
@@ -1576,16 +1789,30 @@ function rearrangeButtons() {
1576
1789
  const buttons = this.getOption("buttons.standard").concat(
1577
1790
  this.getOption("buttons.popper"),
1578
1791
  );
1579
- for (const [, button] of buttons.entries()) {
1580
- const ref = button?.reference;
1581
-
1582
- sum += resolveButtonWidth.call(this, ref);
1792
+ const widths = buttons.map((button) =>
1793
+ resolveButtonWidth.call(this, button?.reference),
1794
+ );
1795
+ const switchWidth = resolveSwitchWidth.call(this);
1796
+ const switchCurrentlyVisible =
1797
+ this[switchElementSymbol] instanceof HTMLElement &&
1798
+ !this[switchElementSymbol].classList.contains("hidden");
1799
+ const { standardButtons, popperButtons } = resolveOverflowButtonLayout({
1800
+ buttons,
1801
+ widths,
1802
+ space,
1803
+ switchWidth,
1804
+ switchCurrentlyVisible,
1805
+ });
1583
1806
 
1584
- if (sum > space) {
1585
- popperButtons.push(clone(button));
1586
- } else {
1587
- standardButtons.push(clone(button));
1588
- }
1807
+ if (
1808
+ shouldSuppressOscillatingTabLayout.call(
1809
+ this,
1810
+ space,
1811
+ standardButtons,
1812
+ popperButtons,
1813
+ )
1814
+ ) {
1815
+ return;
1589
1816
  }
1590
1817
 
1591
1818
  setButtonCollections.call(this, standardButtons, popperButtons);
@@ -1594,6 +1821,74 @@ function rearrangeButtons() {
1594
1821
  });
1595
1822
  }
1596
1823
 
1824
+ /**
1825
+ * @private
1826
+ * @param {Object} options
1827
+ * @param {Object[]} options.buttons
1828
+ * @param {number[]} options.widths
1829
+ * @param {number} options.space
1830
+ * @param {number} options.switchWidth
1831
+ * @param {boolean} options.switchCurrentlyVisible
1832
+ * @return {{standardButtons: Object[], popperButtons: Object[]}}
1833
+ */
1834
+ function resolveOverflowButtonLayout({
1835
+ buttons,
1836
+ widths,
1837
+ space,
1838
+ switchWidth,
1839
+ switchCurrentlyVisible,
1840
+ }) {
1841
+ let layout;
1842
+ if (switchCurrentlyVisible) {
1843
+ layout = fitButtonsIntoSpace(
1844
+ buttons,
1845
+ widths,
1846
+ space - switchWidth - LAYOUT_SWITCH_HYSTERESIS,
1847
+ );
1848
+ if (layout.popperButtons.length === 0) {
1849
+ layout = fitButtonsIntoSpace(buttons, widths, space);
1850
+ }
1851
+ return layout;
1852
+ }
1853
+
1854
+ layout = fitButtonsIntoSpace(
1855
+ buttons,
1856
+ widths,
1857
+ space - LAYOUT_SWITCH_HYSTERESIS,
1858
+ );
1859
+ if (layout.popperButtons.length > 0) {
1860
+ return fitButtonsIntoSpace(buttons, widths, space - switchWidth);
1861
+ }
1862
+
1863
+ return fitButtonsIntoSpace(buttons, widths, space);
1864
+ }
1865
+
1866
+ /**
1867
+ * @private
1868
+ * @param {Object[]} buttons
1869
+ * @param {number[]} widths
1870
+ * @param {number} availableSpace
1871
+ * @return {{standardButtons: Object[], popperButtons: Object[]}}
1872
+ */
1873
+ function fitButtonsIntoSpace(buttons, widths, availableSpace) {
1874
+ let sum = 0;
1875
+ const standardButtons = [];
1876
+ const popperButtons = [];
1877
+ const boundedSpace = Math.max(0, availableSpace);
1878
+
1879
+ for (const [index, button] of buttons.entries()) {
1880
+ const width = widths[index];
1881
+ if (popperButtons.length > 0 || sum + width > boundedSpace) {
1882
+ popperButtons.push(clone(button));
1883
+ } else {
1884
+ sum += width;
1885
+ standardButtons.push(clone(button));
1886
+ }
1887
+ }
1888
+
1889
+ return { standardButtons, popperButtons };
1890
+ }
1891
+
1597
1892
  /**
1598
1893
  * @private
1599
1894
  */
@@ -1625,11 +1920,135 @@ function resolveButtonWidth(ref) {
1625
1920
  return 0;
1626
1921
  }
1627
1922
 
1628
- width = calcBoxWidth.call(this, element);
1629
- this[dimensionsSymbol].setVia(`data.button.${ref}`, width);
1923
+ width = measureButtonWidth.call(this, element);
1924
+ if (width > 0) {
1925
+ this[dimensionsSymbol].setVia(`data.button.${ref}`, width);
1926
+ }
1630
1927
  return width;
1631
1928
  }
1632
1929
 
1930
+ /**
1931
+ * @private
1932
+ * @param {boolean} force
1933
+ * @return {number}
1934
+ */
1935
+ function resolveSwitchWidth(force = false) {
1936
+ let width = this[dimensionsSymbol].getVia("data.switchWidth", 0);
1937
+ if (force !== true && width > 0) {
1938
+ return width;
1939
+ }
1940
+
1941
+ if (!(this[switchElementSymbol] instanceof HTMLElement)) {
1942
+ return 0;
1943
+ }
1944
+
1945
+ width = measureElementWidthInNavigation.call(
1946
+ this,
1947
+ this[switchElementSymbol],
1948
+ {
1949
+ removeHiddenClass: true,
1950
+ },
1951
+ );
1952
+ this[dimensionsSymbol].setVia("data.switchWidth", width);
1953
+ return width;
1954
+ }
1955
+
1956
+ /**
1957
+ * @private
1958
+ * @param {HTMLButtonElement} element
1959
+ * @return {number}
1960
+ */
1961
+ function measureButtonWidth(element) {
1962
+ if (
1963
+ element.closest(`[${ATTRIBUTE_ROLE}=popper-nav]`) instanceof HTMLElement
1964
+ ) {
1965
+ return measureButtonWidthInNavigation.call(this, element);
1966
+ }
1967
+
1968
+ const width = calcBoxWidth.call(this, element);
1969
+ if (width > 0) {
1970
+ return width;
1971
+ }
1972
+
1973
+ return measureButtonWidthInNavigation.call(this, element);
1974
+ }
1975
+
1976
+ /**
1977
+ * @private
1978
+ * @param {HTMLButtonElement} element
1979
+ * @return {number}
1980
+ */
1981
+ function measureButtonWidthInNavigation(element) {
1982
+ return measureElementWidthInNavigation.call(this, element);
1983
+ }
1984
+
1985
+ /**
1986
+ * @private
1987
+ * @param {HTMLElement} element
1988
+ * @param {{removeHiddenClass?: boolean}} options
1989
+ * @return {number}
1990
+ */
1991
+ function measureElementWidthInNavigation(element, options = {}) {
1992
+ const measurementNav = ensureMeasurementNav.call(this);
1993
+ const cloneElement = element.cloneNode(true);
1994
+ prepareMeasurementElement(cloneElement, options);
1995
+
1996
+ measurementNav.appendChild(cloneElement);
1997
+ const width = calcBoxWidth.call(this, cloneElement);
1998
+ cloneElement.remove();
1999
+
2000
+ return width;
2001
+ }
2002
+
2003
+ /**
2004
+ * @private
2005
+ * @param {HTMLElement} element
2006
+ * @param {{removeHiddenClass?: boolean}} options
2007
+ * @return {void}
2008
+ */
2009
+ function prepareMeasurementElement(element, options = {}) {
2010
+ element.setAttribute("aria-hidden", "true");
2011
+ element.setAttribute("tabindex", "-1");
2012
+ element.removeAttribute("data-monster-tab-reference");
2013
+ element.removeAttribute("id");
2014
+ element.dataset.monsterMeasurement = "true";
2015
+
2016
+ if (options.removeHiddenClass === true) {
2017
+ element.classList.remove("hidden");
2018
+ }
2019
+
2020
+ element.style.position = "";
2021
+ element.style.visibility = "";
2022
+ element.style.pointerEvents = "";
2023
+ element.style.inset = "";
2024
+ }
2025
+
2026
+ /**
2027
+ * @private
2028
+ * @return {HTMLElement}
2029
+ */
2030
+ function ensureMeasurementNav() {
2031
+ if (this[measurementNavElementSymbol] instanceof HTMLElement) {
2032
+ return this[measurementNavElementSymbol];
2033
+ }
2034
+
2035
+ const nav = getDocument().createElement("nav");
2036
+ nav.setAttribute(ATTRIBUTE_ROLE, "nav");
2037
+ nav.setAttribute("aria-hidden", "true");
2038
+ nav.dataset.monsterMeasurement = "true";
2039
+ nav.style.position = "absolute";
2040
+ nav.style.visibility = "hidden";
2041
+ nav.style.pointerEvents = "none";
2042
+ nav.style.inset = "0 auto auto 0";
2043
+ nav.style.width = "max-content";
2044
+ nav.style.height = "auto";
2045
+ nav.style.overflow = "visible";
2046
+
2047
+ this[controlElementSymbol].appendChild(nav);
2048
+ this[measurementNavElementSymbol] = nav;
2049
+ return nav;
2050
+ }
2051
+
1633
2052
  /**
1634
2053
  * Keep the main navigation and popper navigation models in sync with one update.
1635
2054
  *
@@ -1641,12 +2060,302 @@ function resolveButtonWidth(ref) {
1641
2060
  * @param {Object[]} popperButtons
1642
2061
  */
1643
2062
  function setButtonCollections(standardButtons, popperButtons) {
2063
+ const signature = getButtonCollectionsSignature(
2064
+ standardButtons,
2065
+ popperButtons,
2066
+ );
2067
+ if (this[buttonCollectionsSignatureSymbol] === signature) {
2068
+ return;
2069
+ }
2070
+
2071
+ const currentSignature = getButtonCollectionsSignature(
2072
+ this.getOption("buttons.standard", []),
2073
+ this.getOption("buttons.popper", []),
2074
+ );
2075
+ if (currentSignature === signature) {
2076
+ this[buttonCollectionsSignatureSymbol] = signature;
2077
+ return;
2078
+ }
2079
+
2080
+ this[buttonCollectionsSignatureSymbol] = signature;
1644
2081
  this.setOption("buttons", {
1645
2082
  standard: clone(standardButtons),
1646
2083
  popper: clone(popperButtons),
1647
2084
  });
1648
2085
  }
1649
2086
 
2087
+ /**
2088
+ * @private
2089
+ * @param {Object[]} standardButtons
2090
+ * @param {Object[]} popperButtons
2091
+ * @return {string}
2092
+ */
2093
+ function getButtonCollectionsSignature(standardButtons, popperButtons) {
2094
+ return JSON.stringify({
2095
+ standard: standardButtons.map(getButtonSignature),
2096
+ popper: popperButtons.map(getButtonSignature),
2097
+ });
2098
+ }
2099
+
2100
+ /**
2101
+ * @private
2102
+ * @param {Object} button
2103
+ * @return {Object}
2104
+ */
2105
+ function getButtonSignature(button) {
2106
+ return {
2107
+ reference: button?.reference,
2108
+ name: button?.name,
2109
+ tab: button?.tab,
2110
+ label: button?.label,
2111
+ error: button?.error,
2112
+ state: button?.state,
2113
+ class: button?.class,
2114
+ disabled: button?.disabled,
2115
+ draggable: button?.draggable,
2116
+ title: button?.title,
2117
+ "aria-label": button?.["aria-label"],
2118
+ remove: button?.remove,
2119
+ kind: button?.kind,
2120
+ priority: button?.priority,
2121
+ group: button?.group,
2122
+ };
2123
+ }
2124
+
2125
+ /**
2126
+ * @private
2127
+ * @param {Object[]} buttons
2128
+ * @return {{standardButtons: Object[], popperButtons: Object[]}}
2129
+ */
2130
+ function splitButtonsByCurrentPopperReferences(buttons) {
2131
+ const popperReferences = new Set(
2132
+ this.getOption("buttons.popper", []).map((button) => button.reference),
2133
+ );
2134
+ const standardButtons = [];
2135
+ const popperButtons = [];
2136
+ for (const button of buttons) {
2137
+ if (popperReferences.has(button.reference)) {
2138
+ popperButtons.push(button);
2139
+ } else {
2140
+ standardButtons.push(button);
2141
+ }
2142
+ }
2143
+
2144
+ return { standardButtons, popperButtons };
2145
+ }
2146
+
2147
+ /**
2148
+ * @private
2149
+ * @return {boolean}
2150
+ */
2151
+ function isTabDragEnabled() {
2152
+ return this.getOption("features.order.drag") !== false;
2153
+ }
2154
+
2155
+ /**
2156
+ * @private
2157
+ * @return {boolean}
2158
+ */
2159
+ function isTabOrderPersistenceEnabled() {
2160
+ return this.getOption("features.order.persist") === true;
2161
+ }
2162
+
2163
+ /**
2164
+ * @private
2165
+ * @return {boolean}
2166
+ */
2167
+ function hasConfigIdentity() {
2168
+ return (
2169
+ (isString(this.id) && this.id.trim() !== "") ||
2170
+ (isString(this.getOption("config.instanceKey")) &&
2171
+ this.getOption("config.instanceKey").trim() !== "")
2172
+ );
2173
+ }
2174
+
2175
+ /**
2176
+ * @private
2177
+ * @return {string}
2178
+ */
2179
+ function getTabOrderConfigKey() {
2180
+ return generateComponentConfigKey("tabs", this?.id, "tab_order", {
2181
+ instanceKey: this.getOption("config.instanceKey"),
2182
+ });
2183
+ }
2184
+
2185
+ /**
2186
+ * @private
2187
+ * @return {HTMLElement|null}
2188
+ */
2189
+ function getHostElement() {
2190
+ return findElementWithSelectorUpwards(this, "monster-host");
2191
+ }
2192
+
2193
+ /**
2194
+ * @private
2195
+ * @return {string[]}
2196
+ */
2197
+ function getCurrentTabOrder() {
2198
+ return getAllTabPanels
2199
+ .call(this)
2200
+ .map((node) => getTabPublicReference(node))
2201
+ .filter((reference) => isString(reference) && reference.trim() !== "");
2202
+ }
2203
+
2204
+ /**
2205
+ * @private
2206
+ * @return {Promise}
2207
+ */
2208
+ function initTabOrderFromHostConfig() {
2209
+ if (isTabOrderPersistenceEnabled.call(this) !== true) {
2210
+ return Promise.resolve();
2211
+ }
2212
+
2213
+ const host = getHostElement.call(this);
2214
+ if (!(host && hasConfigIdentity.call(this))) {
2215
+ return Promise.resolve();
2216
+ }
2217
+
2218
+ const configKey = getTabOrderConfigKey.call(this);
2219
+ return host
2220
+ .hasConfig(configKey)
2221
+ .then((hasConfig) => {
2222
+ if (hasConfig !== true) {
2223
+ return null;
2224
+ }
2225
+ return host.getConfig(configKey);
2226
+ })
2227
+ .then((order) => {
2228
+ if (!isArray(order)) {
2229
+ return;
2230
+ }
2231
+ this[suppressOrderConfigSaveSymbol] = true;
2232
+ applyTabOrder.call(this, order);
2233
+ this[suppressOrderConfigSaveSymbol] = false;
2234
+ })
2235
+ .catch((error) => {
2236
+ this[suppressOrderConfigSaveSymbol] = false;
2237
+ addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, String(error));
2238
+ });
2239
+ }
2240
+
2241
+ /**
2242
+ * @private
2243
+ * @param {string[]} order
2244
+ * @return {void}
2245
+ */
2246
+ function applyTabOrder(order) {
2247
+ const panels = getAllTabPanels.call(this);
2248
+ const panelsByReference = new Map();
2249
+ for (const panel of panels) {
2250
+ panelsByReference.set(getTabReference(panel), panel);
2251
+ const name = getTabName(panel);
2252
+ if (name) {
2253
+ panelsByReference.set(name, panel);
2254
+ }
2255
+ }
2256
+
2257
+ const appended = new Set();
2258
+ for (const reference of order) {
2259
+ const panel = panelsByReference.get(reference);
2260
+ if (panel instanceof HTMLElement && appended.has(panel) !== true) {
2261
+ this.appendChild(panel);
2262
+ appended.add(panel);
2263
+ }
2264
+ }
2265
+
2266
+ for (const panel of panels) {
2267
+ if (appended.has(panel) !== true) {
2268
+ this.appendChild(panel);
2269
+ }
2270
+ }
2271
+ }
2272
+
2273
+ /**
2274
+ * @private
2275
+ * @return {void}
2276
+ */
2277
+ function saveTabOrderConfig() {
2278
+ if (
2279
+ isTabOrderPersistenceEnabled.call(this) !== true ||
2280
+ this[suppressOrderConfigSaveSymbol] === true
2281
+ ) {
2282
+ return;
2283
+ }
2284
+
2285
+ const host = getHostElement.call(this);
2286
+ if (!(host && hasConfigIdentity.call(this))) {
2287
+ return;
2288
+ }
2289
+
2290
+ try {
2291
+ host.setConfig(
2292
+ getTabOrderConfigKey.call(this),
2293
+ getCurrentTabOrder.call(this),
2294
+ );
2295
+ } catch (error) {
2296
+ addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, String(error));
2297
+ }
2298
+ }
2299
+
2300
+ /**
2301
+ * @private
2302
+ * @param {string} sourceReference
2303
+ * @param {string} targetReference
2304
+ * @return {void}
2305
+ */
2306
+ function reorderTabPanels(sourceReference, targetReference) {
2307
+ const sourcePanel = findTabPanel.call(this, sourceReference, {
2308
+ availableOnly: true,
2309
+ });
2310
+ const targetPanel = findTabPanel.call(this, targetReference, {
2311
+ availableOnly: true,
2312
+ });
2313
+
2314
+ if (
2315
+ !(sourcePanel instanceof HTMLElement) ||
2316
+ !(targetPanel instanceof HTMLElement) ||
2317
+ sourcePanel === targetPanel
2318
+ ) {
2319
+ return;
2320
+ }
2321
+
2322
+ const panels = getAllTabPanels.call(this);
2323
+ const sourceIndex = panels.indexOf(sourcePanel);
2324
+ const targetIndex = panels.indexOf(targetPanel);
2325
+
2326
+ if (sourceIndex < targetIndex) {
2327
+ targetPanel.after(sourcePanel);
2328
+ } else {
2329
+ targetPanel.before(sourcePanel);
2330
+ }
2331
+
2332
+ this[dimensionsSymbol].setVia("data.calculated", false);
2333
+ initTabButtons
2334
+ .call(this)
2335
+ .then(() => {
2336
+ saveTabOrderConfig.call(this);
2337
+ fireCustomEvent(this, "monster-tab-order-changed", {
2338
+ order: getCurrentTabOrder.call(this),
2339
+ source: getTabEventDetail(sourcePanel),
2340
+ target: getTabEventDetail(targetPanel),
2341
+ });
2342
+ })
2343
+ .catch((error) => {
2344
+ addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, String(error));
2345
+ });
2346
+ }
2347
+
2348
+ /**
2349
+ * @private
2350
+ * @return {void}
2351
+ */
2352
+ function clearDraggingButton() {
2353
+ const buttons = this.shadowRoot.querySelectorAll("[data-monster-dragging]");
2354
+ for (const button of buttons) {
2355
+ button.removeAttribute("data-monster-dragging");
2356
+ }
2357
+ }
2358
+
1650
2359
  /**
1651
2360
  * @private
1652
2361
  * @return {Object}
@@ -1676,10 +2385,10 @@ function calculateNavigationButtonsDimensions() {
1676
2385
  const element = findButtonElement.call(this, ref);
1677
2386
  if (!(element instanceof HTMLButtonElement)) continue;
1678
2387
 
1679
- this[dimensionsSymbol].setVia(
1680
- `data.button.${ref}`,
1681
- calcBoxWidth.call(this, element),
1682
- );
2388
+ const width = measureButtonWidth.call(this, element);
2389
+ if (width > 0) {
2390
+ this[dimensionsSymbol].setVia(`data.button.${ref}`, width);
2391
+ }
1683
2392
  button["class"] = new TokenList(button["class"])
1684
2393
  .remove("invisible")
1685
2394
  .toString();
@@ -1692,13 +2401,81 @@ function calculateNavigationButtonsDimensions() {
1692
2401
  slot.classList.remove("invisible");
1693
2402
  }
1694
2403
 
1695
- setButtonCollections.call(this, buttons, []);
2404
+ const { standardButtons, popperButtons } =
2405
+ splitButtonsByCurrentPopperReferences.call(this, buttons);
2406
+
2407
+ resolveSwitchWidth.call(this, true);
2408
+ setButtonCollections.call(this, standardButtons, popperButtons);
1696
2409
 
1697
2410
  getWindow().requestAnimationFrame(() => {
1698
2411
  this[dimensionsSymbol].setVia("data.calculated", true);
1699
2412
  });
1700
2413
  }
1701
2414
 
2415
+ /**
2416
+ * @private
2417
+ * @param {number} space
2418
+ * @param {Object[]} standardButtons
2419
+ * @param {Object[]} popperButtons
2420
+ * @return {boolean}
2421
+ */
2422
+ function shouldSuppressOscillatingTabLayout(
2423
+ space,
2424
+ standardButtons,
2425
+ popperButtons,
2426
+ ) {
2427
+ const signature = [
2428
+ Math.round(space),
2429
+ standardButtons.map((button) => button.reference).join(","),
2430
+ popperButtons.map((button) => button.reference).join(","),
2431
+ ].join("|");
2432
+
2433
+ const history = this[layoutSignatureHistorySymbol] || [];
2434
+ history.push({
2435
+ signature,
2436
+ time: performanceNow(),
2437
+ });
2438
+ while (history.length > LAYOUT_OSCILLATION_HISTORY_LIMIT) {
2439
+ history.shift();
2440
+ }
2441
+ this[layoutSignatureHistorySymbol] = history;
2442
+
2443
+ if (history.length < LAYOUT_OSCILLATION_HISTORY_LIMIT) {
2444
+ return false;
2445
+ }
2446
+
2447
+ const [first, second, third, fourth] = history;
2448
+ if (fourth.time - first.time > LAYOUT_OSCILLATION_TIME_WINDOW_MS) {
2449
+ return false;
2450
+ }
2451
+
2452
+ return (
2453
+ first.signature !== second.signature &&
2454
+ first.signature === third.signature &&
2455
+ second.signature === fourth.signature
2456
+ );
2457
+ }
2458
+
2459
+ /**
2460
+ * @private
2461
+ * @return {void}
2462
+ */
2463
+ function resetLayoutSignatureHistory() {
2464
+ delete this[layoutSignatureHistorySymbol];
2465
+ }
2466
+
2467
+ /**
2468
+ * @private
2469
+ * @return {number}
2470
+ */
2471
+ function performanceNow() {
2472
+ const globalObject = getGlobal();
2473
+ if (globalObject?.performance?.now instanceof Function) {
2474
+ return globalObject.performance.now();
2475
+ }
2476
+ return Date.now();
2477
+ }
2478
+
1702
2479
  /**
1703
2480
  * @private
1704
2481
  * @param {string} ref
@@ -1899,6 +2676,7 @@ function getTemplate() {
1899
2676
  data-monster-attributes="
1900
2677
  class path:classes.button,
1901
2678
  data-monster-state path:buttons.state,
2679
+ draggable path:buttons.draggable | if:true,
1902
2680
  disabled path:buttons.disabled | if:true,
1903
2681
  title path:buttons.title,
1904
2682
  aria-label path:buttons.aria-label,