@schukai/monster 4.33.0 → 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 +17 -0
- package/package.json +1 -1
- package/source/components/content/viewer/message.mjs +0 -5
- package/source/components/content/viewer/style/message.pcss +0 -4
- package/source/components/content/viewer/stylesheet/message.mjs +1 -1
- package/source/components/form/select.mjs +2845 -2810
- package/source/components/layout/tabs.mjs +93 -0
- package/source/components/layout/utils/attach-tabs-hash-sync.mjs +166 -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
|
+
}
|