@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 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.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.1"}
@@ -121,7 +121,7 @@ class MessageContent extends CustomElement {
121
121
  },
122
122
 
123
123
  privacy: {
124
- visible: true,
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: 70%;
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: 70%;
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:70%;mask-size:70%;-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\")}
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
+ }