@schukai/monster 4.100.0 → 4.101.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,25 @@
2
2
 
3
3
 
4
4
 
5
+ ## [4.101.0] - 2026-01-17
6
+
7
+ ### Add Features
8
+
9
+ - Add control overflow property to Panel component
10
+
11
+
12
+
13
+ ## [4.100.1] - 2026-01-16
14
+
15
+ ### Bug Fixes
16
+
17
+ - config values [#375](https://gitlab.schukai.com/oss/libraries/javascript/monster/issues/375)
18
+ ### Changes
19
+
20
+ - doc
21
+
22
+
23
+
5
24
  ## [4.100.0] - 2026-01-16
6
25
 
7
26
  ### 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.100.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.101.0"}
@@ -15,9 +15,9 @@
15
15
  import { instanceSymbol } from "../../constants.mjs";
16
16
  import { ATTRIBUTE_DISABLED, ATTRIBUTE_ROLE } from "../../dom/constants.mjs";
17
17
  import {
18
- assembleMethodSymbol,
19
- attributeObserverSymbol,
20
- registerCustomElement,
18
+ assembleMethodSymbol,
19
+ attributeObserverSymbol,
20
+ registerCustomElement,
21
21
  } from "../../dom/customelement.mjs";
22
22
  import { isArray, isString } from "../../types/is.mjs";
23
23
  import { validateString } from "../../types/validate.mjs";
@@ -57,313 +57,326 @@ const measurementPopperSymbol = Symbol("measurementPopper");
57
57
  * @fires monster-click - Fired when button is clicked
58
58
  */
59
59
  class MessageStateButton extends Popper {
60
- /**
61
- * This method is called by the `instanceof` operator.
62
- * @return {symbol}
63
- */
64
- static get [instanceSymbol]() {
65
- return Symbol.for(
66
- "@schukai/monster/components/form/message-state-button@@instance",
67
- );
68
- }
69
-
70
- /**
71
- * Sets the state of the button which affects its visual appearance
72
- *
73
- * @param {string} state - The state to set (e.g. 'success', 'error', 'loading')
74
- * @param {number} timeout - Optional timeout in milliseconds after which state is removed
75
- * @return {MessageStateButton} Returns the button instance for chaining
76
- * @throws {TypeError} When state is not a string or timeout is not a number
77
- */
78
- setState(state, timeout) {
79
- return this[buttonElementSymbol].setState(state, timeout);
80
- }
81
-
82
- /**
83
- *
84
- * @return {MessageStateButton}
85
- */
86
- removeState() {
87
- return this[buttonElementSymbol].removeState();
88
- }
89
-
90
- /**
91
- * @return {MessageStateButton|undefined}
92
- */
93
- getState() {
94
- return this[buttonElementSymbol].getState();
95
- }
96
-
97
- /**
98
- * To set the options via the HTML tag, the attribute `data-monster-options` must be used.
99
- * @see {@link https://monsterjs.org/en/doc/#configurate-a-monster-control}
100
- *
101
- * The individual configuration values can be found in the table.
102
- *
103
- * @property {Object} templates Template definitions
104
- * @property {string} templates.main Main template
105
- * @property {Object} message Message definition
106
- * @property {string|HTMLElement} message.content The message content
107
- * @property {string} message.title The message title
108
- * @property {string} message.icon The message icon
109
- * @property {string} mode The mode of the button, can be `manual` or `submit`
110
- * @property {string} labels.button Button label
111
- * @property {boolean} features.disableButton Disable the button
112
- * @property {Object} aria Aria attributes
113
- * @property {string} aria.role Aria role, only if the button is not a button
114
- */
115
- get defaults() {
116
- return Object.assign({}, super.defaults, {
117
- message: {
118
- title: undefined,
119
- content: undefined,
120
- icon: undefined,
121
- },
122
- templates: {
123
- main: getTemplate(),
124
- },
125
- mode: "manual",
126
- labels: {
127
- button: "<slot></slot>",
128
- },
129
- classes: {
130
- button: "monster-button-outline-primary",
131
- },
132
- actions: {
133
- click: (e) => {},
134
- },
135
-
136
- aria: {
137
- role: null,
138
- label: null,
139
- },
140
- });
141
- }
142
-
143
- /**
144
- * @return {void}
145
- */
146
- [assembleMethodSymbol]() {
147
- super[assembleMethodSymbol]();
148
- initControlReferences.call(this);
149
- initDisabledSync.call(this);
150
-
151
- let modes = null;
152
- const modeOption = this.getOption("mode");
153
- if (typeof modeOption === "string") {
154
- modes = modeOption.split(" ");
155
- }
156
-
157
- if (
158
- modes === null ||
159
- modes === undefined ||
160
- isArray(modes) === false ||
161
- modes.length === 0
162
- ) {
163
- modes = ["manual"];
164
- }
165
-
166
- for (const [, mode] of Object.entries(modes)) {
167
- initEventHandlerByMode.call(this, mode);
168
- }
169
- }
170
-
171
- /**
172
- * Sets the message content to be displayed in the popup overlay
173
- *
174
- * @param {string|HTMLElement} message - The message content as string or HTML element
175
- * @param {string} title - Optional title to show above the message
176
- * @param {string} icon - Optional icon HTML to display next to the title
177
- * @return {MessageStateButton} Returns the button instance for chaining
178
- * @throws {TypeError} When message is empty or invalid type
179
- */
180
- setMessage(message, title, icon) {
181
- if (isString(message)) {
182
- if (message === "") {
183
- throw new TypeError("message must not be empty");
184
- }
185
-
186
- const containerDiv = document.createElement("div");
187
- const messageDiv = document.createElement("div");
188
- const titleDiv = document.createElement("div");
189
- titleDiv.setAttribute(ATTRIBUTE_ROLE, "message-title-box");
190
-
191
- let titleElement, iconElement;
192
- if (title !== undefined) {
193
- title = validateString(title);
194
- titleElement = document.createElement("div");
195
- titleElement.setAttribute("class", "");
196
- titleElement.innerHTML = title;
197
- titleElement.setAttribute(ATTRIBUTE_ROLE, "message-title");
198
- titleDiv.appendChild(titleElement);
199
- }
200
-
201
- if (icon !== undefined) {
202
- icon = validateString(icon);
203
- iconElement = document.createElement("div");
204
- iconElement.setAttribute("class", "");
205
- iconElement.innerHTML = icon;
206
- iconElement.setAttribute(ATTRIBUTE_ROLE, "message-icon");
207
- titleDiv.appendChild(iconElement);
208
- }
209
-
210
- messageDiv.innerHTML = message;
211
- containerDiv.appendChild(titleDiv);
212
- containerDiv.appendChild(messageDiv);
213
-
214
- this.setOption("message.content", containerDiv);
215
- } else if (message instanceof HTMLElement) {
216
- this.setOption("message.content", message);
217
- } else {
218
- throw new TypeError(
219
- "message must be a string or an instance of HTMLElement",
220
- );
221
- }
222
-
223
- return this;
224
- }
225
-
226
- /**
227
- * clears the Message
228
- *
229
- * @return {MessageStateButton}
230
- */
231
- clearMessage() {
232
- this.setOption("message.title", undefined);
233
- this.setOption("message.content", undefined);
234
- this.setOption("message.icon", undefined);
235
- return this;
236
- }
237
-
238
- /**
239
- * Shows the message popup overlay with optional auto-hide timeout
240
- *
241
- * @param {number} timeout - Optional time in milliseconds after which the message will auto-hide
242
- * @return {MessageStateButton} Returns the button instance for chaining
243
- */
244
- showMessage(timeout) {
245
- applyMeasuredMessageWidth.call(this);
246
- this.showDialog.call(this);
247
-
248
- if (timeout !== undefined) {
249
- setTimeout(() => {
250
- super.hideDialog();
251
- }, timeout);
252
- }
253
-
254
- return this;
255
- }
256
-
257
- /**
258
- * With this method, you can show the popper.
259
- *
260
- * @return {MessageStateButton}
261
- */
262
- showDialog() {
263
- if (this.getOption("message.content") === undefined) {
264
- return;
265
- }
266
- super.showDialog();
267
- return this;
268
- }
269
-
270
- /**
271
- *
272
- * @return {MessageStateButton}
273
- */
274
- hideMessage() {
275
- super.hideDialog();
276
- return this;
277
- }
278
-
279
- /**
280
- *
281
- * @return {MessageStateButton}
282
- */
283
- toggleMessage() {
284
- super.toggleDialog();
285
- return this;
286
- }
287
-
288
- /**
289
- *
290
- * @return {Object}
291
- */
292
- getMessage() {
293
- return this.getOption("message");
294
- }
295
-
296
- /**
297
- *
298
- * @return {string}
299
- */
300
- static getTag() {
301
- return "monster-message-state-button";
302
- }
303
-
304
- /**
305
- *
306
- * @return {CSSStyleSheet[]}
307
- */
308
- static getCSSStyleSheet() {
309
- const styles = Popper.getCSSStyleSheet();
310
- styles.push(StateButtonStyleSheet);
311
- styles.push(MessageStateButtonStyleSheet);
312
- return styles;
313
- }
314
-
315
- /**
316
- * Programmatically triggers a click event on the button
317
- * Will not trigger if the button is disabled
318
- *
319
- * @since 3.27.0
320
- * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/click}
321
- * @fires monster-click
322
- */
323
- click() {
324
- if (this.getOption("disabled") === true) {
325
- return;
326
- }
327
-
328
- if (
329
- this[buttonElementSymbol] &&
330
- isFunction(this[buttonElementSymbol].click)
331
- ) {
332
- this[buttonElementSymbol].click();
333
- }
334
- }
335
-
336
- /**
337
- * The Button.focus() method sets focus on the internal button element.
338
- *
339
- * @since 3.27.0
340
- * @param {Object} options
341
- * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus}
342
- */
343
- focus(options) {
344
- if (this.getOption("disabled") === true) {
345
- return;
346
- }
347
-
348
- if (
349
- this[buttonElementSymbol] &&
350
- isFunction(this[buttonElementSymbol].focus)
351
- ) {
352
- this[buttonElementSymbol].focus(options);
353
- }
354
- }
355
-
356
- /**
357
- * The Button.blur() method removes focus from the internal button element.
358
- */
359
- blur() {
360
- if (
361
- this[buttonElementSymbol] &&
362
- isFunction(this[buttonElementSymbol].blur)
363
- ) {
364
- this[buttonElementSymbol].blur();
365
- }
366
- }
60
+ /**
61
+ * This method is called by the `instanceof` operator.
62
+ * @return {symbol}
63
+ */
64
+ static get [instanceSymbol]() {
65
+ return Symbol.for(
66
+ "@schukai/monster/components/form/message-state-button@@instance",
67
+ );
68
+ }
69
+
70
+ /**
71
+ * Sets the state of the button which affects its visual appearance
72
+ *
73
+ * @param {string} state - The state to set (e.g. 'success', 'error', 'loading')
74
+ * @param {number} timeout - Optional timeout in milliseconds after which state is removed
75
+ * @return {MessageStateButton} Returns the button instance for chaining
76
+ * @throws {TypeError} When state is not a string or timeout is not a number
77
+ */
78
+ setState(state, timeout) {
79
+ return this[buttonElementSymbol].setState(state, timeout);
80
+ }
81
+
82
+ /**
83
+ *
84
+ * @return {MessageStateButton}
85
+ */
86
+ removeState() {
87
+ return this[buttonElementSymbol].removeState();
88
+ }
89
+
90
+ /**
91
+ * @return {MessageStateButton|undefined}
92
+ */
93
+ getState() {
94
+ return this[buttonElementSymbol].getState();
95
+ }
96
+
97
+ /**
98
+ * To set the options via the HTML tag, the attribute `data-monster-options` must be used.
99
+ * @see {@link https://monsterjs.org/en/doc/#configurate-a-monster-control}
100
+ *
101
+ * The individual configuration values can be found in the table.
102
+ *
103
+ * @property {Object} templates Template definitions
104
+ * @property {string} templates.main Main template
105
+ * @property {Object} message Message definition
106
+ * @property {string|HTMLElement} message.content The message content
107
+ * @property {string} message.title The message title
108
+ * @property {string} message.icon The message icon
109
+ * @property {Object} message.width Width options for the message popper
110
+ * @property {string|number} message.width.min Minimum width (px, rem, em, vw)
111
+ * @property {string|number} message.width.max Maximum width (px, rem, em, vw)
112
+ * @property {number} message.width.viewportRatio Max width as ratio of viewport width (0-1)
113
+ * @property {string} mode The mode of the button, can be `manual` or `submit`
114
+ * @property {string} labels.button Button label
115
+ * @property {Object} classes Classes for internal elements
116
+ * @property {string} classes.button Button class
117
+ * @property {Object} actions Action callbacks
118
+ * @property {function} actions.click Action triggered on click
119
+ * @property {Object} aria Aria attributes
120
+ * @property {string} aria.role Aria role, only if the button is not a button
121
+ * @property {string} aria.label Aria label for the button
122
+ */
123
+ get defaults() {
124
+ return Object.assign({}, super.defaults, {
125
+ message: {
126
+ title: undefined,
127
+ content: undefined,
128
+ icon: undefined,
129
+ width: {
130
+ min: "12rem",
131
+ max: "32rem",
132
+ viewportRatio: 0.7,
133
+ },
134
+ },
135
+ templates: {
136
+ main: getTemplate(),
137
+ },
138
+ mode: "manual",
139
+ labels: {
140
+ button: "<slot></slot>",
141
+ },
142
+ classes: {
143
+ button: "monster-button-outline-primary",
144
+ },
145
+ actions: {
146
+ click: (e) => {},
147
+ },
148
+
149
+ aria: {
150
+ role: null,
151
+ label: null,
152
+ },
153
+ });
154
+ }
155
+
156
+ /**
157
+ * @return {void}
158
+ */
159
+ [assembleMethodSymbol]() {
160
+ super[assembleMethodSymbol]();
161
+ initControlReferences.call(this);
162
+ initDisabledSync.call(this);
163
+
164
+ let modes = null;
165
+ const modeOption = this.getOption("mode");
166
+ if (typeof modeOption === "string") {
167
+ modes = modeOption.split(" ");
168
+ }
169
+
170
+ if (
171
+ modes === null ||
172
+ modes === undefined ||
173
+ isArray(modes) === false ||
174
+ modes.length === 0
175
+ ) {
176
+ modes = ["manual"];
177
+ }
178
+
179
+ for (const [, mode] of Object.entries(modes)) {
180
+ initEventHandlerByMode.call(this, mode);
181
+ }
182
+ }
183
+
184
+ /**
185
+ * Sets the message content to be displayed in the popup overlay
186
+ *
187
+ * @param {string|HTMLElement} message - The message content as string or HTML element
188
+ * @param {string} title - Optional title to show above the message
189
+ * @param {string} icon - Optional icon HTML to display next to the title
190
+ * @return {MessageStateButton} Returns the button instance for chaining
191
+ * @throws {TypeError} When message is empty or invalid type
192
+ */
193
+ setMessage(message, title, icon) {
194
+ if (isString(message)) {
195
+ if (message === "") {
196
+ throw new TypeError("message must not be empty");
197
+ }
198
+
199
+ const containerDiv = document.createElement("div");
200
+ const messageDiv = document.createElement("div");
201
+ const titleDiv = document.createElement("div");
202
+ titleDiv.setAttribute(ATTRIBUTE_ROLE, "message-title-box");
203
+
204
+ let titleElement, iconElement;
205
+ if (title !== undefined) {
206
+ title = validateString(title);
207
+ titleElement = document.createElement("div");
208
+ titleElement.setAttribute("class", "");
209
+ titleElement.innerHTML = title;
210
+ titleElement.setAttribute(ATTRIBUTE_ROLE, "message-title");
211
+ titleDiv.appendChild(titleElement);
212
+ }
213
+
214
+ if (icon !== undefined) {
215
+ icon = validateString(icon);
216
+ iconElement = document.createElement("div");
217
+ iconElement.setAttribute("class", "");
218
+ iconElement.innerHTML = icon;
219
+ iconElement.setAttribute(ATTRIBUTE_ROLE, "message-icon");
220
+ titleDiv.appendChild(iconElement);
221
+ }
222
+
223
+ messageDiv.innerHTML = message;
224
+ containerDiv.appendChild(titleDiv);
225
+ containerDiv.appendChild(messageDiv);
226
+
227
+ this.setOption("message.content", containerDiv);
228
+ } else if (message instanceof HTMLElement) {
229
+ this.setOption("message.content", message);
230
+ } else {
231
+ throw new TypeError(
232
+ "message must be a string or an instance of HTMLElement",
233
+ );
234
+ }
235
+
236
+ return this;
237
+ }
238
+
239
+ /**
240
+ * clears the Message
241
+ *
242
+ * @return {MessageStateButton}
243
+ */
244
+ clearMessage() {
245
+ this.setOption("message.title", undefined);
246
+ this.setOption("message.content", undefined);
247
+ this.setOption("message.icon", undefined);
248
+ return this;
249
+ }
250
+
251
+ /**
252
+ * Shows the message popup overlay with optional auto-hide timeout
253
+ *
254
+ * @param {number} timeout - Optional time in milliseconds after which the message will auto-hide
255
+ * @return {MessageStateButton} Returns the button instance for chaining
256
+ */
257
+ showMessage(timeout) {
258
+ applyMeasuredMessageWidth.call(this);
259
+ this.showDialog.call(this);
260
+
261
+ if (timeout !== undefined) {
262
+ setTimeout(() => {
263
+ super.hideDialog();
264
+ }, timeout);
265
+ }
266
+
267
+ return this;
268
+ }
269
+
270
+ /**
271
+ * With this method, you can show the popper.
272
+ *
273
+ * @return {MessageStateButton}
274
+ */
275
+ showDialog() {
276
+ if (this.getOption("message.content") === undefined) {
277
+ return;
278
+ }
279
+ super.showDialog();
280
+ return this;
281
+ }
282
+
283
+ /**
284
+ *
285
+ * @return {MessageStateButton}
286
+ */
287
+ hideMessage() {
288
+ super.hideDialog();
289
+ return this;
290
+ }
291
+
292
+ /**
293
+ *
294
+ * @return {MessageStateButton}
295
+ */
296
+ toggleMessage() {
297
+ super.toggleDialog();
298
+ return this;
299
+ }
300
+
301
+ /**
302
+ *
303
+ * @return {Object}
304
+ */
305
+ getMessage() {
306
+ return this.getOption("message");
307
+ }
308
+
309
+ /**
310
+ *
311
+ * @return {string}
312
+ */
313
+ static getTag() {
314
+ return "monster-message-state-button";
315
+ }
316
+
317
+ /**
318
+ *
319
+ * @return {CSSStyleSheet[]}
320
+ */
321
+ static getCSSStyleSheet() {
322
+ const styles = Popper.getCSSStyleSheet();
323
+ styles.push(StateButtonStyleSheet);
324
+ styles.push(MessageStateButtonStyleSheet);
325
+ return styles;
326
+ }
327
+
328
+ /**
329
+ * Programmatically triggers a click event on the button
330
+ * Will not trigger if the button is disabled
331
+ *
332
+ * @since 3.27.0
333
+ * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/click}
334
+ * @fires monster-click
335
+ */
336
+ click() {
337
+ if (this.getOption("disabled") === true) {
338
+ return;
339
+ }
340
+
341
+ if (
342
+ this[buttonElementSymbol] &&
343
+ isFunction(this[buttonElementSymbol].click)
344
+ ) {
345
+ this[buttonElementSymbol].click();
346
+ }
347
+ }
348
+
349
+ /**
350
+ * The Button.focus() method sets focus on the internal button element.
351
+ *
352
+ * @since 3.27.0
353
+ * @param {Object} options
354
+ * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus}
355
+ */
356
+ focus(options) {
357
+ if (this.getOption("disabled") === true) {
358
+ return;
359
+ }
360
+
361
+ if (
362
+ this[buttonElementSymbol] &&
363
+ isFunction(this[buttonElementSymbol].focus)
364
+ ) {
365
+ this[buttonElementSymbol].focus(options);
366
+ }
367
+ }
368
+
369
+ /**
370
+ * The Button.blur() method removes focus from the internal button element.
371
+ */
372
+ blur() {
373
+ if (
374
+ this[buttonElementSymbol] &&
375
+ isFunction(this[buttonElementSymbol].blur)
376
+ ) {
377
+ this[buttonElementSymbol].blur();
378
+ }
379
+ }
367
380
  }
368
381
 
369
382
  /**
@@ -371,27 +384,27 @@ class MessageStateButton extends Popper {
371
384
  * @param mode
372
385
  */
373
386
  function initEventHandlerByMode(mode) {
374
- switch (mode) {
375
- case "manual":
376
- this[buttonElementSymbol].setOption("actions.click", (e) => {
377
- const callback = this.getOption("actions.click");
378
- if (isFunction(callback)) {
379
- callback(e);
380
- }
381
- });
382
-
383
- break;
384
- case "submit":
385
- this[buttonElementSymbol].setOption("actions.click", (e) => {
386
- const form = this.form;
387
-
388
- if (form instanceof HTMLFormElement) {
389
- form.requestSubmit();
390
- }
391
- });
392
-
393
- break;
394
- }
387
+ switch (mode) {
388
+ case "manual":
389
+ this[buttonElementSymbol].setOption("actions.click", (e) => {
390
+ const callback = this.getOption("actions.click");
391
+ if (isFunction(callback)) {
392
+ callback(e);
393
+ }
394
+ });
395
+
396
+ break;
397
+ case "submit":
398
+ this[buttonElementSymbol].setOption("actions.click", (e) => {
399
+ const form = this.form;
400
+
401
+ if (form instanceof HTMLFormElement) {
402
+ form.requestSubmit();
403
+ }
404
+ });
405
+
406
+ break;
407
+ }
395
408
  }
396
409
 
397
410
  /**
@@ -399,83 +412,83 @@ function initEventHandlerByMode(mode) {
399
412
  * @return {Select}
400
413
  */
401
414
  function initControlReferences() {
402
- this[buttonElementSymbol] = this.shadowRoot.querySelector(
403
- `[${ATTRIBUTE_ROLE}=button]`,
404
- );
405
- this[popperElementSymbol] = this.shadowRoot.querySelector(
406
- `[${ATTRIBUTE_ROLE}=popper]`,
407
- );
408
- this[messageElementSymbol] = this.shadowRoot.querySelector(
409
- `[${ATTRIBUTE_ROLE}=message]`,
410
- );
415
+ this[buttonElementSymbol] = this.shadowRoot.querySelector(
416
+ `[${ATTRIBUTE_ROLE}=button]`,
417
+ );
418
+ this[popperElementSymbol] = this.shadowRoot.querySelector(
419
+ `[${ATTRIBUTE_ROLE}=popper]`,
420
+ );
421
+ this[messageElementSymbol] = this.shadowRoot.querySelector(
422
+ `[${ATTRIBUTE_ROLE}=message]`,
423
+ );
411
424
  }
412
425
 
413
426
  /**
414
427
  * @private
415
428
  */
416
429
  function initDisabledSync() {
417
- const self = this;
418
- const attachInnerObserver = (button) => {
419
- if (!button || !isFunction(button.attachObserver)) {
420
- return;
421
- }
422
-
423
- const existing = self[innerDisabledObserverSymbol];
424
- if (existing?.button === button) {
425
- return;
426
- }
427
-
428
- if (
429
- existing?.button &&
430
- isFunction(existing.button.detachObserver) &&
431
- existing.observer
432
- ) {
433
- existing.button.detachObserver(existing.observer);
434
- }
435
-
436
- const observer = new Observer(syncDisabled);
437
- button.attachObserver(observer);
438
- self[innerDisabledObserverSymbol] = { button, observer };
439
- };
440
-
441
- const syncDisabled = () => {
442
- const disabled = self.getOption("disabled", false);
443
- const button =
444
- self.shadowRoot?.querySelector(`[${ATTRIBUTE_ROLE}=button]`) ??
445
- self[buttonElementSymbol];
446
- if (!button) {
447
- return;
448
- }
449
-
450
- self[buttonElementSymbol] = button;
451
-
452
- if (disabled) {
453
- button.setAttribute(ATTRIBUTE_DISABLED, "");
454
- } else {
455
- button.removeAttribute(ATTRIBUTE_DISABLED);
456
- }
457
-
458
- if (isFunction(button.setOption)) {
459
- button.setOption("disabled", disabled);
460
- }
461
-
462
- attachInnerObserver(button);
463
- };
464
-
465
- syncDisabled();
466
- const existingObserver = self[attributeObserverSymbol]?.[ATTRIBUTE_DISABLED];
467
- if (existingObserver) {
468
- self[attributeObserverSymbol][ATTRIBUTE_DISABLED] = () => {
469
- existingObserver.call(self);
470
- syncDisabled();
471
- };
472
- }
473
- self.attachObserver(new Observer(syncDisabled));
474
- if (typeof customElements?.whenDefined === "function") {
475
- customElements.whenDefined("monster-state-button").then(() => {
476
- syncDisabled();
477
- });
478
- }
430
+ const self = this;
431
+ const attachInnerObserver = (button) => {
432
+ if (!button || !isFunction(button.attachObserver)) {
433
+ return;
434
+ }
435
+
436
+ const existing = self[innerDisabledObserverSymbol];
437
+ if (existing?.button === button) {
438
+ return;
439
+ }
440
+
441
+ if (
442
+ existing?.button &&
443
+ isFunction(existing.button.detachObserver) &&
444
+ existing.observer
445
+ ) {
446
+ existing.button.detachObserver(existing.observer);
447
+ }
448
+
449
+ const observer = new Observer(syncDisabled);
450
+ button.attachObserver(observer);
451
+ self[innerDisabledObserverSymbol] = { button, observer };
452
+ };
453
+
454
+ const syncDisabled = () => {
455
+ const disabled = self.getOption("disabled", false);
456
+ const button =
457
+ self.shadowRoot?.querySelector(`[${ATTRIBUTE_ROLE}=button]`) ??
458
+ self[buttonElementSymbol];
459
+ if (!button) {
460
+ return;
461
+ }
462
+
463
+ self[buttonElementSymbol] = button;
464
+
465
+ if (disabled) {
466
+ button.setAttribute(ATTRIBUTE_DISABLED, "");
467
+ } else {
468
+ button.removeAttribute(ATTRIBUTE_DISABLED);
469
+ }
470
+
471
+ if (isFunction(button.setOption)) {
472
+ button.setOption("disabled", disabled);
473
+ }
474
+
475
+ attachInnerObserver(button);
476
+ };
477
+
478
+ syncDisabled();
479
+ const existingObserver = self[attributeObserverSymbol]?.[ATTRIBUTE_DISABLED];
480
+ if (existingObserver) {
481
+ self[attributeObserverSymbol][ATTRIBUTE_DISABLED] = () => {
482
+ existingObserver.call(self);
483
+ syncDisabled();
484
+ };
485
+ }
486
+ self.attachObserver(new Observer(syncDisabled));
487
+ if (typeof customElements?.whenDefined === "function") {
488
+ customElements.whenDefined("monster-state-button").then(() => {
489
+ syncDisabled();
490
+ });
491
+ }
479
492
  }
480
493
 
481
494
  /**
@@ -483,8 +496,8 @@ function initDisabledSync() {
483
496
  * @return {string}
484
497
  */
485
498
  function getTemplate() {
486
- // language=HTML
487
- return `
499
+ // language=HTML
500
+ return `
488
501
  <div data-monster-role="control" part="control">
489
502
 
490
503
  <monster-state-button exportparts="button:button-button,control:button-control"
@@ -515,15 +528,15 @@ function getTemplate() {
515
528
  * @return {HTMLElement|string|null}
516
529
  */
517
530
  function getMeasurementContent(content) {
518
- if (isString(content)) {
519
- return content;
520
- }
531
+ if (isString(content)) {
532
+ return content;
533
+ }
521
534
 
522
- if (content instanceof HTMLElement) {
523
- return content.cloneNode(true);
524
- }
535
+ if (content instanceof HTMLElement) {
536
+ return content.cloneNode(true);
537
+ }
525
538
 
526
- return null;
539
+ return null;
527
540
  }
528
541
 
529
542
  /**
@@ -531,84 +544,159 @@ function getMeasurementContent(content) {
531
544
  * @return {{popper: HTMLElement, message: HTMLElement}|null}
532
545
  */
533
546
  function ensureMeasurementPopper() {
534
- if (this[measurementPopperSymbol]) {
535
- return this[measurementPopperSymbol];
536
- }
537
-
538
- if (!this.shadowRoot) {
539
- return null;
540
- }
541
-
542
- const popper = document.createElement("div");
543
- popper.setAttribute(ATTRIBUTE_ROLE, "popper");
544
- popper.setAttribute("data-measurement", "true");
545
- popper.setAttribute("aria-hidden", "true");
546
- popper.style.position = "absolute";
547
- popper.style.left = "-10000px";
548
- popper.style.top = "-10000px";
549
- popper.style.visibility = "hidden";
550
- popper.style.display = "block";
551
- popper.style.pointerEvents = "none";
552
- popper.style.maxWidth = "none";
553
- popper.style.width = "max-content";
554
-
555
- const message = document.createElement("div");
556
- message.setAttribute(ATTRIBUTE_ROLE, "message");
557
- message.className = "flex";
558
-
559
- popper.appendChild(message);
560
- this.shadowRoot.appendChild(popper);
561
-
562
- this[measurementPopperSymbol] = { popper, message };
563
- return this[measurementPopperSymbol];
547
+ if (this[measurementPopperSymbol]) {
548
+ return this[measurementPopperSymbol];
549
+ }
550
+
551
+ if (!this.shadowRoot) {
552
+ return null;
553
+ }
554
+
555
+ const popper = document.createElement("div");
556
+ popper.setAttribute(ATTRIBUTE_ROLE, "popper");
557
+ popper.setAttribute("data-measurement", "true");
558
+ popper.setAttribute("aria-hidden", "true");
559
+ popper.style.position = "absolute";
560
+ popper.style.left = "-10000px";
561
+ popper.style.top = "-10000px";
562
+ popper.style.visibility = "hidden";
563
+ popper.style.display = "block";
564
+ popper.style.pointerEvents = "none";
565
+ popper.style.maxWidth = "none";
566
+ popper.style.width = "max-content";
567
+ if (this[popperElementSymbol]?.className) {
568
+ popper.className = this[popperElementSymbol].className;
569
+ }
570
+
571
+ const message = document.createElement("div");
572
+ message.setAttribute(ATTRIBUTE_ROLE, "message");
573
+ message.className = "flex";
574
+
575
+ popper.appendChild(message);
576
+ this.shadowRoot.appendChild(popper);
577
+
578
+ this[measurementPopperSymbol] = { popper, message };
579
+ return this[measurementPopperSymbol];
564
580
  }
565
581
 
566
582
  /**
567
583
  * @private
568
584
  */
569
585
  function applyMeasuredMessageWidth() {
570
- const popper = this[popperElementSymbol];
571
- if (!popper) {
572
- return;
573
- }
574
-
575
- const content = this.getOption("message.content");
576
- const measureContent = getMeasurementContent(content);
577
- if (!measureContent) {
578
- return;
579
- }
580
-
581
- const measurement = ensureMeasurementPopper.call(this);
582
- if (!measurement?.message) {
583
- return;
584
- }
585
-
586
- measurement.message.innerHTML = "";
587
- if (isString(measureContent)) {
588
- measurement.message.innerHTML = measureContent;
589
- } else {
590
- measurement.message.appendChild(measureContent);
591
- }
592
-
593
- const measuredWidth = Math.ceil(
594
- measurement.popper.getBoundingClientRect().width,
595
- );
596
- const fontSize = parseFloat(getComputedStyle(popper).fontSize) || 16;
597
- const minWidth = Math.round(fontSize * 12);
598
- const maxWidth = Math.max(
599
- minWidth,
600
- Math.min(window.innerWidth * 0.7, fontSize * 32),
601
- );
602
- const targetWidth = Math.max(
603
- minWidth,
604
- Math.min(measuredWidth || minWidth, maxWidth),
605
- );
606
-
607
- popper.style.width = `${targetWidth}px`;
608
- popper.style.minWidth = `${minWidth}px`;
609
- popper.style.maxWidth = `${maxWidth}px`;
610
- popper.style.whiteSpace = "normal";
611
- popper.style.overflowWrap = "anywhere";
586
+ const popper = this[popperElementSymbol];
587
+ if (!popper) {
588
+ return;
589
+ }
590
+
591
+ const content = this.getOption("message.content");
592
+ const measureContent = getMeasurementContent(content);
593
+ if (!measureContent) {
594
+ return;
595
+ }
596
+
597
+ const measurement = ensureMeasurementPopper.call(this);
598
+ if (!measurement?.message) {
599
+ return;
600
+ }
601
+
602
+ if (popper.className && measurement.popper.className !== popper.className) {
603
+ measurement.popper.className = popper.className;
604
+ }
605
+
606
+ measurement.message.innerHTML = "";
607
+ if (isString(measureContent)) {
608
+ measurement.message.innerHTML = measureContent;
609
+ } else {
610
+ measurement.message.appendChild(measureContent);
611
+ }
612
+
613
+ const measuredWidth = Math.ceil(
614
+ measurement.popper.getBoundingClientRect().width,
615
+ );
616
+ const fontSize = parseFloat(getComputedStyle(popper).fontSize) || 16;
617
+ const rootFontSize =
618
+ parseFloat(getComputedStyle(document.documentElement).fontSize) || 16;
619
+ const widthOptions = this.getOption("message.width", {});
620
+ const minWidthOption = resolveLength(
621
+ widthOptions?.min,
622
+ fontSize,
623
+ rootFontSize,
624
+ window.innerWidth,
625
+ );
626
+ const maxWidthOption = resolveLength(
627
+ widthOptions?.max,
628
+ fontSize,
629
+ rootFontSize,
630
+ window.innerWidth,
631
+ );
632
+ const viewportRatio =
633
+ typeof widthOptions?.viewportRatio === "number" &&
634
+ widthOptions.viewportRatio > 0 &&
635
+ widthOptions.viewportRatio <= 1
636
+ ? widthOptions.viewportRatio
637
+ : 0.7;
638
+
639
+ const minWidth = Math.max(0, minWidthOption ?? Math.round(fontSize * 12));
640
+ const maxViewportWidth = Math.max(
641
+ minWidth,
642
+ window.innerWidth * viewportRatio,
643
+ );
644
+ const maxWidth = Math.max(
645
+ minWidth,
646
+ Math.min(maxWidthOption ?? fontSize * 32, maxViewportWidth),
647
+ );
648
+ const targetWidth = Math.max(
649
+ minWidth,
650
+ Math.min(measuredWidth || minWidth, maxWidth),
651
+ );
652
+
653
+ popper.style.width = `${targetWidth}px`;
654
+ popper.style.minWidth = `${minWidth}px`;
655
+ popper.style.maxWidth = `${maxWidth}px`;
656
+ popper.style.whiteSpace = "normal";
657
+ popper.style.overflowWrap = "anywhere";
658
+ }
659
+
660
+ /**
661
+ * @private
662
+ * @param {unknown} value
663
+ * @param {number} fontSize
664
+ * @param {number} rootFontSize
665
+ * @param {number} viewportWidth
666
+ * @return {number|null}
667
+ */
668
+ function resolveLength(value, fontSize, rootFontSize, viewportWidth) {
669
+ if (typeof value === "number" && Number.isFinite(value)) {
670
+ return value;
671
+ }
672
+
673
+ if (!isString(value)) {
674
+ return null;
675
+ }
676
+
677
+ const trimmed = value.trim();
678
+ const number = parseFloat(trimmed);
679
+ if (!Number.isFinite(number)) {
680
+ return null;
681
+ }
682
+
683
+ if (trimmed.endsWith("rem")) {
684
+ return number * rootFontSize;
685
+ }
686
+
687
+ if (trimmed.endsWith("em")) {
688
+ return number * fontSize;
689
+ }
690
+
691
+ if (trimmed.endsWith("vw")) {
692
+ return (number / 100) * viewportWidth;
693
+ }
694
+
695
+ if (trimmed.endsWith("px")) {
696
+ return number;
697
+ }
698
+
699
+ return number;
612
700
  }
613
701
 
614
702
  registerCustomElement(MessageStateButton);
@@ -72,6 +72,7 @@ class Panel extends CustomElement {
72
72
  * @property {Object} templates Template definitions
73
73
  * @property {string} templates.main Main template
74
74
  * @property {string} heightAdjustment Height adjustment
75
+ * @property {string} controlOverflow Overflow value applied to the control element
75
76
  */
76
77
  get defaults() {
77
78
  return Object.assign({}, super.defaults, {
@@ -79,6 +80,7 @@ class Panel extends CustomElement {
79
80
  main: getTemplate(),
80
81
  },
81
82
  heightAdjustment: 4,
83
+ controlOverflow: "auto",
82
84
  });
83
85
  }
84
86
 
@@ -91,6 +93,7 @@ class Panel extends CustomElement {
91
93
 
92
94
  initControlReferences.call(this);
93
95
  initEventHandler.call(this);
96
+ applyControlOverflow.call(this);
94
97
 
95
98
  calcHeight.call(this);
96
99
  }
@@ -272,6 +275,24 @@ function initEventHandler() {
272
275
  return this;
273
276
  }
274
277
 
278
+ /**
279
+ * @private
280
+ */
281
+ function applyControlOverflow() {
282
+ const control = this[PanelElementSymbol];
283
+ if (!(control instanceof HTMLElement)) {
284
+ return;
285
+ }
286
+
287
+ const overflow = this.getOption("controlOverflow");
288
+ if (overflow === null || overflow === undefined || overflow === "") {
289
+ control.style.removeProperty("overflow");
290
+ return;
291
+ }
292
+
293
+ control.style.overflow = overflow;
294
+ }
295
+
275
296
  /**
276
297
  * @private
277
298
  * @return {string}