@schukai/monster 4.33.1 → 4.34.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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,14 @@
2
2
 
3
3
 
4
4
 
5
+ ## [4.34.0] - 2025-07-13
6
+
7
+ ### Add Features
8
+
9
+ - Add dynamic tab management and URL hash synchronization
10
+
11
+
12
+
5
13
  ## [4.33.1] - 2025-07-12
6
14
 
7
15
  ### Bug Fixes
package/package.json CHANGED
@@ -1 +1 @@
1
- {"author":"schukai GmbH","dependencies":{"@floating-ui/dom":"^1.7.2","@popperjs/core":"^2.11.8"},"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.33.1"}
1
+ {"author":"schukai GmbH","dependencies":{"@floating-ui/dom":"^1.7.2","@popperjs/core":"^2.11.8"},"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.34.0"}
@@ -283,6 +283,99 @@ class Tabs extends CustomElement {
283
283
  return "monster-tabs";
284
284
  }
285
285
 
286
+ /**
287
+ * This method is called internal and should not be called directly.
288
+ * @param tabId
289
+ * @returns {Tabs}
290
+ */
291
+ removeTab(tabId) {
292
+ const tabs = this.getTabs();
293
+ for (const tab of tabs) {
294
+ if ((tab.getAttribute("id") === tabId || tab.getAttribute("data-monster-name") === tabId) && tab.hasAttribute("data-monster-removable")) {
295
+ tab.remove();
296
+ initTabButtons.call(this);
297
+ return this;
298
+ }
299
+ }
300
+ return this;
301
+ }
302
+
303
+ /**
304
+ * This method is called internal and should not be called directly.
305
+ * @returns {[]}
306
+ */
307
+ getTabs() {
308
+ const nodes = getSlottedElements.call(this);
309
+ const tabs = [];
310
+ for (const node of nodes) {
311
+ if (node instanceof HTMLElement) {
312
+ tabs.push(node);
313
+ }
314
+ }
315
+ return tabs;
316
+ }
317
+
318
+ /**
319
+ * This method is called internal and should not be called directly.
320
+ * @param content
321
+ * @param active
322
+ * @param label
323
+ * @param tabId
324
+ * @param removable
325
+ * @returns {Tabs}
326
+ */
327
+ addTab(content, {
328
+ active = false,
329
+ label = null,
330
+ tabId = null,
331
+ removable = true
332
+ } = {} ) {
333
+ const tab = document.createElement("div");
334
+ if (!isString(label) || label.trim() === "") {
335
+ label = this.getOption("labels.new-tab-label");
336
+ }
337
+ tab.setAttribute("data-monster-button-label" ,label);
338
+
339
+ if (!isString(tabId) || tabId.trim() === "") {
340
+ let thisID = this.getAttribute("id");
341
+ if (!thisID) {
342
+ thisID = new ID("tab").toString();
343
+ }
344
+
345
+ tabId = new ID(thisID).toString();
346
+ }
347
+
348
+ // check if id is already used
349
+ const existingTabs = this.getTabs();
350
+ for (const existingTab of existingTabs) {
351
+ if (existingTab.getAttribute("id") === tabId || existingTab.getAttribute("data-monster-name") === tabId) {
352
+ throw new Error(`Tab with id "${tabId}" already exists.`);
353
+ }
354
+ }
355
+
356
+
357
+ tab.setAttribute("id", tabId);
358
+
359
+ if (active === true) {
360
+ tab.classList.add("active");
361
+ }
362
+
363
+ tab.setAttribute(ATTRIBUTE_ROLE, "tab");
364
+
365
+ if(removable === true) {
366
+ tab.setAttribute("data-monster-removable", "true");
367
+ }
368
+
369
+ if(content instanceof HTMLElement) {
370
+ tab.appendChild(content);
371
+ } else if (isString(content)) {
372
+ tab.innerHTML = content;
373
+ }
374
+
375
+ this.appendChild(tab);
376
+ return this
377
+ }
378
+
286
379
  /**
287
380
  * A function that activates a tab based on the provided name.
288
381
  *
@@ -0,0 +1,166 @@
1
+ import {
2
+ parseBracketedKeyValueHash,
3
+ createBracketedKeyValueHash,
4
+ } from "../../../text/bracketed-key-value-hash.mjs";
5
+
6
+ /**
7
+ * Synchronizes a <monster-tabs> instance with the URL hash,
8
+ * including active tab and all existing tab IDs.
9
+ *
10
+ * @param {HTMLElement} tabsEl - The monster-tabs element
11
+ * @param {string} selector - Hash selector name (e.g. "tabs", "tabs2")
12
+ * @param {string} activeKey - Key for the active tab (default: "active")
13
+ * @param {string} allTabsKey - Key for all tab IDs (default: "all")
14
+ */
15
+ export function attachTabsHashSync(
16
+ tabsEl,
17
+ selector = "tabs",
18
+ activeKey = "active",
19
+ allTabsKey = "all",
20
+ ) {
21
+ if (!(tabsEl instanceof HTMLElement)) {
22
+ throw new TypeError("Expected a monster-tabs HTMLElement");
23
+ }
24
+
25
+ let lastKnownActiveTabId = null;
26
+ let lastKnownAllTabIds = [];
27
+
28
+ /**
29
+ * Reads active and all tab IDs from the URL hash.
30
+ * @returns {{activeTabId: string|null, allTabIds: string[]}}
31
+ */
32
+ function getTabStateFromHash() {
33
+ const hashObj = parseBracketedKeyValueHash(location.hash);
34
+ const tabsData = hashObj?.[selector] ?? {};
35
+ const activeTabId = tabsData[activeKey] ?? null;
36
+ const allTabIdsString = tabsData[allTabsKey] ?? "";
37
+ const allTabIds = allTabIdsString
38
+ .split(",")
39
+ .filter((id) => id.trim() !== "");
40
+ return { activeTabId, allTabIds };
41
+ }
42
+
43
+ /**
44
+ * Synchronizes tab state from hash on page load and hash changes.
45
+ */
46
+ function syncFromHash() {
47
+ const { activeTabId, allTabIds } = getTabStateFromHash();
48
+
49
+ // Sync active tab
50
+ if (activeTabId && activeTabId !== lastKnownActiveTabId) {
51
+ tabsEl.activeTab(activeTabId);
52
+ lastKnownActiveTabId = activeTabId;
53
+ }
54
+
55
+ // Sync all tabs (add/remove tabs based on hash)
56
+ const currentTabs = tabsEl.getTabs().map((tab) => tab.getAttribute("id"));
57
+
58
+ // Add tabs that are in hash but not in DOM
59
+ for (const tabId of allTabIds) {
60
+ if (!currentTabs.includes(tabId)) {
61
+ // You'll need a way to get the label and content for new tabs
62
+ // This is a placeholder. You might fetch it or have a default.
63
+ // For existing tabs in the HTML, we're just ensuring they are present.
64
+ // For truly new tabs from the hash, you'd need their content/label.
65
+ // For this example, we'll assume the initial HTML already has all potential tabs,
66
+ // and we're just making sure they are displayed if their IDs are in the hash.
67
+ // If you truly want to create new tabs from scratch based on the hash,
68
+ // you'd need more information in the hash or a lookup mechanism.
69
+ console.warn(
70
+ `Tab with ID '${tabId}' found in hash but not in DOM. Add logic to create it if necessary.`,
71
+ );
72
+ }
73
+ }
74
+
75
+ // Remove tabs that are in DOM but not in hash
76
+ for (const tabId of currentTabs) {
77
+ if (!allTabIds.includes(tabId)) {
78
+ tabsEl.removeTab(tabId);
79
+ }
80
+ }
81
+
82
+ lastKnownAllTabIds = allTabIds;
83
+ }
84
+
85
+ window.addEventListener("hashchange", syncFromHash);
86
+ syncFromHash(); // initial load
87
+
88
+ /**
89
+ * Writes the current active tab and all tab IDs to the URL hash.
90
+ * @param {string|null} activeTabId - The ID of the currently active tab.
91
+ * @param {string[]} allTabIds - An array of all current tab IDs.
92
+ */
93
+ function writeHash(activeTabId, allTabIds) {
94
+ const hashObj = parseBracketedKeyValueHash(location.hash);
95
+ hashObj[selector] = { ...(hashObj[selector] ?? {}) };
96
+
97
+ if (activeTabId) {
98
+ hashObj[selector][activeKey] = activeTabId;
99
+ } else {
100
+ delete hashObj[selector][activeKey];
101
+ }
102
+
103
+ if (allTabIds.length > 0) {
104
+ hashObj[selector][allTabsKey] = allTabIds.join(",");
105
+ } else {
106
+ delete hashObj[selector][allTabsKey];
107
+ }
108
+
109
+ const newHash = createBracketedKeyValueHash(hashObj);
110
+ if (location.hash !== newHash) {
111
+ history.replaceState(null, "", newHash);
112
+ }
113
+ lastKnownActiveTabId = activeTabId;
114
+ lastKnownAllTabIds = allTabIds;
115
+ }
116
+
117
+ // Listen for tab changes (active tab)
118
+ tabsEl.addEventListener("monster-tab-changed", (e) => {
119
+ if (e.target !== tabsEl) return; // Ignore bubbled events
120
+ const newActiveTabId = e.detail?.reference;
121
+ const currentTabs = tabsEl.getTabs().map((tab) => tab.getAttribute("id"));
122
+ writeHash(newActiveTabId, currentTabs);
123
+ });
124
+
125
+ // Listen for tab additions
126
+ const observer = new MutationObserver((mutationsList) => {
127
+ let tabsChanged = false;
128
+ for (const mutation of mutationsList) {
129
+ if (
130
+ mutation.type === "childList" &&
131
+ (mutation.addedNodes.length > 0 || mutation.removedNodes.length > 0)
132
+ ) {
133
+ // Filter for actual tab elements
134
+ const hasTabNodes = Array.from(mutation.addedNodes).some(
135
+ (node) => node.nodeType === 1 && node.hasAttribute("data-monster-button-label")
136
+ ) || Array.from(mutation.removedNodes).some(
137
+ (node) => node.nodeType === 1 && node.hasAttribute("data-monster-button-label")
138
+ );
139
+
140
+ if (hasTabNodes) {
141
+ tabsChanged = true;
142
+ break;
143
+ }
144
+ }
145
+ }
146
+ if (tabsChanged) {
147
+ const currentActiveTabId = tabsEl.getActiveTab();
148
+ const currentTabs = tabsEl.getTabs().map((tab) => tab.getAttribute("id"));
149
+ // Only update hash if the list of tabs has actually changed
150
+ if (
151
+ currentTabs.length !== lastKnownAllTabIds.length ||
152
+ !currentTabs.every((id) => lastKnownAllTabIds.includes(id))
153
+ ) {
154
+ writeHash(currentActiveTabId, currentTabs);
155
+ }
156
+ }
157
+ });
158
+
159
+ // Observe the tabsEl for direct child additions/removals
160
+ observer.observe(tabsEl, { childList: true });
161
+
162
+ // Initial write of all existing tabs to the hash
163
+ const initialActiveTab = tabsEl.getActiveTab();
164
+ const initialTabs = tabsEl.getTabs().map((tab) => tab.getAttribute("id"));
165
+ writeHash(initialActiveTab, initialTabs);
166
+ }