@schukai/monster 4.33.1 → 4.34.1
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 +16 -0
- package/package.json +1 -1
- package/source/components/content/viewer/message.mjs +6 -3
- package/source/components/content/viewer/style/html.pcss +2 -2
- package/source/components/content/viewer/stylesheet/html.mjs +1 -1
- package/source/components/layout/tabs.mjs +93 -0
- package/source/components/layout/utils/attach-tabs-hash-sync.mjs +166 -0
package/CHANGELOG.md
CHANGED
@@ -2,6 +2,22 @@
|
|
2
2
|
|
3
3
|
|
4
4
|
|
5
|
+
## [4.34.1] - 2025-07-14
|
6
|
+
|
7
|
+
### Bug Fixes
|
8
|
+
|
9
|
+
- Update node and pnpx binaries; Adjust image privacy handling
|
10
|
+
|
11
|
+
|
12
|
+
|
13
|
+
## [4.34.0] - 2025-07-13
|
14
|
+
|
15
|
+
### Add Features
|
16
|
+
|
17
|
+
- Add dynamic tab management and URL hash synchronization
|
18
|
+
|
19
|
+
|
20
|
+
|
5
21
|
## [4.33.1] - 2025-07-12
|
6
22
|
|
7
23
|
### 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.
|
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.1"}
|
@@ -121,7 +121,7 @@ class MessageContent extends CustomElement {
|
|
121
121
|
},
|
122
122
|
|
123
123
|
privacy: {
|
124
|
-
visible:
|
124
|
+
visible: false,
|
125
125
|
},
|
126
126
|
|
127
127
|
content: "",
|
@@ -364,7 +364,6 @@ class MessageContent extends CustomElement {
|
|
364
364
|
|
365
365
|
this[contentContainerElementSymbol].setOption("content", htmlContent);
|
366
366
|
this.setOption("message.attachments", attachments);
|
367
|
-
|
368
367
|
this.setOption("message.parts", message?.parts || []);
|
369
368
|
|
370
369
|
return this;
|
@@ -454,6 +453,7 @@ function replaceCidImages(htmlContent, replacements) {
|
|
454
453
|
const parser = new DOMParser();
|
455
454
|
const doc = parser.parseFromString(htmlContent, "text/html");
|
456
455
|
const images = doc.querySelectorAll("img");
|
456
|
+
let hasPrivacyImage = false;
|
457
457
|
images.forEach((img) => {
|
458
458
|
const src = img.getAttribute("src");
|
459
459
|
if (src && src.toLowerCase().startsWith("cid:")) {
|
@@ -469,7 +469,7 @@ function replaceCidImages(htmlContent, replacements) {
|
|
469
469
|
img.classList.add("privacyImage");
|
470
470
|
img.setAttribute("data-monster-privacy", "true");
|
471
471
|
img.setAttribute("data-monster-privacy_origin-url", src);
|
472
|
-
|
472
|
+
hasPrivacyImage = true;
|
473
473
|
// If the src is an HTTP URL, we can keep it as is
|
474
474
|
img.setAttribute(
|
475
475
|
"title",
|
@@ -482,6 +482,9 @@ function replaceCidImages(htmlContent, replacements) {
|
|
482
482
|
}
|
483
483
|
});
|
484
484
|
|
485
|
+
if (hasPrivacyImage) {
|
486
|
+
this.setOption("privacy.visible", true); // Show the privacy text if any images are marked as privacy
|
487
|
+
}
|
485
488
|
// Serialize the modified document back to HTML
|
486
489
|
const serializer = new XMLSerializer();
|
487
490
|
return serializer.serializeToString(doc);
|
@@ -24,7 +24,7 @@ div[data-monster-role="content-container"] {
|
|
24
24
|
|
25
25
|
appearance: none;
|
26
26
|
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='currentColor' viewBox='0 0 16 16'%3E%3Cpath d='M5.338 1.59a61 61 0 0 0-2.837.856.48.48 0 0 0-.328.39c-.554 4.157.726 7.19 2.253 9.188a10.7 10.7 0 0 0 2.287 2.233c.346.244.652.42.893.533q.18.085.293.118a1 1 0 0 0 .101.025 1 1 0 0 0 .1-.025q.114-.034.294-.118c.24-.113.547-.29.893-.533a10.7 10.7 0 0 0 2.287-2.233c1.527-1.997 2.807-5.031 2.253-9.188a.48.48 0 0 0-.328-.39c-.651-.213-1.75-.56-2.837-.855C9.552 1.29 8.531 1.067 8 1.067c-.53 0-1.552.223-2.662.524zM5.072.56C6.157.265 7.31 0 8 0s1.843.265 2.928.56c1.11.3 2.229.655 2.887.87a1.54 1.54 0 0 1 1.044 1.262c.596 4.477-.787 7.795-2.465 9.99a11.8 11.8 0 0 1-2.517 2.453 7 7 0 0 1-1.048.625c-.28.132-.581.24-.829.24s-.548-.108-.829-.24a7 7 0 0 1-1.048-.625 11.8 11.8 0 0 1-2.517-2.453C1.928 10.487.545 7.169 1.141 2.692A1.54 1.54 0 0 1 2.185 1.43 63 63 0 0 1 5.072.56'/%3E%3Cpath d='M7.001 11a1 1 0 1 1 2 0 1 1 0 0 1-2 0M7.1 4.995a.905.905 0 1 1 1.8 0l-.35 3.507a.553.553 0 0 1-1.1 0z'/%3E%3C/svg%3E");
|
27
|
-
mask-size:
|
27
|
+
mask-size: 50%;
|
28
28
|
mask-repeat: no-repeat;
|
29
29
|
mask-position: center center;
|
30
30
|
background-color: var(--monster-bg-color-primary-4);
|
@@ -35,7 +35,7 @@ div[data-monster-role="content-container"] {
|
|
35
35
|
|
36
36
|
appearance: none;
|
37
37
|
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='currentColor' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M3.112 5.112a3 3 0 0 0-.17.613C1.266 6.095 0 7.555 0 9.318 0 11.366 1.708 13 3.781 13H11l-1-1H3.781C2.231 12 1 10.785 1 9.318c0-1.365 1.064-2.513 2.46-2.666l.446-.05v-.447q0-.113.018-.231zm2.55-1.45-.725-.725A5.5 5.5 0 0 1 8 2c2.69 0 4.923 2 5.166 4.579C14.758 6.804 16 8.137 16 9.773a3.2 3.2 0 0 1-1.516 2.711l-.733-.733C14.498 11.378 15 10.626 15 9.773c0-1.216-1.02-2.228-2.313-2.228h-.5v-.5C12.188 4.825 10.328 3 8 3c-.875 0-1.678.26-2.339.661z'/%3E%3Cpath d='m13.646 14.354-12-12 .708-.708 12 12z'/%3E%3C/svg%3E");
|
38
|
-
mask-size:
|
38
|
+
mask-size: 50%;
|
39
39
|
mask-repeat: no-repeat;
|
40
40
|
mask-position: center center;
|
41
41
|
background-color: var(--monster-bg-color-primary-4);
|
@@ -24,7 +24,7 @@ const HtmlStyleSheet = new CSSStyleSheet();
|
|
24
24
|
try {
|
25
25
|
HtmlStyleSheet.insertRule(`
|
26
26
|
@layer html {
|
27
|
-
div,img{transition:opacity .9s ease,transform .3s ease,filter .3s ease,box-shadow .3s ease,background-color .3s ease}div[data-monster-role=content-container]{display:flex;flex-direction:column;height:100%;overflow:auto;position:relative;width:100%}.privacyImage{-webkit-mask-image:url(\"data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='currentColor' viewBox='0 0 16 16'%3E%3Cpath d='M5.338 1.59a61 61 0 0 0-2.837.856.48.48 0 0 0-.328.39c-.554 4.157.726 7.19 2.253 9.188a10.7 10.7 0 0 0 2.287 2.233c.346.244.652.42.893.533q.18.085.293.118a1 1 0 0 0 .101.025 1 1 0 0 0 .1-.025q.114-.034.294-.118c.24-.113.547-.29.893-.533a10.7 10.7 0 0 0 2.287-2.233c1.527-1.997 2.807-5.031 2.253-9.188a.48.48 0 0 0-.328-.39c-.651-.213-1.75-.56-2.837-.855C9.552 1.29 8.531 1.067 8 1.067c-.53 0-1.552.223-2.662.524zM5.072.56C6.157.265 7.31 0 8 0s1.843.265 2.928.56c1.11.3 2.229.655 2.887.87a1.54 1.54 0 0 1 1.044 1.262c.596 4.477-.787 7.795-2.465 9.99a11.8 11.8 0 0 1-2.517 2.453 7 7 0 0 1-1.048.625c-.28.132-.581.24-.829.24s-.548-.108-.829-.24a7 7 0 0 1-1.048-.625 11.8 11.8 0 0 1-2.517-2.453C1.928 10.487.545 7.169 1.141 2.692A1.54 1.54 0 0 1 2.185 1.43 63 63 0 0 1 5.072.56'/%3E%3Cpath d='M7.001 11a1 1 0 1 1 2 0 1 1 0 0 1-2 0M7.1 4.995a.905.905 0 1 1 1.8 0l-.35 3.507a.553.553 0 0 1-1.1 0z'/%3E%3C/svg%3E\");mask-image:url(\"data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='currentColor' viewBox='0 0 16 16'%3E%3Cpath d='M5.338 1.59a61 61 0 0 0-2.837.856.48.48 0 0 0-.328.39c-.554 4.157.726 7.19 2.253 9.188a10.7 10.7 0 0 0 2.287 2.233c.346.244.652.42.893.533q.18.085.293.118a1 1 0 0 0 .101.025 1 1 0 0 0 .1-.025q.114-.034.294-.118c.24-.113.547-.29.893-.533a10.7 10.7 0 0 0 2.287-2.233c1.527-1.997 2.807-5.031 2.253-9.188a.48.48 0 0 0-.328-.39c-.651-.213-1.75-.56-2.837-.855C9.552 1.29 8.531 1.067 8 1.067c-.53 0-1.552.223-2.662.524zM5.072.56C6.157.265 7.31 0 8 0s1.843.265 2.928.56c1.11.3 2.229.655 2.887.87a1.54 1.54 0 0 1 1.044 1.262c.596 4.477-.787 7.795-2.465 9.99a11.8 11.8 0 0 1-2.517 2.453 7 7 0 0 1-1.048.625c-.28.132-.581.24-.829.24s-.548-.108-.829-.24a7 7 0 0 1-1.048-.625 11.8 11.8 0 0 1-2.517-2.453C1.928 10.487.545 7.169 1.141 2.692A1.54 1.54 0 0 1 2.185 1.43 63 63 0 0 1 5.072.56'/%3E%3Cpath d='M7.001 11a1 1 0 1 1 2 0 1 1 0 0 1-2 0M7.1 4.995a.905.905 0 1 1 1.8 0l-.35 3.507a.553.553 0 0 1-1.1 0z'/%3E%3C/svg%3E\")}.notFoundImage,.privacyImage{-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:var(--monster-bg-color-primary-4);-webkit-mask-position:center center;mask-position:center center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:
|
27
|
+
div,img{transition:opacity .9s ease,transform .3s ease,filter .3s ease,box-shadow .3s ease,background-color .3s ease}div[data-monster-role=content-container]{display:flex;flex-direction:column;height:100%;overflow:auto;position:relative;width:100%}.privacyImage{-webkit-mask-image:url(\"data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='currentColor' viewBox='0 0 16 16'%3E%3Cpath d='M5.338 1.59a61 61 0 0 0-2.837.856.48.48 0 0 0-.328.39c-.554 4.157.726 7.19 2.253 9.188a10.7 10.7 0 0 0 2.287 2.233c.346.244.652.42.893.533q.18.085.293.118a1 1 0 0 0 .101.025 1 1 0 0 0 .1-.025q.114-.034.294-.118c.24-.113.547-.29.893-.533a10.7 10.7 0 0 0 2.287-2.233c1.527-1.997 2.807-5.031 2.253-9.188a.48.48 0 0 0-.328-.39c-.651-.213-1.75-.56-2.837-.855C9.552 1.29 8.531 1.067 8 1.067c-.53 0-1.552.223-2.662.524zM5.072.56C6.157.265 7.31 0 8 0s1.843.265 2.928.56c1.11.3 2.229.655 2.887.87a1.54 1.54 0 0 1 1.044 1.262c.596 4.477-.787 7.795-2.465 9.99a11.8 11.8 0 0 1-2.517 2.453 7 7 0 0 1-1.048.625c-.28.132-.581.24-.829.24s-.548-.108-.829-.24a7 7 0 0 1-1.048-.625 11.8 11.8 0 0 1-2.517-2.453C1.928 10.487.545 7.169 1.141 2.692A1.54 1.54 0 0 1 2.185 1.43 63 63 0 0 1 5.072.56'/%3E%3Cpath d='M7.001 11a1 1 0 1 1 2 0 1 1 0 0 1-2 0M7.1 4.995a.905.905 0 1 1 1.8 0l-.35 3.507a.553.553 0 0 1-1.1 0z'/%3E%3C/svg%3E\");mask-image:url(\"data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='currentColor' viewBox='0 0 16 16'%3E%3Cpath d='M5.338 1.59a61 61 0 0 0-2.837.856.48.48 0 0 0-.328.39c-.554 4.157.726 7.19 2.253 9.188a10.7 10.7 0 0 0 2.287 2.233c.346.244.652.42.893.533q.18.085.293.118a1 1 0 0 0 .101.025 1 1 0 0 0 .1-.025q.114-.034.294-.118c.24-.113.547-.29.893-.533a10.7 10.7 0 0 0 2.287-2.233c1.527-1.997 2.807-5.031 2.253-9.188a.48.48 0 0 0-.328-.39c-.651-.213-1.75-.56-2.837-.855C9.552 1.29 8.531 1.067 8 1.067c-.53 0-1.552.223-2.662.524zM5.072.56C6.157.265 7.31 0 8 0s1.843.265 2.928.56c1.11.3 2.229.655 2.887.87a1.54 1.54 0 0 1 1.044 1.262c.596 4.477-.787 7.795-2.465 9.99a11.8 11.8 0 0 1-2.517 2.453 7 7 0 0 1-1.048.625c-.28.132-.581.24-.829.24s-.548-.108-.829-.24a7 7 0 0 1-1.048-.625 11.8 11.8 0 0 1-2.517-2.453C1.928 10.487.545 7.169 1.141 2.692A1.54 1.54 0 0 1 2.185 1.43 63 63 0 0 1 5.072.56'/%3E%3Cpath d='M7.001 11a1 1 0 1 1 2 0 1 1 0 0 1-2 0M7.1 4.995a.905.905 0 1 1 1.8 0l-.35 3.507a.553.553 0 0 1-1.1 0z'/%3E%3C/svg%3E\")}.notFoundImage,.privacyImage{-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:var(--monster-bg-color-primary-4);-webkit-mask-position:center center;mask-position:center center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:50%;mask-size:50%;-o-object-fit:contain;object-fit:contain}.notFoundImage{-webkit-mask-image:url(\"data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='currentColor' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M3.112 5.112a3 3 0 0 0-.17.613C1.266 6.095 0 7.555 0 9.318 0 11.366 1.708 13 3.781 13H11l-1-1H3.781C2.231 12 1 10.785 1 9.318c0-1.365 1.064-2.513 2.46-2.666l.446-.05v-.447q0-.113.018-.231zm2.55-1.45-.725-.725A5.5 5.5 0 0 1 8 2c2.69 0 4.923 2 5.166 4.579C14.758 6.804 16 8.137 16 9.773a3.2 3.2 0 0 1-1.516 2.711l-.733-.733C14.498 11.378 15 10.626 15 9.773c0-1.216-1.02-2.228-2.313-2.228h-.5v-.5C12.188 4.825 10.328 3 8 3c-.875 0-1.678.26-2.339.661z'/%3E%3Cpath d='m13.646 14.354-12-12 .708-.708 12 12z'/%3E%3C/svg%3E\");mask-image:url(\"data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='currentColor' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M3.112 5.112a3 3 0 0 0-.17.613C1.266 6.095 0 7.555 0 9.318 0 11.366 1.708 13 3.781 13H11l-1-1H3.781C2.231 12 1 10.785 1 9.318c0-1.365 1.064-2.513 2.46-2.666l.446-.05v-.447q0-.113.018-.231zm2.55-1.45-.725-.725A5.5 5.5 0 0 1 8 2c2.69 0 4.923 2 5.166 4.579C14.758 6.804 16 8.137 16 9.773a3.2 3.2 0 0 1-1.516 2.711l-.733-.733C14.498 11.378 15 10.626 15 9.773c0-1.216-1.02-2.228-2.313-2.228h-.5v-.5C12.188 4.825 10.328 3 8 3c-.875 0-1.678.26-2.339.661z'/%3E%3Cpath d='m13.646 14.354-12-12 .708-.708 12 12z'/%3E%3C/svg%3E\")}
|
28
28
|
}`, 0);
|
29
29
|
} catch (e) {
|
30
30
|
addAttributeToken(document.getRootNode().querySelector('html'), ATTRIBUTE_ERRORMESSAGE, e + "");
|
@@ -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
|
+
}
|