@schukai/monster 4.111.0 → 4.112.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.
@@ -0,0 +1,1399 @@
1
+ /**
2
+ * Copyright © Volker Schukai and all contributing authors, {{copyRightYear}}. All rights reserved.
3
+ * Node module: @schukai/monster
4
+ *
5
+ * This source code is licensed under the GNU Affero General Public License version 3 (AGPLv3).
6
+ * The full text of the license can be found at: https://www.gnu.org/licenses/agpl-3.0.en.html
7
+ *
8
+ * For those who do not wish to adhere to the AGPLv3, a commercial license is available.
9
+ * Acquiring a commercial license allows you to use this software without complying with the AGPLv3 terms.
10
+ * For more information about purchasing a commercial license, please contact Volker Schukai.
11
+ *
12
+ * SPDX-License-Identifier: AGPL-3.0
13
+ */
14
+
15
+ import { instanceSymbol } from "../../constants.mjs";
16
+ import { createPopper } from "@popperjs/core";
17
+ import { extend } from "../../data/extend.mjs";
18
+ import { Pathfinder } from "../../data/pathfinder.mjs";
19
+ import {
20
+ addAttributeToken,
21
+ addToObjectLink,
22
+ hasObjectLink,
23
+ } from "../../dom/attributes.mjs";
24
+ import {
25
+ ATTRIBUTE_ERRORMESSAGE,
26
+ ATTRIBUTE_PREFIX,
27
+ ATTRIBUTE_ROLE,
28
+ } from "../../dom/constants.mjs";
29
+ import {
30
+ assembleMethodSymbol,
31
+ CustomElement,
32
+ getSlottedElements,
33
+ registerCustomElement,
34
+ } from "../../dom/customelement.mjs";
35
+ import {
36
+ findTargetElementFromEvent,
37
+ fireCustomEvent,
38
+ } from "../../dom/events.mjs";
39
+ import { getDocument, getWindow } from "../../dom/util.mjs";
40
+ import { random } from "../../math/random.mjs";
41
+ import { getGlobal } from "../../types/global.mjs";
42
+ import { ID } from "../../types/id.mjs";
43
+ import { isArray, isString } from "../../types/is.mjs";
44
+ import { TokenList } from "../../types/tokenlist.mjs";
45
+ import { clone } from "../../util/clone.mjs";
46
+ import { DeadMansSwitch } from "../../util/deadmansswitch.mjs";
47
+ import { Processing } from "../../util/processing.mjs";
48
+ import {
49
+ ATTRIBUTE_BUTTON_LABEL,
50
+ ATTRIBUTE_FORM_RELOAD,
51
+ ATTRIBUTE_FORM_URL,
52
+ STYLE_DISPLAY_MODE_BLOCK,
53
+ } from "../form/constants.mjs";
54
+
55
+ import { TabsStyleSheet } from "./stylesheet/tabs.mjs";
56
+ import { VerticalTabsStyleSheet } from "./stylesheet/vertical-tabs.mjs";
57
+ import { loadAndAssignContent } from "../form/util/fetch.mjs";
58
+ import { ThemeStyleSheet } from "../stylesheet/theme.mjs";
59
+ import {
60
+ popperInstanceSymbol,
61
+ setEventListenersModifiers,
62
+ } from "../form/util/popper.mjs";
63
+ import { getLocaleOfDocument } from "../../dom/locale.mjs";
64
+
65
+ export { VerticalTabs };
66
+
67
+ /**
68
+ * @private
69
+ * @type {symbol}
70
+ */
71
+ const popperElementSymbol = Symbol("popperElement");
72
+
73
+ /**
74
+ * @private
75
+ * @type {symbol}
76
+ */
77
+ const popperNavElementSymbol = Symbol("popperNavElement");
78
+
79
+ /**
80
+ * @private
81
+ * @type {symbol}
82
+ */
83
+ const controlElementSymbol = Symbol("controlElement");
84
+
85
+ /**
86
+ * @private
87
+ * @type {symbol}
88
+ */
89
+ const navElementSymbol = Symbol("navElement");
90
+ /**
91
+ * @private
92
+ * @type {symbol}
93
+ */
94
+ const switchElementSymbol = Symbol("switchElement");
95
+
96
+ /**
97
+ * @private
98
+ * @type {symbol}
99
+ */
100
+ const changeTabEventHandler = Symbol("changeTabEventHandler");
101
+ /**
102
+ * @private
103
+ * @type {symbol}
104
+ */
105
+ const removeTabEventHandler = Symbol("removeTabEventHandler");
106
+
107
+ /**
108
+ * @private
109
+ * @type {symbol}
110
+ */
111
+ const popperSwitchEventHandler = Symbol("popperSwitchEventHandler");
112
+
113
+ /**
114
+ * local symbol
115
+ * @private
116
+ * @type {symbol}
117
+ */
118
+ const closeEventHandler = Symbol("closeEventHandler");
119
+
120
+ /**
121
+ * @private
122
+ * @type {symbol}
123
+ */
124
+ const mutationObserverSymbol = Symbol("mutationObserver");
125
+
126
+ /**
127
+ * @private
128
+ * @type {symbol}
129
+ */
130
+ const dimensionsSymbol = Symbol("dimensions");
131
+
132
+ /**
133
+ * @private
134
+ * @type {symbol}
135
+ */
136
+ const timerCallbackSymbol = Symbol("timerCallback");
137
+
138
+ /**
139
+ * local symbol
140
+ * @private
141
+ * @type {symbol}
142
+ */
143
+ const resizeObserverSymbol = Symbol("resizeObserver");
144
+
145
+ /**
146
+ * A Vertical Tabs Control
147
+ *
148
+ * @fragments /fragments/components/layout/tabs/
149
+ *
150
+ * @example /examples/components/layout/tabs-simple Simple Tabs
151
+ * @example /examples/components/layout/tabs-active Active Tabs
152
+ * @example /examples/components/layout/tabs-removable Removable Tabs
153
+ * @example /examples/components/layout/tabs-with-icon Tabs with Icon
154
+ * @example /examples/components/layout/tabs-fetch Fetch Tab Content from URL
155
+ *
156
+ * @issue https://localhost.alvine.dev:8440/development/issues/closed/268.html
157
+ * @issue https://localhost.alvine.dev:8440/development/issues/closed/271.html
158
+ * @issue https://localhost.alvine.dev:8440/development/issues/closed/273.html
159
+ *
160
+ * @since 3.74.0
161
+ * @copyright Volker Schukai
162
+ * @summary This CustomControl creates a vertical tab element with a variety of options.
163
+ */
164
+ class VerticalTabs extends CustomElement {
165
+ /**
166
+ * This method is called by the `instanceof` operator.
167
+ * @return {symbol}
168
+ */
169
+ static get [instanceSymbol]() {
170
+ return Symbol.for("@schukai/monster/components/layout/vertical-tabs");
171
+ }
172
+
173
+ /**
174
+ * To set the options via the HTML tag, the attribute `data-monster-options` must be used.
175
+ * @see {@link https://monsterjs.org/en/doc/#configurate-a-monster-control}
176
+ *
177
+ * The individual configuration values can be found in the table.
178
+ *
179
+ * @property {Object} templates Template definitions
180
+ * @property {string} templates.main Main template
181
+ * @property {Object} labels
182
+ * @property {string} labels.new-tab-label="New Tab"
183
+ * @property {Object} features
184
+ * @property {number} features.openDelay=500 Open delay in milliseconds
185
+ * @property {string} features.removeBehavior Remove behavior, auto, next, previous and none
186
+ * @property {boolean} features.openFirst Open the first tab when no active tab is set
187
+ * @property {Object} fetch Fetch [see Using Fetch mozilla.org](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch)
188
+ * @property {String} fetch.redirect=error
189
+ * @property {String} fetch.method=GET
190
+ * @property {String} fetch.mode=same-origin
191
+ * @property {String} fetch.credentials=same-origin
192
+ * @property {Object} fetch.headers={"accept":"text/html"}}
193
+ * @property {Object} popper [PopperJS Options](https://popper.js.org/docs/v2/)
194
+ * @property {string} popper.placement=bottom PopperJS placement
195
+ * @property {Object[]} modifiers={name:offset} PopperJS placement
196
+ */
197
+ get defaults() {
198
+ return Object.assign({}, super.defaults, {
199
+ templates: {
200
+ main: getTemplate(),
201
+ },
202
+ labels: getTranslations(),
203
+ buttons: {
204
+ standard: [],
205
+ popper: [],
206
+ },
207
+ fetch: {
208
+ redirect: "error",
209
+ method: "GET",
210
+ mode: "same-origin",
211
+ credentials: "same-origin",
212
+ headers: {
213
+ accept: "text/html",
214
+ },
215
+ },
216
+
217
+ features: {
218
+ openDelay: null,
219
+ removeBehavior: "auto",
220
+ openFirst: false,
221
+ },
222
+
223
+ classes: {
224
+ button: "monster-theme-primary-1",
225
+ popper: "monster-theme-primary-1",
226
+ navigation: "monster-theme-primary-1",
227
+ },
228
+
229
+ popper: {
230
+ placement: "right-start",
231
+ modifiers: [
232
+ {
233
+ name: "offset",
234
+ options: {
235
+ offset: [0, 2],
236
+ },
237
+ },
238
+
239
+ {
240
+ name: "eventListeners",
241
+ enabled: false,
242
+ },
243
+ ],
244
+ },
245
+ });
246
+ }
247
+
248
+ /**
249
+ * This method is called internal and should not be called directly.
250
+ */
251
+ [assembleMethodSymbol]() {
252
+ super[assembleMethodSymbol]();
253
+
254
+ initControlReferences.call(this);
255
+
256
+ this[dimensionsSymbol] = new Pathfinder({ data: {} });
257
+
258
+ initEventHandler.call(this);
259
+
260
+ // setup structure
261
+ initTabButtons.call(this).then(() => {
262
+ initPopperSwitch.call(this);
263
+ initPopper.call(this);
264
+ attachResizeObserver.call(this);
265
+ attachTabChangeObserver.call(this);
266
+ });
267
+ }
268
+
269
+ /**
270
+ * This method is called internal and should not be called directly.
271
+ *
272
+ * @return {CSSStyleSheet[]}
273
+ */
274
+ static getCSSStyleSheet() {
275
+ return [TabsStyleSheet, VerticalTabsStyleSheet];
276
+ }
277
+
278
+ /**
279
+ * This method is called internal and should not be called directly.
280
+ *
281
+ * @return {string}
282
+ */
283
+ static getTag() {
284
+ return "monster-vertical-tabs";
285
+ }
286
+
287
+ /**
288
+ * This method is called internal and should not be called directly.
289
+ * @param tabId
290
+ * @returns {VerticalTabs}
291
+ */
292
+ removeTab(tabId) {
293
+ const tabs = this.getTabs();
294
+ for (const tab of tabs) {
295
+ if (
296
+ (tab.getAttribute("id") === tabId ||
297
+ tab.getAttribute("data-monster-name") === tabId) &&
298
+ tab.hasAttribute("data-monster-removable")
299
+ ) {
300
+ tab.remove();
301
+ initTabButtons.call(this);
302
+ return this;
303
+ }
304
+ }
305
+ return this;
306
+ }
307
+
308
+ /**
309
+ * This method is called internal and should not be called directly.
310
+ * @returns {[]}
311
+ */
312
+ getTabs() {
313
+ const nodes = getSlottedElements.call(this);
314
+ const tabs = [];
315
+ for (const node of nodes) {
316
+ if (node instanceof HTMLElement) {
317
+ tabs.push(node);
318
+ }
319
+ }
320
+ return tabs;
321
+ }
322
+
323
+ /**
324
+ * This method is called internal and should not be called directly.
325
+ * @param content
326
+ * @param active
327
+ * @param label
328
+ * @param tabId
329
+ * @param removable
330
+ * @returns {VerticalTabs}
331
+ */
332
+ addTab(
333
+ content,
334
+ { active = false, label = null, tabId = null, removable = true } = {},
335
+ ) {
336
+ const tab = document.createElement("div");
337
+ if (!isString(label) || label.trim() === "") {
338
+ label = this.getOption("labels.new-tab-label");
339
+ }
340
+ tab.setAttribute("data-monster-button-label", label);
341
+
342
+ if (!isString(tabId) || tabId.trim() === "") {
343
+ let thisID = this.getAttribute("id");
344
+ if (!thisID) {
345
+ thisID = new ID("tab").toString();
346
+ }
347
+
348
+ tabId = new ID(thisID).toString();
349
+ }
350
+
351
+ // check if id is already used
352
+ const existingTabs = this.getTabs();
353
+ for (const existingTab of existingTabs) {
354
+ if (
355
+ existingTab.getAttribute("id") === tabId ||
356
+ existingTab.getAttribute("data-monster-name") === tabId
357
+ ) {
358
+ throw new Error(`Tab with id "${tabId}" already exists.`);
359
+ }
360
+ }
361
+
362
+ tab.setAttribute("id", tabId);
363
+
364
+ if (active === true) {
365
+ tab.classList.add("active");
366
+ }
367
+
368
+ tab.setAttribute(ATTRIBUTE_ROLE, "tab");
369
+
370
+ if (removable === true) {
371
+ tab.setAttribute("data-monster-removable", "true");
372
+ }
373
+
374
+ if (content instanceof HTMLElement) {
375
+ tab.appendChild(content);
376
+ } else if (isString(content)) {
377
+ tab.innerHTML = content;
378
+ }
379
+
380
+ this.appendChild(tab);
381
+ return this;
382
+ }
383
+
384
+ /**
385
+ * A function that activates a tab based on the provided name.
386
+ *
387
+ * The tabs have to be named with the `data-monster-name` attribute.
388
+ *
389
+ * @param {type} idOrName - the name or id of the tab to activate
390
+ * @return {VerticalTabs} - The current instance
391
+ */
392
+ activeTab(idOrName) {
393
+ let found = false;
394
+
395
+ getSlottedElements.call(this).forEach((node) => {
396
+ if (found === true) {
397
+ return;
398
+ }
399
+
400
+ if (node.getAttribute("data-monster-name") === idOrName) {
401
+ const tabControl = this.shadowRoot.querySelector(
402
+ `[data-monster-tab-reference="${node.getAttribute("id")}"]`,
403
+ );
404
+
405
+ if (tabControl) {
406
+ tabControl.click();
407
+ found = true;
408
+ }
409
+ }
410
+
411
+ if (node.getAttribute("id") === idOrName) {
412
+ const tabControl = this.shadowRoot.querySelector(
413
+ `[data-monster-tab-reference="${node.getAttribute("id")}"]`,
414
+ );
415
+
416
+ if (tabControl) {
417
+ tabControl.click();
418
+ found = true;
419
+ }
420
+ }
421
+ });
422
+
423
+ return this;
424
+ }
425
+
426
+ /**
427
+ * A function that returns the name or id of the currently active tab.
428
+ *
429
+ * The tabs have to be named with the `data-monster-name` attribute.
430
+ *
431
+ * @return {string|null}
432
+ */
433
+ getActiveTab() {
434
+ const nodes = getSlottedElements.call(this);
435
+ for (const node of nodes) {
436
+ if (node.matches(".active") === true) {
437
+ if (node.hasAttribute("data-monster-name")) {
438
+ return node.getAttribute("data-monster-name");
439
+ }
440
+
441
+ return node.getAttribute("id");
442
+ }
443
+ }
444
+ return null;
445
+ }
446
+
447
+ /**
448
+ * This method is called by the dom and should not be called directly.
449
+ *
450
+ * @return {void}
451
+ */
452
+ connectedCallback() {
453
+ super.connectedCallback();
454
+
455
+ const document = getDocument();
456
+
457
+ for (const [, type] of Object.entries(["click", "touch"])) {
458
+ // close on outside ui-events
459
+ document.addEventListener(type, this[closeEventHandler]);
460
+ }
461
+ }
462
+
463
+ /**
464
+ * This method is called by the dom and should not be called directly.
465
+ *
466
+ * @return {void}
467
+ */
468
+ disconnectedCallback() {
469
+ super.disconnectedCallback();
470
+
471
+ const document = getDocument();
472
+
473
+ // close on outside ui-events
474
+ for (const [, type] of Object.entries(["click", "touch"])) {
475
+ document.removeEventListener(type, this[closeEventHandler]);
476
+ }
477
+ }
478
+ }
479
+
480
+ /**
481
+ * @private
482
+ * @returns {object}
483
+ */
484
+ function getTranslations() {
485
+ const locale = getLocaleOfDocument();
486
+ switch (locale.language) {
487
+ case "de":
488
+ return {
489
+ "new-tab-label": "Neuer Tab",
490
+ };
491
+ case "fr":
492
+ return {
493
+ "new-tab-label": "Nouvel Onglet",
494
+ };
495
+ case "sp":
496
+ return {
497
+ "new-tab-label": "Nueva Pestaña",
498
+ };
499
+ case "it":
500
+ return {
501
+ "new-tab-label": "Nuova Scheda",
502
+ };
503
+ case "pl":
504
+ return {
505
+ "new-tab-label": "Nowa Karta",
506
+ };
507
+ case "no":
508
+ return {
509
+ "new-tab-label": "Ny Fane",
510
+ };
511
+ case "dk":
512
+ return {
513
+ "new-tab-label": "Ny Fane",
514
+ };
515
+ case "sw":
516
+ return {
517
+ "new-tab-label": "Ny Flik",
518
+ };
519
+ default:
520
+ case "en":
521
+ return {
522
+ "new-tab-label": "New Tab",
523
+ };
524
+ }
525
+ }
526
+
527
+ /**
528
+ * @private
529
+ */
530
+ function initPopperSwitch() {
531
+ const nodes = getSlottedElements.call(this, `[${ATTRIBUTE_ROLE}="switch"]`); // null ↦ only unnamed slots
532
+ let switchButton;
533
+ if (nodes.size === 0) {
534
+ switchButton = document.createElement("button");
535
+ switchButton.setAttribute(ATTRIBUTE_ROLE, "switch");
536
+ switchButton.setAttribute("part", "switch");
537
+ switchButton.classList.add("hidden");
538
+ const classList = this.getOption("classes.button");
539
+ if (classList) {
540
+ switchButton.classList.add(classList);
541
+ }
542
+ switchButton.innerHTML =
543
+ '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16"><path d="M9.5 13a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0zm0-5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0zm0-5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0z"/></svg>';
544
+ this[navElementSymbol].prepend(switchButton);
545
+ } else {
546
+ switchButton = nodes.next();
547
+ }
548
+
549
+ /**
550
+ * @param {Event} event
551
+ */
552
+ this[popperSwitchEventHandler] = (event) => {
553
+ const element = findTargetElementFromEvent(event, ATTRIBUTE_ROLE, "switch");
554
+
555
+ if (element instanceof HTMLButtonElement) {
556
+ togglePopper.call(this);
557
+ }
558
+ };
559
+
560
+ for (const type of ["click", "touch"]) {
561
+ switchButton.addEventListener(type, this[popperSwitchEventHandler]);
562
+ }
563
+
564
+ this[switchElementSymbol] = switchButton;
565
+ }
566
+
567
+ /**
568
+ * @private
569
+ */
570
+ function hidePopper() {
571
+ if (!this[popperInstanceSymbol]) {
572
+ return;
573
+ }
574
+
575
+ this[popperElementSymbol].style.display = "none";
576
+ // performance https://popper.js.org/docs/v2/tutorial/#performance
577
+ setEventListenersModifiers.call(this, false);
578
+ }
579
+
580
+ /**
581
+ * @private
582
+ */
583
+ function showPopper() {
584
+ if (this[popperElementSymbol].style.display === STYLE_DISPLAY_MODE_BLOCK) {
585
+ return;
586
+ }
587
+
588
+ this[popperElementSymbol].style.visibility = "hidden";
589
+ this[popperElementSymbol].style.display = STYLE_DISPLAY_MODE_BLOCK;
590
+ // performance https://popper.js.org/docs/v2/tutorial/#performance
591
+ setEventListenersModifiers.call(this, true);
592
+
593
+ this[popperInstanceSymbol].update();
594
+
595
+ new Processing(() => {
596
+ this[popperElementSymbol].style.removeProperty("visibility");
597
+ })
598
+ .run(undefined)
599
+ .then(() => {})
600
+ .catch((e) => {
601
+ addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, e.message);
602
+ });
603
+ }
604
+
605
+ /**
606
+ * @private
607
+ */
608
+ function togglePopper() {
609
+ if (this[popperElementSymbol].style.display === STYLE_DISPLAY_MODE_BLOCK) {
610
+ hidePopper.call(this);
611
+ } else {
612
+ showPopper.call(this);
613
+ }
614
+ }
615
+
616
+ /**
617
+ * @private
618
+ */
619
+ function attachResizeObserver() {
620
+ // against flickering
621
+ this[resizeObserverSymbol] = new ResizeObserver((entries) => {
622
+ if (this[timerCallbackSymbol] instanceof DeadMansSwitch) {
623
+ try {
624
+ this[timerCallbackSymbol].touch();
625
+ return;
626
+ } catch (e) {
627
+ delete this[timerCallbackSymbol];
628
+ }
629
+ }
630
+
631
+ this[timerCallbackSymbol] = new DeadMansSwitch(200, () => {
632
+ this[dimensionsSymbol].setVia("data.calculated", false);
633
+ checkAndRearrangeButtons.call(this);
634
+ });
635
+ });
636
+
637
+ this[resizeObserverSymbol].observe(this[navElementSymbol]);
638
+ }
639
+
640
+ /**
641
+ * @private
642
+ */
643
+ function attachTabChangeObserver() {
644
+ // against flickering
645
+ new MutationObserver((mutations) => {
646
+ let runUpdate = false;
647
+
648
+ for (const mutation of mutations) {
649
+ if (mutation.type === "childList") {
650
+ if (
651
+ mutation.addedNodes.length > 0 ||
652
+ mutation.removedNodes.length > 0
653
+ ) {
654
+ runUpdate = true;
655
+ break;
656
+ }
657
+ }
658
+ }
659
+
660
+ if (runUpdate === true) {
661
+ this[dimensionsSymbol].setVia("data.calculated", false);
662
+ initTabButtons.call(this);
663
+ }
664
+ }).observe(this, {
665
+ childList: true,
666
+ });
667
+ }
668
+
669
+ /**
670
+ * @private
671
+ * @return {Select}
672
+ * @external "external:createPopper"
673
+ */
674
+ function initPopper() {
675
+ const self = this;
676
+
677
+ const options = extend({}, self.getOption("popper"));
678
+
679
+ self[popperInstanceSymbol] = createPopper(
680
+ self[switchElementSymbol],
681
+ self[popperElementSymbol],
682
+ options,
683
+ );
684
+
685
+ const observer1 = new MutationObserver(function (mutations) {
686
+ let runUpdate = false;
687
+ for (const mutation of mutations) {
688
+ if (mutation.type === "childList") {
689
+ if (
690
+ mutation.addedNodes.length > 0 ||
691
+ mutation.removedNodes.length > 0
692
+ ) {
693
+ runUpdate = true;
694
+ break;
695
+ }
696
+ }
697
+ }
698
+
699
+ if (runUpdate === true) {
700
+ self[popperInstanceSymbol].update();
701
+ }
702
+ });
703
+
704
+ observer1.observe(self[popperNavElementSymbol], {
705
+ childList: true,
706
+ subtree: true,
707
+ });
708
+
709
+ return self;
710
+ }
711
+
712
+ /**
713
+ * @private
714
+ * @param {HTMLElement} element
715
+ */
716
+ function show(element) {
717
+ if (!this.shadowRoot) {
718
+ throw new Error("no shadow-root is defined");
719
+ }
720
+
721
+ const reference = element.getAttribute(`${ATTRIBUTE_PREFIX}tab-reference`);
722
+
723
+ const nodes = getSlottedElements.call(this);
724
+ for (const node of nodes) {
725
+ const id = node.getAttribute("id");
726
+
727
+ if (id === reference) {
728
+ node.classList.add("active");
729
+
730
+ fireCustomEvent(this, "monster-tab-change", {
731
+ reference,
732
+ });
733
+
734
+ const openDelay = Number.parseInt(
735
+ this.getOption("features.openDelay"),
736
+ 10,
737
+ );
738
+
739
+ if (!Number.isNaN(openDelay) && openDelay > 0) {
740
+ node.style.visibility = "hidden";
741
+
742
+ setTimeout(() => {
743
+ node.style.visibility = "visible";
744
+ }, openDelay);
745
+ }
746
+
747
+ // get all data- from button and filter out data-monster-attributes and data-monster-insert
748
+ const data = {};
749
+ const mask = [
750
+ "data-monster-attributes",
751
+ "data-monster-insert-reference",
752
+ "data-monster-state",
753
+ "data-monster-button-label",
754
+ "data-monster-objectlink",
755
+ "data-monster-role",
756
+ ];
757
+
758
+ for (const [, attr] of Object.entries(node.attributes)) {
759
+ if (attr.name.startsWith("data-") && mask.indexOf(attr.name) === -1) {
760
+ data[attr.name] = attr.value;
761
+ }
762
+ }
763
+
764
+ if (node.hasAttribute(ATTRIBUTE_FORM_URL)) {
765
+ const url = node.getAttribute(ATTRIBUTE_FORM_URL);
766
+
767
+ if (
768
+ !node.hasAttribute(ATTRIBUTE_FORM_RELOAD) ||
769
+ node.getAttribute(ATTRIBUTE_FORM_RELOAD).toLowerCase() === "onshow"
770
+ ) {
771
+ node.removeAttribute(ATTRIBUTE_FORM_URL);
772
+ }
773
+
774
+ const options = this.getOption("fetch", {});
775
+ const filter = undefined;
776
+ loadAndAssignContent(node, url, options, filter)
777
+ .then(() => {
778
+ fireCustomEvent(this, "monster-tab-changed", {
779
+ reference,
780
+ });
781
+ })
782
+ .catch((e) => {
783
+ addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, e.message);
784
+ });
785
+ } else {
786
+ fireCustomEvent(this, "monster-tab-changed", {
787
+ reference,
788
+ data,
789
+ });
790
+ }
791
+ } else {
792
+ node.classList.remove("active");
793
+ }
794
+ }
795
+
796
+ const standardButtons = this.getOption("buttons.standard");
797
+ for (const index in standardButtons) {
798
+ const button = standardButtons[index];
799
+ const state = button["reference"] === reference ? "active" : "inactive";
800
+ this.setOption(`buttons.standard.${index}.state`, state);
801
+ }
802
+
803
+ const popperButton = this.getOption("buttons.popper");
804
+ for (const index in popperButton) {
805
+ const button = popperButton[index];
806
+ const state = button["reference"] === reference ? "active" : "inactive";
807
+ this.setOption(`buttons.popper.${index}.state`, state);
808
+ }
809
+
810
+ hidePopper.call(this);
811
+ }
812
+
813
+ /**
814
+ * @private
815
+ */
816
+ function initEventHandler() {
817
+ const self = this;
818
+
819
+ if (!this.shadowRoot) {
820
+ throw new Error("no shadow-root is defined");
821
+ }
822
+
823
+ /**
824
+ * @param {Event} event
825
+ * @fires monster-tab-remove
826
+ */
827
+ this[removeTabEventHandler] = (event) => {
828
+ const element = findTargetElementFromEvent(
829
+ event,
830
+ ATTRIBUTE_ROLE,
831
+ "remove-tab",
832
+ );
833
+
834
+ if (element instanceof HTMLElement) {
835
+ const button = findTargetElementFromEvent(
836
+ event,
837
+ ATTRIBUTE_ROLE,
838
+ "button",
839
+ );
840
+
841
+ if (button instanceof HTMLButtonElement && button.disabled !== true) {
842
+ const reference = button.getAttribute(
843
+ `${ATTRIBUTE_PREFIX}tab-reference`,
844
+ );
845
+
846
+ let doChange = false;
847
+ let nextName = null;
848
+ let previousName = null;
849
+ let targetReference = null;
850
+ let activeReference = null;
851
+ let restoreOpenFirst = null;
852
+
853
+ const btn = this.getOption("buttons");
854
+ if (btn?.standard) {
855
+ const activeButton = btn.standard.find(
856
+ (item) => item.state === "active",
857
+ );
858
+ activeReference = activeButton?.reference || null;
859
+ }
860
+
861
+ const nodes = getSlottedElements.call(this);
862
+ const orderedTabs = [];
863
+ for (const node of nodes) {
864
+ if (node instanceof HTMLElement) {
865
+ orderedTabs.push(node);
866
+ }
867
+ }
868
+
869
+ for (let i = 0; i < orderedTabs.length; i++) {
870
+ const node = orderedTabs[i];
871
+ if (node.getAttribute("id") === reference) {
872
+ if (reference === activeReference) {
873
+ doChange = true;
874
+ nextName = orderedTabs[i + 1]?.getAttribute?.("id") || null;
875
+ previousName = orderedTabs[i - 1]?.getAttribute?.("id") || null;
876
+ }
877
+ break;
878
+ }
879
+ }
880
+
881
+ if (reference) {
882
+ const escapedReference =
883
+ typeof CSS !== "undefined" && CSS.escape
884
+ ? CSS.escape(reference)
885
+ : reference;
886
+ const container = this.querySelector(`[id=${escapedReference}]`);
887
+ if (container instanceof HTMLElement) {
888
+ if (doChange) {
889
+ switch (this.getOption("features.removeBehavior")) {
890
+ case "auto":
891
+ if (nextName !== null) {
892
+ targetReference = nextName;
893
+ } else {
894
+ if (previousName !== null) {
895
+ targetReference = previousName;
896
+ }
897
+ }
898
+ break;
899
+ case "next":
900
+ if (nextName !== null) {
901
+ targetReference = nextName;
902
+ }
903
+ break;
904
+ case "previous":
905
+ if (previousName !== null) {
906
+ targetReference = previousName;
907
+ }
908
+ break;
909
+
910
+ default: // and "none"
911
+ break;
912
+ }
913
+ if (targetReference) {
914
+ self.activeTab(targetReference);
915
+ restoreOpenFirst = this.getOption("features.openFirst");
916
+ this.setOption("features.openFirst", false);
917
+ }
918
+ }
919
+
920
+ container.remove();
921
+ initTabButtons.call(this);
922
+ if (targetReference) {
923
+ if (restoreOpenFirst !== null) {
924
+ this.setOption("features.openFirst", restoreOpenFirst);
925
+ }
926
+ self.activeTab(targetReference);
927
+ }
928
+ fireCustomEvent(this, "monster-tab-remove", {
929
+ reference,
930
+ });
931
+ }
932
+ }
933
+ }
934
+ }
935
+ };
936
+
937
+ /**
938
+ * @param {Event} event
939
+ */
940
+ this[changeTabEventHandler] = (event) => {
941
+ const element = findTargetElementFromEvent(event, ATTRIBUTE_ROLE, "button");
942
+
943
+ if (element instanceof HTMLButtonElement && element.disabled !== true) {
944
+ show.call(this, element);
945
+ }
946
+ };
947
+
948
+ /**
949
+ * @param {Event} event
950
+ */
951
+ this[closeEventHandler] = (event) => {
952
+ const path = event.composedPath();
953
+
954
+ for (const [, element] of Object.entries(path)) {
955
+ if (element === this) {
956
+ return;
957
+ }
958
+ }
959
+
960
+ hidePopper.call(this);
961
+ };
962
+
963
+ // the order is important, because the remove must be before the change
964
+ this[navElementSymbol].addEventListener("touch", this[removeTabEventHandler]);
965
+ this[navElementSymbol].addEventListener("click", this[removeTabEventHandler]);
966
+
967
+ this[navElementSymbol].addEventListener("touch", this[changeTabEventHandler]);
968
+ this[navElementSymbol].addEventListener("click", this[changeTabEventHandler]);
969
+
970
+ return this;
971
+ }
972
+
973
+ /**
974
+ * @private
975
+ * @param observedNode
976
+ */
977
+ function attachTabMutationObserver(observedNode) {
978
+ const self = this;
979
+
980
+ if (hasObjectLink(observedNode, mutationObserverSymbol)) {
981
+ return;
982
+ }
983
+
984
+ /**
985
+ * this construct monitors a node whether it is disabled or modified
986
+ * @type {MutationObserver}
987
+ */
988
+ const observer = new MutationObserver(function (mutations) {
989
+ if (isArray(mutations)) {
990
+ const mutation = mutations.pop();
991
+ if (mutation instanceof MutationRecord) {
992
+ initTabButtons.call(self);
993
+ }
994
+ }
995
+ });
996
+
997
+ observer.observe(observedNode, {
998
+ childList: false,
999
+ attributes: true,
1000
+ subtree: false,
1001
+ attributeFilter: [
1002
+ "disabled",
1003
+ ATTRIBUTE_BUTTON_LABEL,
1004
+ `${ATTRIBUTE_PREFIX}button-icon`,
1005
+ ],
1006
+ });
1007
+
1008
+ addToObjectLink(observedNode, mutationObserverSymbol, observer);
1009
+ }
1010
+
1011
+ /**
1012
+ * @private
1013
+ * @return {Select}
1014
+ * @throws {Error} no shadow-root is defined
1015
+ */
1016
+ function initControlReferences() {
1017
+ if (!this.shadowRoot) {
1018
+ throw new Error("no shadow-root is defined");
1019
+ }
1020
+
1021
+ this[controlElementSymbol] = this.shadowRoot.querySelector(
1022
+ `[${ATTRIBUTE_ROLE}=control]`,
1023
+ );
1024
+ this[navElementSymbol] = this.shadowRoot.querySelector(
1025
+ `nav[${ATTRIBUTE_ROLE}=nav]`,
1026
+ );
1027
+ this[popperElementSymbol] = this.shadowRoot.querySelector(
1028
+ `[${ATTRIBUTE_ROLE}=popper]`,
1029
+ );
1030
+ this[popperNavElementSymbol] = this.shadowRoot.querySelector(
1031
+ `[${ATTRIBUTE_ROLE}=popper-nav]`,
1032
+ );
1033
+ }
1034
+
1035
+ /**
1036
+ * @private
1037
+ * @return {Promise<unknown>}
1038
+ * @throws {Error} no shadow-root is defined
1039
+ *
1040
+ */
1041
+ function initTabButtons() {
1042
+ if (!this.shadowRoot) {
1043
+ throw new Error("no shadow-root is defined");
1044
+ }
1045
+
1046
+ let activeReference;
1047
+
1048
+ const dimensionsCalculated = this[dimensionsSymbol].getVia(
1049
+ "data.calculated",
1050
+ false,
1051
+ );
1052
+
1053
+ const buttons = [];
1054
+ const nodes = getSlottedElements.call(this, undefined, null); // null ↦ only unnamed slots
1055
+
1056
+ for (const node of nodes) {
1057
+ if (!(node instanceof HTMLElement)) continue;
1058
+ let label = getButtonLabel.call(this, node);
1059
+
1060
+ let reference;
1061
+ if (node.hasAttribute("id")) {
1062
+ reference = node.getAttribute("id");
1063
+ }
1064
+
1065
+ let disabled;
1066
+ if (node.hasAttribute("disabled") || node.disabled === true) {
1067
+ disabled = true;
1068
+ }
1069
+
1070
+ if (!reference) {
1071
+ reference = new ID("tab").toString();
1072
+ node.setAttribute("id", reference);
1073
+ }
1074
+
1075
+ if (node.hasAttribute(`${ATTRIBUTE_PREFIX}button-icon`)) {
1076
+ label = `<span part="label">${label}</span><img part="icon" alt="this is an icon" src="${node.getAttribute(
1077
+ `${ATTRIBUTE_PREFIX}button-icon`,
1078
+ )}">`;
1079
+ }
1080
+
1081
+ let remove = false;
1082
+ if (node.hasAttribute(`${ATTRIBUTE_PREFIX}removable`)) {
1083
+ remove = true;
1084
+ }
1085
+
1086
+ if (node.matches(".active") === true && disabled !== true) {
1087
+ activeReference = reference;
1088
+ }
1089
+
1090
+ const state = "";
1091
+ const classes = dimensionsCalculated ? "" : "invisible";
1092
+
1093
+ buttons.push({
1094
+ reference,
1095
+ label,
1096
+ state,
1097
+ class: classes,
1098
+ disabled,
1099
+ remove,
1100
+ });
1101
+
1102
+ attachTabMutationObserver.call(this, node);
1103
+ }
1104
+
1105
+ this.setOption("buttons.standard", clone(buttons));
1106
+ this.setOption("buttons.popper", []);
1107
+ this.setOption("marker", random());
1108
+
1109
+ return adjustButtonVisibility.call(this).then(() => {
1110
+ if (!activeReference && this.getOption("features.openFirst") === true) {
1111
+ const firstButton = this.getOption("buttons.standard").find(
1112
+ (button) => button.disabled !== true,
1113
+ );
1114
+ if (firstButton) {
1115
+ activeReference = firstButton.reference;
1116
+ }
1117
+ }
1118
+
1119
+ if (activeReference) {
1120
+ return new Processing(() => {
1121
+ const button = this.shadowRoot.querySelector(
1122
+ `[${ATTRIBUTE_PREFIX}tab-reference="${activeReference}"]`,
1123
+ );
1124
+ if (button instanceof HTMLButtonElement && button.disabled !== true) {
1125
+ show.call(this, button);
1126
+ }
1127
+ })
1128
+ .run(undefined)
1129
+ .then(() => {})
1130
+ .catch((e) => {
1131
+ addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, e.message);
1132
+ });
1133
+ }
1134
+
1135
+ return Promise.resolve();
1136
+ });
1137
+ }
1138
+
1139
+ function checkAndRearrangeButtons() {
1140
+ if (this[dimensionsSymbol].getVia("data.calculated", false) !== true) {
1141
+ calculateNavigationButtonsDimensions.call(this);
1142
+ }
1143
+
1144
+ rearrangeButtons.call(this);
1145
+ }
1146
+
1147
+ /**
1148
+ * @private
1149
+ * @return {Promise<unknown>}
1150
+ */
1151
+ function adjustButtonVisibility() {
1152
+ const self = this;
1153
+
1154
+ return new Promise((resolve) => {
1155
+ const observer = new MutationObserver(function (mutations) {
1156
+ const defCount = self.getOption("buttons.standard").length;
1157
+ const domCount = self[navElementSymbol].querySelectorAll(
1158
+ 'button[data-monster-role="button"]',
1159
+ ).length;
1160
+
1161
+ // in drawing
1162
+ if (defCount !== domCount) return;
1163
+
1164
+ observer.disconnect();
1165
+
1166
+ checkAndRearrangeButtons.call(self);
1167
+
1168
+ resolve();
1169
+ });
1170
+
1171
+ observer.observe(self[navElementSymbol], {
1172
+ attributes: true,
1173
+ });
1174
+ });
1175
+ }
1176
+
1177
+ /**
1178
+ * @private
1179
+ * @param {string} value
1180
+ * @return {number}
1181
+ */
1182
+ function getDimValue(value) {
1183
+ if ([undefined, null].indexOf(value) !== -1) {
1184
+ return 0;
1185
+ }
1186
+
1187
+ const valueAsInt = parseInt(value, 10);
1188
+
1189
+ if (isNaN(valueAsInt)) {
1190
+ return 0;
1191
+ }
1192
+
1193
+ return valueAsInt;
1194
+ }
1195
+
1196
+ /**
1197
+ * @private
1198
+ * @param {HTMLElement} node
1199
+ * @return {number}
1200
+ */
1201
+ function calcBoxHeight(node) {
1202
+ const dim = getGlobal("window").getComputedStyle(node);
1203
+ const bounding = node.getBoundingClientRect();
1204
+
1205
+ return (
1206
+ getDimValue(dim["border-top-width"]) +
1207
+ getDimValue(dim["padding-top"]) +
1208
+ getDimValue(dim["margin-top"]) +
1209
+ getDimValue(bounding["height"]) +
1210
+ getDimValue(dim["border-bottom-width"]) +
1211
+ getDimValue(dim["margin-bottom"]) +
1212
+ getDimValue(dim["padding-bottom"])
1213
+ );
1214
+ }
1215
+
1216
+ /**
1217
+ * @private
1218
+ * @return {Object}
1219
+ */
1220
+ function rearrangeButtons() {
1221
+ getWindow().requestAnimationFrame(() => {
1222
+ const standardButtons = [];
1223
+ const popperButtons = [];
1224
+ let sum = 0;
1225
+ const space = this[dimensionsSymbol].getVia("data.space");
1226
+
1227
+ if (space <= 0) {
1228
+ return;
1229
+ }
1230
+
1231
+ const buttons = this.getOption("buttons.standard");
1232
+ for (const [, button] of buttons.entries()) {
1233
+ const ref = button?.reference;
1234
+
1235
+ sum += this[dimensionsSymbol].getVia(`data.button.${ref}`);
1236
+
1237
+ if (sum > space) {
1238
+ popperButtons.push(clone(button));
1239
+ } else {
1240
+ standardButtons.push(clone(button));
1241
+ }
1242
+ }
1243
+
1244
+ this.setOption("buttons.standard", standardButtons);
1245
+ this.setOption("buttons.popper", popperButtons);
1246
+
1247
+ if (this[switchElementSymbol]) {
1248
+ if (popperButtons.length > 0) {
1249
+ this[switchElementSymbol].classList.remove("hidden");
1250
+ } else {
1251
+ this[switchElementSymbol].classList.add("hidden");
1252
+ }
1253
+ }
1254
+ });
1255
+ }
1256
+
1257
+ /**
1258
+ * @private
1259
+ * @return {Object}
1260
+ */
1261
+ function calculateNavigationButtonsDimensions() {
1262
+ const height = this[navElementSymbol].getBoundingClientRect().height;
1263
+
1264
+ let startEndHeight = 0;
1265
+
1266
+ getSlottedElements.call(this, undefined, "start").forEach((node) => {
1267
+ startEndHeight += calcBoxHeight.call(this, node);
1268
+ });
1269
+
1270
+ getSlottedElements.call(this, undefined, "end").forEach((node) => {
1271
+ startEndHeight += calcBoxHeight.call(this, node);
1272
+ });
1273
+
1274
+ this[dimensionsSymbol].setVia("data.space", height - startEndHeight - 2);
1275
+ this[dimensionsSymbol].setVia("data.visible", !(height === 0));
1276
+
1277
+ const buttons = this.getOption("buttons.standard").concat(
1278
+ this.getOption("buttons.popper"),
1279
+ );
1280
+
1281
+ for (const [i, button] of buttons.entries()) {
1282
+ const ref = button?.reference;
1283
+ const element = this[navElementSymbol].querySelector(
1284
+ `:scope > [${ATTRIBUTE_PREFIX}tab-reference="${ref}"]`,
1285
+ );
1286
+ if (!(element instanceof HTMLButtonElement)) continue;
1287
+
1288
+ this[dimensionsSymbol].setVia(
1289
+ `data.button.${ref}`,
1290
+ calcBoxHeight.call(this, element),
1291
+ );
1292
+ button["class"] = new TokenList(button["class"])
1293
+ .remove("invisible")
1294
+ .toString();
1295
+ }
1296
+
1297
+ const slots = this[controlElementSymbol].querySelectorAll(
1298
+ `nav[${ATTRIBUTE_PREFIX}role=nav] > slot.invisible, slot[${ATTRIBUTE_PREFIX}role=slot].invisible`,
1299
+ );
1300
+ for (const [, slot] of slots.entries()) {
1301
+ slot.classList.remove("invisible");
1302
+ }
1303
+
1304
+ this.setOption("buttons.standard", clone(buttons));
1305
+
1306
+ getWindow().requestAnimationFrame(() => {
1307
+ this[dimensionsSymbol].setVia("data.calculated", true);
1308
+ });
1309
+ }
1310
+
1311
+ /**
1312
+ * @private
1313
+ * @param {HTMLElement} node
1314
+ * @return {string}
1315
+ */
1316
+ function getButtonLabel(node) {
1317
+ let label;
1318
+ let setLabel = false;
1319
+ if (node.hasAttribute(ATTRIBUTE_BUTTON_LABEL)) {
1320
+ label = node.getAttribute(ATTRIBUTE_BUTTON_LABEL);
1321
+ } else {
1322
+ label = node.innerText;
1323
+ setLabel = true;
1324
+ }
1325
+
1326
+ if (!isString(label)) {
1327
+ label = "";
1328
+ }
1329
+
1330
+ if (setLabel === true) {
1331
+ if (label.trim() === "") {
1332
+ label = node.textContent || "";
1333
+ }
1334
+ label = label.replace(/\s+/g, " ").trim();
1335
+ const words = label.split(" ").filter((word) => word !== "");
1336
+ if (words.length > 3) {
1337
+ label = `${words.slice(0, 3).join(" ")}…`;
1338
+ }
1339
+ }
1340
+ label = label.trim();
1341
+
1342
+ if (label === "") {
1343
+ label = this.getOption("labels.new-tab-label", "New Tab");
1344
+ }
1345
+
1346
+ if (label.length > 100) {
1347
+ label = `${label.substring(0, 99)}…`;
1348
+ }
1349
+
1350
+ if (setLabel === true) {
1351
+ node.setAttribute(ATTRIBUTE_BUTTON_LABEL, label);
1352
+ }
1353
+
1354
+ return label;
1355
+ }
1356
+
1357
+ /**
1358
+ * @private
1359
+ * @return {string}
1360
+ */
1361
+ function getTemplate() {
1362
+ // language=HTML
1363
+ return `
1364
+ <template id="buttons">
1365
+ <button part="button"
1366
+ tabindex="0"
1367
+ data-monster-role="button"
1368
+ data-monster-attributes="
1369
+ class path:classes.button,
1370
+ data-monster-state path:buttons.state,
1371
+ disabled path:buttons.disabled | if:true,
1372
+ data-monster-tab-reference path:buttons.reference"><span
1373
+ data-monster-replace="path:buttons.label"></span><span part="remove-tab"
1374
+ data-monster-attributes="class path:buttons.remove | ?:remove-tab:hidden "
1375
+ data-monster-role="remove-tab"
1376
+ tabindex="-1"></span></button>
1377
+ </template>
1378
+ <div data-monster-role="control" part="control">
1379
+ <nav data-monster-role="nav" part="nav"
1380
+ data-monster-attributes="data-monster-marker path:marker, class path:classes.navigation"
1381
+ data-monster-insert="buttons path:buttons.standard">
1382
+ <slot name="start" class="invisible"></slot>
1383
+ <div data-monster-role="popper" part="popper" tabindex="-1"
1384
+ data-monster-attributes="class path:classes.popper">
1385
+ <div data-popper-arrow></div>
1386
+
1387
+
1388
+ <div part="popper-nav" data-monster-role="popper-nav"
1389
+ data-monster-insert="buttons path:buttons.popper"
1390
+ tabindex="-1"></div>
1391
+ </div>
1392
+ <slot name="popper" class="invisible"></slot>
1393
+ <slot name="end" class="invisible"></slot>
1394
+ </nav>
1395
+ <slot data-monster-role="slot" class="invisible"></slot>
1396
+ </div>`;
1397
+ }
1398
+
1399
+ registerCustomElement(VerticalTabs);