@schukai/monster 4.120.0 → 4.121.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,22 @@
2
2
 
3
3
 
4
4
 
5
+ ## [4.121.0] - 2026-02-13
6
+
7
+ ### Add Features
8
+
9
+ - Enhance dismissal functionality for Popper and Select components
10
+
11
+
12
+
13
+ ## [4.120.1] - 2026-02-12
14
+
15
+ ### Bug Fixes
16
+
17
+ - new setContent/setPDF API for MonsterViewer
18
+
19
+
20
+
5
21
  ## [4.120.0] - 2026-02-03
6
22
 
7
23
  ### Add Features
package/package.json CHANGED
@@ -1 +1 @@
1
- {"author":"Volker Schukai","dependencies":{"@floating-ui/dom":"^1.7.4","@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.120.0"}
1
+ {"author":"Volker Schukai","dependencies":{"@floating-ui/dom":"^1.7.4","@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.121.0"}
@@ -138,7 +138,14 @@ class Viewer extends CustomElement {
138
138
  /**
139
139
  * Sets the content of an element based on the provided content and media type.
140
140
  *
141
- * @param {string} content - The content to be set.
141
+ * Supported forms:
142
+ * - setContent(contentString, mediaType)
143
+ * - setContent({ mediaType, data, encoding })
144
+ *
145
+ * The declarative form requires an explicit `encoding` when `data` is a string.
146
+ * Use `encoding: "url"` for URLs and `encoding: "base64"` for base64 payloads.
147
+ *
148
+ * @param {string|object} content - The content to be set or a declarative payload.
142
149
  * @param {string} [mediaType="text/plain"] - The media type of the content. Defaults to "text/plain" if not specified.
143
150
  * @return {void} This method does not return a value.
144
151
  * @throws {Error} Throws an error if shadowRoot is not defined.
@@ -148,6 +155,44 @@ class Viewer extends CustomElement {
148
155
  throw new Error("no shadow-root is defined");
149
156
  }
150
157
 
158
+ let declarativePayload = null;
159
+ // Declarative form: { mediaType, data, encoding }
160
+ if (
161
+ content &&
162
+ typeof content === "object" &&
163
+ !isBlob(content) &&
164
+ !(content instanceof URL)
165
+ ) {
166
+ const { mediaType: mt, data, encoding } = content;
167
+ mediaType = mt ?? mediaType ?? "text/plain";
168
+ declarativePayload = { data, encoding };
169
+
170
+ if (isString(data)) {
171
+ if (!encoding) {
172
+ this.dispatchEvent(
173
+ new CustomEvent("viewer-error", {
174
+ detail: "String content requires explicit encoding",
175
+ }),
176
+ );
177
+ return;
178
+ }
179
+ if (encoding === "base64") {
180
+ content = data;
181
+ } else if (encoding === "url") {
182
+ content = data;
183
+ } else {
184
+ this.dispatchEvent(
185
+ new CustomEvent("viewer-error", {
186
+ detail: "Invalid encoding",
187
+ }),
188
+ );
189
+ return;
190
+ }
191
+ } else {
192
+ content = data;
193
+ }
194
+ }
195
+
151
196
  const renderers = this.getOption("renderers");
152
197
 
153
198
  const isDataURL = (value) => {
@@ -235,7 +280,10 @@ class Viewer extends CustomElement {
235
280
  switch (mediaTypeObject.subtype) {
236
281
  case "pdf":
237
282
  if (checkRenderer("pdf", mediaTypeObject.toString())) {
238
- renderers.pdf.call(this, content);
283
+ renderers.pdf.call(
284
+ this,
285
+ declarativePayload ? declarativePayload : content,
286
+ );
239
287
  }
240
288
  break;
241
289
 
@@ -426,13 +474,63 @@ class Viewer extends CustomElement {
426
474
  /**
427
475
  * Configures and embeds a PDF document into the application with customizable display settings.
428
476
  *
429
- * @param {Blob|URL|string} data The PDF data to be embedded. Can be provided as a Blob, URL, or base64 string.
477
+ * Supported forms:
478
+ * - setPDF(blobOrUrlOrBase64String, navigation, toolbar, scrollbar)
479
+ * - setPDF({ data, encoding, navigation, toolbar, scrollbar })
480
+ *
481
+ * The declarative form requires an explicit `encoding` when `data` is a string.
482
+ * Use `encoding: "url"` for URLs and `encoding: "base64"` for base64 payloads.
483
+ *
484
+ * @param {Blob|URL|string|object} data The PDF data to be embedded. Can be provided as a Blob, URL, or base64 string,
485
+ * or a declarative payload: { data, encoding, navigation, toolbar, scrollbar }.
430
486
  * @param {boolean} [navigation=true] Determines whether the navigation pane is displayed in the PDF viewer.
431
487
  * @param {boolean} [toolbar=true] Controls the visibility of the toolbar in the PDF viewer.
432
488
  * @param {boolean} [scrollbar=false] Configures the display of the scrollbar in the PDF viewer.
433
489
  * @return {void} This method returns nothing but sets the embedded PDF as the content.
434
490
  */
435
491
  setPDF(data, navigation = true, toolbar = true, scrollbar = false) {
492
+ let stringEncoding = null;
493
+ // Declarative form: { data, encoding, navigation, toolbar, scrollbar }
494
+ if (
495
+ data &&
496
+ typeof data === "object" &&
497
+ !isBlob(data) &&
498
+ !(data instanceof URL)
499
+ ) {
500
+ const payload = data;
501
+ data = payload.data;
502
+ stringEncoding = payload.encoding ?? null;
503
+ if (typeof payload.navigation === "boolean") {
504
+ navigation = payload.navigation;
505
+ }
506
+ if (typeof payload.toolbar === "boolean") {
507
+ toolbar = payload.toolbar;
508
+ }
509
+ if (typeof payload.scrollbar === "boolean") {
510
+ scrollbar = payload.scrollbar;
511
+ }
512
+ if (isString(data)) {
513
+ if (!payload.encoding) {
514
+ this.dispatchEvent(
515
+ new CustomEvent("viewer-error", {
516
+ detail: "String PDF data requires explicit encoding",
517
+ }),
518
+ );
519
+ return;
520
+ }
521
+ if (payload.encoding === "base64") {
522
+ // handled below as string/base64
523
+ } else if (payload.encoding === "url") {
524
+ // handled below as string/url
525
+ } else {
526
+ this.dispatchEvent(
527
+ new CustomEvent("viewer-error", { detail: "Invalid encoding" }),
528
+ );
529
+ return;
530
+ }
531
+ }
532
+ }
533
+
436
534
  const hashes =
437
535
  "#toolbar=" +
438
536
  (toolbar ? "1" : "0") +
@@ -440,7 +538,6 @@ class Viewer extends CustomElement {
440
538
  (navigation ? "1" : "0") +
441
539
  "&scrollbar=" +
442
540
  (scrollbar ? "1" : "0");
443
-
444
541
  let pdfURL = "";
445
542
  if (isBlob(data)) {
446
543
  pdfURL = URL.createObjectURL(data);
@@ -453,11 +550,18 @@ class Viewer extends CustomElement {
453
550
  pdfURL = data.toString();
454
551
  }
455
552
  } else if (isString(data)) {
456
- //URL.createObjectURL(data);
457
- const blobObj = new Blob([atob(data)], { type: "application/pdf" });
458
- const url = window.URL.createObjectURL(blobObj);
459
-
460
- pdfURL = data;
553
+ if (stringEncoding === "url") {
554
+ if (data.indexOf("#") === -1) {
555
+ pdfURL = data + hashes;
556
+ } else {
557
+ pdfURL = data;
558
+ }
559
+ } else {
560
+ // Default legacy behavior: treat string as base64
561
+ const blobObj = new Blob([atob(data)], { type: "application/pdf" });
562
+ const url = window.URL.createObjectURL(blobObj);
563
+ pdfURL = url + hashes;
564
+ }
461
565
  } else {
462
566
  this.dispatchEvent(
463
567
  new CustomEvent("viewer-error", { detail: "Blob or URL expected" }),
@@ -18,7 +18,7 @@ import {
18
18
  assembleMethodSymbol,
19
19
  registerCustomElement,
20
20
  } from "../../dom/customelement.mjs";
21
- import { getDocument } from "../../dom/util.mjs";
21
+ import { findElementWithSelectorUpwards, getDocument } from "../../dom/util.mjs";
22
22
  import { isFunction } from "../../types/is.mjs";
23
23
  import { DeadMansSwitch } from "../../util/deadmansswitch.mjs";
24
24
  import { Popper } from "../layout/popper.mjs";
@@ -77,6 +77,20 @@ const popperElementSymbol = Symbol("popperElement");
77
77
  */
78
78
  const arrowElementSymbol = Symbol("arrowElement");
79
79
 
80
+ /**
81
+ * local symbol
82
+ * @private
83
+ * @type {symbol}
84
+ */
85
+ const hostElementSymbol = Symbol("hostElement");
86
+
87
+ /**
88
+ * local symbol
89
+ * @private
90
+ * @type {symbol}
91
+ */
92
+ const usesHostDismissSymbol = Symbol("usesHostDismiss");
93
+
80
94
  /**
81
95
  * A beautiful popper button that can make your life easier and also looks good.
82
96
  *
@@ -149,6 +163,10 @@ class PopperButton extends Popper {
149
163
  role: null,
150
164
  label: null,
151
165
  },
166
+
167
+ features: {
168
+ closeOnOutsideClick: true,
169
+ },
152
170
  });
153
171
  }
154
172
 
@@ -187,11 +205,24 @@ class PopperButton extends Popper {
187
205
  connectedCallback() {
188
206
  super.connectedCallback();
189
207
 
208
+ this[hostElementSymbol] = findElementWithSelectorUpwards(
209
+ this,
210
+ "monster-host",
211
+ );
212
+ this[usesHostDismissSymbol] =
213
+ this[hostElementSymbol] &&
214
+ typeof this[hostElementSymbol].registerDismissable === "function";
215
+
190
216
  const document = getDocument();
191
217
 
192
- for (const [, type] of Object.entries(["click", "touch"])) {
193
- // close on outside ui-events
194
- document.addEventListener(type, this[closeEventHandler]);
218
+ if (
219
+ this.getOption("features.closeOnOutsideClick", true) === true &&
220
+ !this[usesHostDismissSymbol]
221
+ ) {
222
+ for (const [, type] of Object.entries(["click", "touch"])) {
223
+ // close on outside ui-events
224
+ document.addEventListener(type, this[closeEventHandler]);
225
+ }
195
226
  }
196
227
 
197
228
  updatePopper.call(this);
@@ -204,9 +235,14 @@ class PopperButton extends Popper {
204
235
  disconnectedCallback() {
205
236
  super.disconnectedCallback();
206
237
 
207
- // close on outside ui-events
208
- for (const [, type] of Object.entries(["click", "touch"])) {
209
- document.removeEventListener(type, this[closeEventHandler]);
238
+ if (
239
+ this.getOption("features.closeOnOutsideClick", true) === true &&
240
+ !this[usesHostDismissSymbol]
241
+ ) {
242
+ // close on outside ui-events
243
+ for (const [, type] of Object.entries(["click", "touch"])) {
244
+ document.removeEventListener(type, this[closeEventHandler]);
245
+ }
210
246
  }
211
247
 
212
248
  disconnectResizeObserver.call(this);
@@ -38,7 +38,7 @@ import {
38
38
  fireEvent,
39
39
  } from "../../dom/events.mjs";
40
40
  import { getLocaleOfDocument } from "../../dom/locale.mjs";
41
- import { getDocument } from "../../dom/util.mjs";
41
+ import { findElementWithSelectorUpwards, getDocument } from "../../dom/util.mjs";
42
42
  import {
43
43
  getDocumentTranslations,
44
44
  Translations,
@@ -105,6 +105,9 @@ const isLoadingSymbol = Symbol("isLoading");
105
105
  * @type {Symbol}
106
106
  */
107
107
  const closeEventHandler = Symbol("closeEventHandler");
108
+ const hostElementSymbol = Symbol("hostElement");
109
+ const dismissRecordSymbol = Symbol("dismissRecord");
110
+ const usesHostDismissSymbol = Symbol("usesHostDismiss");
108
111
 
109
112
  /**
110
113
  * local symbol
@@ -841,11 +844,20 @@ class Select extends CustomControl {
841
844
  */
842
845
  connectedCallback() {
843
846
  super.connectedCallback();
844
- const document = getDocument();
845
-
846
- for (const [, type] of Object.entries(["click", "touch"])) {
847
- // close on outside ui-events
848
- document.addEventListener(type, this[closeEventHandler]);
847
+ this[hostElementSymbol] = findElementWithSelectorUpwards(
848
+ this,
849
+ "monster-host",
850
+ );
851
+ this[usesHostDismissSymbol] =
852
+ this[hostElementSymbol] &&
853
+ typeof this[hostElementSymbol].registerDismissable === "function";
854
+
855
+ if (!this[usesHostDismissSymbol]) {
856
+ const document = getDocument();
857
+ for (const [, type] of Object.entries(["click", "touch"])) {
858
+ // close on outside ui-events
859
+ document.addEventListener(type, this[closeEventHandler]);
860
+ }
849
861
  }
850
862
 
851
863
  parseSlotsToOptions.call(this);
@@ -873,13 +885,16 @@ class Select extends CustomControl {
873
885
  */
874
886
  disconnectedCallback() {
875
887
  super.disconnectedCallback();
876
- const document = getDocument();
877
-
878
- // close on outside ui-events
879
- for (const [, type] of Object.entries(["click", "touch"])) {
880
- document.removeEventListener(type, this[closeEventHandler]);
888
+ if (!this[usesHostDismissSymbol]) {
889
+ const document = getDocument();
890
+ // close on outside ui-events
891
+ for (const [, type] of Object.entries(["click", "touch"])) {
892
+ document.removeEventListener(type, this[closeEventHandler]);
893
+ }
881
894
  }
882
895
 
896
+ unregisterFromHost.call(this);
897
+
883
898
  disconnectResizeObserver.call(this);
884
899
  }
885
900
 
@@ -3703,6 +3718,7 @@ function hide() {
3703
3718
  this[popperElementSymbol].style.display = "none";
3704
3719
  setStatusOrRemoveBadges.call(this, "closed");
3705
3720
  removeAttributeToken(this[controlElementSymbol], "class", "open");
3721
+ unregisterFromHost.call(this);
3706
3722
  }
3707
3723
 
3708
3724
  /**
@@ -3772,6 +3788,7 @@ function show() {
3772
3788
  setStatusOrRemoveBadges.call(this, "open");
3773
3789
 
3774
3790
  addAttributeToken(this[controlElementSymbol], "class", "open");
3791
+ registerWithHost.call(this);
3775
3792
 
3776
3793
  new Processing(() => {
3777
3794
  if (!self?.[remoteFilterFirstOpendSymbol]) {
@@ -3798,6 +3815,56 @@ function show() {
3798
3815
  });
3799
3816
  }
3800
3817
 
3818
+ /**
3819
+ * @private
3820
+ */
3821
+ function registerWithHost() {
3822
+ if (!this[usesHostDismissSymbol]) {
3823
+ return;
3824
+ }
3825
+
3826
+ if (!(this[hostElementSymbol] instanceof HTMLElement)) {
3827
+ return;
3828
+ }
3829
+
3830
+ const record = this[hostElementSymbol].registerDismissable?.({
3831
+ element: this,
3832
+ owner: this,
3833
+ close: () => {
3834
+ hide.call(this);
3835
+ },
3836
+ priority: 20,
3837
+ options: {
3838
+ dismissOnOutside: true,
3839
+ },
3840
+ });
3841
+
3842
+ if (record) {
3843
+ this[dismissRecordSymbol] = record;
3844
+ }
3845
+ }
3846
+
3847
+ /**
3848
+ * @private
3849
+ */
3850
+ function unregisterFromHost() {
3851
+ if (!this[usesHostDismissSymbol]) {
3852
+ return;
3853
+ }
3854
+
3855
+ if (!(this[hostElementSymbol] instanceof HTMLElement)) {
3856
+ return;
3857
+ }
3858
+
3859
+ if (this[dismissRecordSymbol]) {
3860
+ this[hostElementSymbol].unregisterDismissable?.(this[dismissRecordSymbol]);
3861
+ this[dismissRecordSymbol] = null;
3862
+ return;
3863
+ }
3864
+
3865
+ this[hostElementSymbol].unregisterDismissable?.(this);
3866
+ }
3867
+
3801
3868
  function initDefaultOptionsFromUrl() {
3802
3869
  const url = this.getOption("filter.defaultOptionsUrl");
3803
3870
  if (!url) {