@schukai/monster 4.145.3 → 4.147.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
 
@@ -109,6 +114,30 @@ const changeTabEventHandler = Symbol("changeTabEventHandler");
109
114
  */
110
115
  const removeTabEventHandler = Symbol("removeTabEventHandler");
111
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
+
112
141
  /**
113
142
  * @private
114
143
  * @type {symbol}
@@ -160,6 +189,20 @@ const resizeObserverSymbol = Symbol("resizeObserver");
160
189
  */
161
190
  const activeReferenceSymbol = Symbol("activeReference");
162
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
+
163
206
  /**
164
207
  * @private
165
208
  * @type {symbol}
@@ -225,6 +268,11 @@ class Tabs extends CustomElement {
225
268
  * @property {string} features.removeBehavior Remove behavior, auto, next, previous and none
226
269
  * @property {boolean} features.openFirst Open the first tab when no active tab is set
227
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
228
276
  *
229
277
  * Tab panels can use `data-monster-name` as stable public identity, while
230
278
  * DOM ids remain the internal button reference. `data-monster-tab-available`
@@ -278,6 +326,14 @@ class Tabs extends CustomElement {
278
326
  removeBehavior: "auto",
279
327
  openFirst: true,
280
328
  deriveLabelFromContent: true,
329
+ order: {
330
+ drag: true,
331
+ persist: false,
332
+ },
333
+ },
334
+
335
+ config: {
336
+ instanceKey: null,
281
337
  },
282
338
 
283
339
  classes: {
@@ -333,11 +389,22 @@ class Tabs extends CustomElement {
333
389
  attachTabChangeObserver.call(this);
334
390
 
335
391
  // setup structure
336
- initTabButtons.call(this).then(() => {
337
- initPopperSwitch.call(this);
338
- initPopper.call(this);
339
- attachResizeObserver.call(this);
340
- });
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
+ });
341
408
  }
342
409
 
343
410
  /**
@@ -582,6 +649,19 @@ class Tabs extends CustomElement {
582
649
  this[popperInstanceSymbol]?.destroy();
583
650
  this[tabListMutationObserverSymbol]?.disconnect();
584
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
+ );
585
665
  if (this[rearrangeFrameSymbol] !== undefined) {
586
666
  getWindow().cancelAnimationFrame(this[rearrangeFrameSymbol]);
587
667
  delete this[rearrangeFrameSymbol];
@@ -1268,6 +1348,93 @@ function initEventHandler() {
1268
1348
  this[navElementSymbol].addEventListener("touch", this[changeTabEventHandler]);
1269
1349
  this[navElementSymbol].addEventListener("click", this[changeTabEventHandler]);
1270
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
+
1271
1438
  return this;
1272
1439
  }
1273
1440
 
@@ -1436,6 +1603,7 @@ function initTabButtons() {
1436
1603
  state,
1437
1604
  class: classes,
1438
1605
  disabled,
1606
+ draggable: isTabDragEnabled.call(this) === true && disabled !== true,
1439
1607
  title: disabled === true ? disabledReason : null,
1440
1608
  "aria-label": disabled === true ? disabledReason : null,
1441
1609
  remove,
@@ -1774,9 +1942,13 @@ function resolveSwitchWidth(force = false) {
1774
1942
  return 0;
1775
1943
  }
1776
1944
 
1777
- width = measureElementWidthInNavigation.call(this, this[switchElementSymbol], {
1778
- removeHiddenClass: true,
1779
- });
1945
+ width = measureElementWidthInNavigation.call(
1946
+ this,
1947
+ this[switchElementSymbol],
1948
+ {
1949
+ removeHiddenClass: true,
1950
+ },
1951
+ );
1780
1952
  this[dimensionsSymbol].setVia("data.switchWidth", width);
1781
1953
  return width;
1782
1954
  }
@@ -1888,7 +2060,10 @@ function ensureMeasurementNav() {
1888
2060
  * @param {Object[]} popperButtons
1889
2061
  */
1890
2062
  function setButtonCollections(standardButtons, popperButtons) {
1891
- const signature = getButtonCollectionsSignature(standardButtons, popperButtons);
2063
+ const signature = getButtonCollectionsSignature(
2064
+ standardButtons,
2065
+ popperButtons,
2066
+ );
1892
2067
  if (this[buttonCollectionsSignatureSymbol] === signature) {
1893
2068
  return;
1894
2069
  }
@@ -1937,6 +2112,7 @@ function getButtonSignature(button) {
1937
2112
  state: button?.state,
1938
2113
  class: button?.class,
1939
2114
  disabled: button?.disabled,
2115
+ draggable: button?.draggable,
1940
2116
  title: button?.title,
1941
2117
  "aria-label": button?.["aria-label"],
1942
2118
  remove: button?.remove,
@@ -1968,6 +2144,218 @@ function splitButtonsByCurrentPopperReferences(buttons) {
1968
2144
  return { standardButtons, popperButtons };
1969
2145
  }
1970
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
+
1971
2359
  /**
1972
2360
  * @private
1973
2361
  * @return {Object}
@@ -2288,6 +2676,7 @@ function getTemplate() {
2288
2676
  data-monster-attributes="
2289
2677
  class path:classes.button,
2290
2678
  data-monster-state path:buttons.state,
2679
+ draggable path:buttons.draggable | if:true,
2291
2680
  disabled path:buttons.disabled | if:true,
2292
2681
  title path:buttons.title,
2293
2682
  aria-label path:buttons.aria-label,
@@ -25,16 +25,26 @@
25
25
  }
26
26
 
27
27
  box-sizing: border-box;
28
- position: absolute;
29
- top: 0;
28
+ position: fixed;
29
+ top: 50%;
30
+ transform: translateY(-50%);
30
31
  display: block;
31
32
  cursor: pointer;
32
33
  width: 20px;
34
+ z-index: var(--monster-z-index-fixed);
33
35
 
34
36
  transition: top 0.2s ease, right 0.3s ease, bottom 0.3s ease, left 0.3s ease, visibility 0.3s ease, opacity 0.3s ease, width 0.3s ease;
35
37
 
38
+ &.follow-scroll {
39
+ position: absolute;
40
+ top: 0;
41
+ transform: none;
42
+ }
43
+
36
44
  & [data-monster-role="navigation-list"] {
37
45
  box-sizing: border-box;
46
+ max-height: calc(100vh - (var(--monster-space-6) * 2));
47
+ max-width: min(32rem, calc(100vw - (var(--monster-space-6) * 2)));
38
48
  overflow-y: auto;
39
49
  overscroll-behavior: contain;
40
50
  scrollbar-width: thin;