@schukai/monster 4.46.1 → 4.46.3

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.46.3] - 2025-11-19
6
+
7
+ ### Bug Fixes
8
+
9
+ - **quantity:** max boundary
10
+
11
+
12
+
13
+ ## [4.46.2] - 2025-11-19
14
+
15
+ ### Bug Fixes
16
+
17
+ - field-set-alignment should be changeable
18
+
19
+
20
+
5
21
  ## [4.46.1] - 2025-11-18
6
22
 
7
23
  ### Bug Fixes
package/package.json CHANGED
@@ -1 +1 @@
1
- {"author":"schukai GmbH","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.46.1"}
1
+ {"author":"schukai GmbH","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.46.3"}
@@ -15,14 +15,14 @@
15
15
  import { instanceSymbol } from "../../constants.mjs";
16
16
  import { addAttributeToken } from "../../dom/attributes.mjs";
17
17
  import {
18
- ATTRIBUTE_ERRORMESSAGE,
19
- ATTRIBUTE_ROLE,
18
+ ATTRIBUTE_ERRORMESSAGE,
19
+ ATTRIBUTE_ROLE,
20
20
  } from "../../dom/constants.mjs";
21
21
  import { CustomControl } from "../../dom/customcontrol.mjs";
22
22
  import {
23
- assembleMethodSymbol,
24
- getSlottedElements,
25
- registerCustomElement,
23
+ assembleMethodSymbol,
24
+ getSlottedElements,
25
+ registerCustomElement,
26
26
  } from "../../dom/customelement.mjs";
27
27
  import { isFunction } from "../../types/is.mjs";
28
28
  import { FieldSetStyleSheet } from "./stylesheet/field-set.mjs";
@@ -82,166 +82,168 @@ const extendedSwitchElementSymbol = Symbol("extendedSwitchElement");
82
82
  * @summary A field set control
83
83
  */
84
84
  class FieldSet extends CustomControl {
85
- /**
86
- * This method is called by the `instanceof` operator.
87
- * @return {symbol}
88
- */
89
- static get [instanceSymbol]() {
90
- return Symbol.for("@schukai/monster/components/form/fieldset@@instance");
91
- }
92
-
93
- /**
94
- * @return {Components.Form.FieldSet
95
- */
96
- [assembleMethodSymbol]() {
97
- super[assembleMethodSymbol]();
98
- initControlReferences.call(this);
99
- initEventHandler.call(this);
100
- updateExtendedFields.call(this);
101
- updateColumns.call(this);
102
- return this;
103
- }
104
-
105
- /**
106
- * To set the options via the HTML tag, the attribute `data-monster-options` must be used.
107
- * @see {@link https://monsterjs.org/en/doc/#configurate-a-monster-control}
108
- *
109
- * The individual configuration values can be found in the table.
110
- *
111
- * @property {Object} templates Template definitions
112
- * @property {string} templates.main Main template
113
- * @property {Object} labels Label definitions
114
- * @property {Object} actions Callbacks
115
- * @property {string} actions.click="throw Error" Callback when clicked
116
- * @property {Object} features Features
117
- * @property {boolean} features.multipleColumns=true Multiple columns
118
- * @property {Object} classes CSS classes
119
- * @property {boolean} disabled=false Disabled state
120
- */
121
- get defaults() {
122
- return Object.assign({}, super.defaults, {
123
- templates: {
124
- main: getTemplate(),
125
- },
126
- labels: getTranslations(),
127
- classes: {},
128
- disabled: false,
129
- features: {
130
- multipleColumns: true,
131
- },
132
- actions: {
133
- click: () => {},
134
- },
135
- value: null,
136
- });
137
- }
138
-
139
- /**
140
- *
141
- * @return {string}
142
- */
143
- static getTag() {
144
- return "monster-field-set";
145
- }
146
-
147
- /**
148
- *
149
- * @return {CSSStyleSheet[]}
150
- */
151
- static getCSSStyleSheet() {
152
- return [FieldSetStyleSheet, InvalidStyleSheet];
153
- }
154
-
155
- /**
156
- * The FieldSet.click() method simulates a click on the internal element.
157
- *
158
- * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/click}
159
- */
160
- click() {
161
- if (this.getOption("disabled") === true) {
162
- return;
163
- }
164
-
165
- if (
166
- this[fieldSetElementSymbol] &&
167
- isFunction(this[fieldSetElementSymbol].click)
168
- ) {
169
- this[fieldSetElementSymbol].click();
170
- }
171
- }
172
-
173
- /**
174
- * The Button.focus() method sets focus on the internal element.
175
- *
176
- * @param {Object} options
177
- * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus}
178
- */
179
- focus(options) {
180
- if (this.getOption("disabled") === true) {
181
- return;
182
- }
183
-
184
- if (
185
- this[fieldSetElementSymbol] &&
186
- isFunction(this[fieldSetElementSymbol].focus)
187
- ) {
188
- this[fieldSetElementSymbol].focus(options);
189
- }
190
- }
191
-
192
- /**
193
- * The Button.blur() method removes focus from the internal element.
194
- */
195
- blur() {
196
- if (
197
- this[fieldSetElementSymbol] &&
198
- isFunction(this[fieldSetElementSymbol].blur)
199
- ) {
200
- this[fieldSetElementSymbol].blur();
201
- }
202
- }
203
-
204
- /**
205
- * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/attachInternals}
206
- * @return {boolean}
207
- */
208
- static get formAssociated() {
209
- return true;
210
- }
211
-
212
- /**
213
- * The current value of the form control.
214
- *
215
- * ```js
216
- * e = document.querySelector('monster-field-set');
217
- * console.log(e.value)
218
- * ```
219
- *
220
- * @property {string}
221
- */
222
- get value() {
223
- return this.getOption("value");
224
- }
225
-
226
- /**
227
- * Set the value of the form control.
228
- *
229
- * ```
230
- * e = document.querySelector('monster-field-set');
231
- * e.value=1
232
- * ```
233
- *
234
- * @property {string} value
235
- * @throws {Error} unsupported type
236
- */
237
- set value(value) {
238
- this.setOption("value", value);
239
- try {
240
- this?.setFormValue(this.value);
241
- } catch (e) {
242
- addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, e.message);
243
- }
244
- }
85
+ /**
86
+ * This method is called by the `instanceof` operator.
87
+ * @return {symbol}
88
+ */
89
+ static get [instanceSymbol]() {
90
+ return Symbol.for("@schukai/monster/components/form/fieldset@@instance");
91
+ }
92
+
93
+ /**
94
+ * @return {Components.Form.FieldSet
95
+ */
96
+ [assembleMethodSymbol]() {
97
+ super[assembleMethodSymbol]();
98
+ initControlReferences.call(this);
99
+ initEventHandler.call(this);
100
+ updateExtendedFields.call(this);
101
+ updateColumns.call(this);
102
+ return this;
103
+ }
104
+
105
+ /**
106
+ * To set the options via the HTML tag, the attribute `data-monster-options` must be used.
107
+ * @see {@link https://monsterjs.org/en/doc/#configurate-a-monster-control}
108
+ *
109
+ * The individual configuration values can be found in the table.
110
+ *
111
+ * @property {Object} templates Template definitions
112
+ * @property {string} templates.main Main template
113
+ * @property {Object} labels Label definitions
114
+ * @property {Object} actions Callbacks
115
+ * @property {string} actions.click="throw Error" Callback when clicked
116
+ * @property {Object} features Features
117
+ * @property {boolean} features.multipleColumns=true Multiple columns
118
+ * @property {Object} classes CSS classes
119
+ * @property {boolean} disabled=false Disabled state
120
+ */
121
+ get defaults() {
122
+ return Object.assign({}, super.defaults, {
123
+ templates: {
124
+ main: getTemplate(),
125
+ },
126
+ labels: getTranslations(),
127
+ classes: {
128
+ content: "collapse-alignment",
129
+ },
130
+ disabled: false,
131
+ features: {
132
+ multipleColumns: true,
133
+ },
134
+ actions: {
135
+ click: () => {},
136
+ },
137
+ value: null,
138
+ });
139
+ }
140
+
141
+ /**
142
+ *
143
+ * @return {string}
144
+ */
145
+ static getTag() {
146
+ return "monster-field-set";
147
+ }
148
+
149
+ /**
150
+ *
151
+ * @return {CSSStyleSheet[]}
152
+ */
153
+ static getCSSStyleSheet() {
154
+ return [FieldSetStyleSheet, InvalidStyleSheet];
155
+ }
156
+
157
+ /**
158
+ * The FieldSet.click() method simulates a click on the internal element.
159
+ *
160
+ * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/click}
161
+ */
162
+ click() {
163
+ if (this.getOption("disabled") === true) {
164
+ return;
165
+ }
166
+
167
+ if (
168
+ this[fieldSetElementSymbol] &&
169
+ isFunction(this[fieldSetElementSymbol].click)
170
+ ) {
171
+ this[fieldSetElementSymbol].click();
172
+ }
173
+ }
174
+
175
+ /**
176
+ * The Button.focus() method sets focus on the internal element.
177
+ *
178
+ * @param {Object} options
179
+ * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus}
180
+ */
181
+ focus(options) {
182
+ if (this.getOption("disabled") === true) {
183
+ return;
184
+ }
185
+
186
+ if (
187
+ this[fieldSetElementSymbol] &&
188
+ isFunction(this[fieldSetElementSymbol].focus)
189
+ ) {
190
+ this[fieldSetElementSymbol].focus(options);
191
+ }
192
+ }
193
+
194
+ /**
195
+ * The Button.blur() method removes focus from the internal element.
196
+ */
197
+ blur() {
198
+ if (
199
+ this[fieldSetElementSymbol] &&
200
+ isFunction(this[fieldSetElementSymbol].blur)
201
+ ) {
202
+ this[fieldSetElementSymbol].blur();
203
+ }
204
+ }
205
+
206
+ /**
207
+ * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/attachInternals}
208
+ * @return {boolean}
209
+ */
210
+ static get formAssociated() {
211
+ return true;
212
+ }
213
+
214
+ /**
215
+ * The current value of the form control.
216
+ *
217
+ * ```js
218
+ * e = document.querySelector('monster-field-set');
219
+ * console.log(e.value)
220
+ * ```
221
+ *
222
+ * @property {string}
223
+ */
224
+ get value() {
225
+ return this.getOption("value");
226
+ }
227
+
228
+ /**
229
+ * Set the value of the form control.
230
+ *
231
+ * ```
232
+ * e = document.querySelector('monster-field-set');
233
+ * e.value=1
234
+ * ```
235
+ *
236
+ * @property {string} value
237
+ * @throws {Error} unsupported type
238
+ */
239
+ set value(value) {
240
+ this.setOption("value", value);
241
+ try {
242
+ this?.setFormValue(this.value);
243
+ } catch (e) {
244
+ addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, e.message);
245
+ }
246
+ }
245
247
  }
246
248
 
247
249
  /**
@@ -249,97 +251,97 @@ class FieldSet extends CustomControl {
249
251
  * @returns {object}
250
252
  */
251
253
  function getTranslations() {
252
- const locale = getLocaleOfDocument();
253
- switch (locale.language) {
254
- case "de":
255
- return {
256
- toggleSwitchOn: "✔",
257
- toggleSwitchOff: "✖",
258
- toggleSwitchLabel: "Erweitern",
259
- title: "",
260
- };
261
- case "fr":
262
- return {
263
- toggleSwitchOn: "✔",
264
- toggleSwitchOff: "✖",
265
- toggleSwitchLabel: "Développer",
266
- title: "",
267
- };
268
- case "sp":
269
- return {
270
- toggleSwitchOn: "✔",
271
- toggleSwitchOff: "✖",
272
- toggleSwitchLabel: "Expandir",
273
- title: "",
274
- };
275
- case "it":
276
- return {
277
- toggleSwitchOn: "✔",
278
- toggleSwitchOff: "✖",
279
- toggleSwitchLabel: "Espandi",
280
- title: "",
281
- };
282
- case "pl":
283
- return {
284
- toggleSwitchOn: "✔",
285
- toggleSwitchOff: "✖",
286
- toggleSwitchLabel: "Rozwiń",
287
- title: "",
288
- };
289
- case "no":
290
- return {
291
- toggleSwitchOn: "✔",
292
- toggleSwitchOff: "✖",
293
- toggleSwitchLabel: "Utvid",
294
- title: "",
295
- };
296
- case "dk":
297
- return {
298
- toggleSwitchOn: "✔",
299
- toggleSwitchOff: "✖",
300
- toggleSwitchLabel: "Udvid",
301
- title: "",
302
- };
303
- case "sw":
304
- return {
305
- toggleSwitchOn: "✔",
306
- toggleSwitchOff: "✖",
307
- toggleSwitchLabel: "Expandera",
308
- title: "",
309
- };
310
- default:
311
- case "en":
312
- return {
313
- toggleSwitchOn: "✔",
314
- toggleSwitchOff: "✖",
315
- toggleSwitchLabel: "Expand",
316
- title: "",
317
- };
318
- }
254
+ const locale = getLocaleOfDocument();
255
+ switch (locale.language) {
256
+ case "de":
257
+ return {
258
+ toggleSwitchOn: "✔",
259
+ toggleSwitchOff: "✖",
260
+ toggleSwitchLabel: "Erweitern",
261
+ title: "",
262
+ };
263
+ case "fr":
264
+ return {
265
+ toggleSwitchOn: "✔",
266
+ toggleSwitchOff: "✖",
267
+ toggleSwitchLabel: "Développer",
268
+ title: "",
269
+ };
270
+ case "sp":
271
+ return {
272
+ toggleSwitchOn: "✔",
273
+ toggleSwitchOff: "✖",
274
+ toggleSwitchLabel: "Expandir",
275
+ title: "",
276
+ };
277
+ case "it":
278
+ return {
279
+ toggleSwitchOn: "✔",
280
+ toggleSwitchOff: "✖",
281
+ toggleSwitchLabel: "Espandi",
282
+ title: "",
283
+ };
284
+ case "pl":
285
+ return {
286
+ toggleSwitchOn: "✔",
287
+ toggleSwitchOff: "✖",
288
+ toggleSwitchLabel: "Rozwiń",
289
+ title: "",
290
+ };
291
+ case "no":
292
+ return {
293
+ toggleSwitchOn: "✔",
294
+ toggleSwitchOff: "✖",
295
+ toggleSwitchLabel: "Utvid",
296
+ title: "",
297
+ };
298
+ case "dk":
299
+ return {
300
+ toggleSwitchOn: "✔",
301
+ toggleSwitchOff: "✖",
302
+ toggleSwitchLabel: "Udvid",
303
+ title: "",
304
+ };
305
+ case "sw":
306
+ return {
307
+ toggleSwitchOn: "✔",
308
+ toggleSwitchOff: "✖",
309
+ toggleSwitchLabel: "Expandera",
310
+ title: "",
311
+ };
312
+ default:
313
+ case "en":
314
+ return {
315
+ toggleSwitchOn: "✔",
316
+ toggleSwitchOff: "✖",
317
+ toggleSwitchLabel: "Expand",
318
+ title: "",
319
+ };
320
+ }
319
321
  }
320
322
 
321
323
  /**
322
324
  * @private
323
325
  */
324
326
  function updateExtendedFields() {
325
- const nodes = getSlottedElements.call(this, "", "extended");
326
- if (nodes.size > 0) {
327
- this[extendedSwitchSymbol].classList.remove("hidden");
328
- } else {
329
- this[extendedSwitchSymbol].classList.add("hidden");
330
- }
327
+ const nodes = getSlottedElements.call(this, "", "extended");
328
+ if (nodes.size > 0) {
329
+ this[extendedSwitchSymbol].classList.remove("hidden");
330
+ } else {
331
+ this[extendedSwitchSymbol].classList.add("hidden");
332
+ }
331
333
  }
332
334
 
333
335
  /**
334
336
  * @private
335
337
  */
336
338
  function updateColumns() {
337
- if (this.getOption("features.multipleColumns") !== true) {
338
- this[fieldSetElementSymbol].classList.remove("multiple-columns");
339
- return;
340
- }
339
+ if (this.getOption("features.multipleColumns") !== true) {
340
+ this[fieldSetElementSymbol].classList.remove("multiple-columns");
341
+ return;
342
+ }
341
343
 
342
- this[fieldSetElementSymbol].classList.add("multiple-columns");
344
+ this[fieldSetElementSymbol].classList.add("multiple-columns");
343
345
  }
344
346
 
345
347
  /**
@@ -348,53 +350,53 @@ function updateColumns() {
348
350
  * @fires monster-field-set-clicked
349
351
  */
350
352
  function initEventHandler() {
351
- this[toggleSwitchElementSymbol].setOption(
352
- "labels.toggleSwitchOn",
353
- this.getOption("labels.toggleSwitchOn"),
354
- );
355
- this[toggleSwitchElementSymbol].setOption(
356
- "labels.toggleSwitchOff",
357
- this.getOption("labels.toggleSwitchOff"),
358
- );
359
-
360
- this[toggleSwitchElementSymbol].setOption("actions.on", () => {
361
- this[collapseElementSymbol].open();
362
- });
363
-
364
- this[toggleSwitchElementSymbol].setOption("actions.off", () => {
365
- this[collapseElementSymbol].close();
366
- });
367
-
368
- return this;
353
+ this[toggleSwitchElementSymbol].setOption(
354
+ "labels.toggleSwitchOn",
355
+ this.getOption("labels.toggleSwitchOn"),
356
+ );
357
+ this[toggleSwitchElementSymbol].setOption(
358
+ "labels.toggleSwitchOff",
359
+ this.getOption("labels.toggleSwitchOff"),
360
+ );
361
+
362
+ this[toggleSwitchElementSymbol].setOption("actions.on", () => {
363
+ this[collapseElementSymbol].open();
364
+ });
365
+
366
+ this[toggleSwitchElementSymbol].setOption("actions.off", () => {
367
+ this[collapseElementSymbol].close();
368
+ });
369
+
370
+ return this;
369
371
  }
370
372
 
371
373
  /**
372
374
  * @private
373
375
  */
374
376
  function initControlReferences() {
375
- this[fieldSetElementSymbol] = this.shadowRoot.querySelector(
376
- `[${ATTRIBUTE_ROLE}="control"]`,
377
- );
377
+ this[fieldSetElementSymbol] = this.shadowRoot.querySelector(
378
+ `[${ATTRIBUTE_ROLE}="control"]`,
379
+ );
378
380
 
379
- this[extendedSwitchElementSymbol] = this.shadowRoot.querySelector(
380
- `[${ATTRIBUTE_ROLE}="extended-switch"]`,
381
- );
381
+ this[extendedSwitchElementSymbol] = this.shadowRoot.querySelector(
382
+ `[${ATTRIBUTE_ROLE}="extended-switch"]`,
383
+ );
382
384
 
383
- this[collapseElementSymbol] = this.shadowRoot.querySelector(
384
- `[${ATTRIBUTE_ROLE}="collapse"]`,
385
- );
385
+ this[collapseElementSymbol] = this.shadowRoot.querySelector(
386
+ `[${ATTRIBUTE_ROLE}="collapse"]`,
387
+ );
386
388
 
387
- this[headerElementSymbol] = this.shadowRoot.querySelector(
388
- `[${ATTRIBUTE_ROLE}="header"]`,
389
- );
389
+ this[headerElementSymbol] = this.shadowRoot.querySelector(
390
+ `[${ATTRIBUTE_ROLE}="header"]`,
391
+ );
390
392
 
391
- this[extendedSwitchSymbol] = this.shadowRoot.querySelector(
392
- `[${ATTRIBUTE_ROLE}="extended-switch"]`,
393
- );
393
+ this[extendedSwitchSymbol] = this.shadowRoot.querySelector(
394
+ `[${ATTRIBUTE_ROLE}="extended-switch"]`,
395
+ );
394
396
 
395
- this[toggleSwitchElementSymbol] = this.shadowRoot.querySelector(
396
- `monster-toggle-switch`,
397
- );
397
+ this[toggleSwitchElementSymbol] = this.shadowRoot.querySelector(
398
+ `monster-toggle-switch`,
399
+ );
398
400
  }
399
401
 
400
402
  /**
@@ -402,8 +404,8 @@ function initControlReferences() {
402
404
  * @return {string}
403
405
  */
404
406
  function getTemplate() {
405
- // language=HTML
406
- return `
407
+ // language=HTML
408
+ return `
407
409
  <div data-monster-role="control" part="control">
408
410
  <div data-monster-role="header" part="header">
409
411
  <div data-monster-replace="path:labels.title" data-monster-role="title" part="title"></div>
@@ -413,7 +415,8 @@ function getTemplate() {
413
415
  </div>
414
416
  </div>
415
417
  <div data-monster-role="container" part="container">
416
- <div class="collapse-alignment" part="content">
418
+ <div data-monster-attributes="class path:classes.content"
419
+ part="content">
417
420
  <slot></slot>
418
421
  </div>
419
422
  <monster-collapse data-monster-role="collapse" part="collapse">
@@ -14,8 +14,8 @@ import { instanceSymbol } from "../../constants.mjs";
14
14
  import { ATTRIBUTE_ROLE } from "../../dom/constants.mjs";
15
15
  import { CustomControl } from "../../dom/customcontrol.mjs";
16
16
  import {
17
- assembleMethodSymbol,
18
- registerCustomElement,
17
+ assembleMethodSymbol,
18
+ registerCustomElement,
19
19
  } from "../../dom/customelement.mjs";
20
20
  import { fireCustomEvent } from "../../dom/events.mjs";
21
21
 
@@ -61,100 +61,105 @@ const holdIntervalSymbol = Symbol("holdInterval");
61
61
  * @fires monster-quantity-change
62
62
  */
63
63
  class Quantity extends CustomControl {
64
- static get [instanceSymbol]() {
65
- return Symbol.for("@schukai/monster/components/form/quantity@@instance");
66
- }
67
-
68
- [assembleMethodSymbol]() {
69
- super[assembleMethodSymbol]();
70
-
71
- initControlReferences.call(this);
72
- initEventHandler.call(this);
73
- applyEditableState.call(this);
74
- clampAndRender.call(this, this.getOption("value"));
75
-
76
- return this;
77
- }
78
-
79
- /**
80
- * Current numeric value
81
- * @return {number|null}
82
- */
83
- get value() {
84
- return this.getOption("value");
85
- }
86
-
87
- /**
88
- * Sets the value programmatically (including clamping & FormValue)
89
- * @param {number|string|null} v
90
- */
91
- set value(v) {
92
- const n = normalizeNumber(v, this.getOption("precision"));
93
- clampAndRender.call(this, n);
94
- }
95
-
96
- /**
97
- * Options
98
- *
99
- * @property {Object} templates
100
- * @property {string} templates.main Main template
101
- * @property {Object} templateMapping
102
- * @property {string} templateMapping.plus Icon (SVG-Path) Plus
103
- * @property {string} templateMapping.minus Icon (SVG-Path) Minus
104
- * @property {Object} classes CSS classes
105
- * @property {string} classes.button Button class (e.g. monster-button-outline-primary)
106
- * @property {string} classes.input Additional class for input
107
- * @property {Object} features Feature toggles
108
- * @property {boolean} features.editable Allow manual input
109
- * @property {boolean} features.hold Press-and-hold accelerates
110
- * @property {boolean} features.enforceBounds Clamp value when manual input is out of bounds
111
- * @property {number} value Current value
112
- * @property {number} min Use Number.NEGATIVE_INFINITY and Number.POSITIVE_INFINITY for no bounds
113
- * @property {number} max Use Number.NEGATIVE_INFINITY and Number.POSITIVE_INFINITY for no bounds
114
- * @property {number} step Increment/decrement step
115
- * @property {number} precision Round to N decimal places (null = no explicit rounding)
116
- * @property {boolean} disabled Disable the input field (also disables manual input)
117
- * @property {string} placeholder Placeholder text
118
- * @property {string} inputmode For mobile keyboards
119
- */
120
- get defaults() {
121
- return Object.assign({}, super.defaults, {
122
- templates: { main: getTemplate() },
123
- templateMapping: {
124
- plus: `
64
+ static get [instanceSymbol]() {
65
+ return Symbol.for("@schukai/monster/components/form/quantity@@instance");
66
+ }
67
+
68
+ [assembleMethodSymbol]() {
69
+ super[assembleMethodSymbol]();
70
+
71
+ initControlReferences.call(this);
72
+ initEventHandler.call(this);
73
+ applyEditableState.call(this);
74
+ clampAndRender.call(this, this.getOption("value"));
75
+
76
+ return this;
77
+ }
78
+
79
+ /**
80
+ * Current numeric value
81
+ * @return {number|null}
82
+ */
83
+ get value() {
84
+ return this.getOption("value");
85
+ }
86
+
87
+ /**
88
+ * Sets the value programmatically (including clamping & FormValue)
89
+ * @param {number|string|null} v
90
+ */
91
+ set value(v) {
92
+ const n = normalizeNumber(v, this.getOption("precision"));
93
+ clampAndRender.call(this, n);
94
+ }
95
+
96
+ /**
97
+ * Options
98
+ *
99
+ * @property {Object} templates
100
+ * @property {string} templates.main Main template
101
+ * @property {Object} templateMapping
102
+ * @property {string} templateMapping.plus Icon (SVG-Path) Plus
103
+ * @property {string} templateMapping.minus Icon (SVG-Path) Minus
104
+ * @property {Object} classes CSS classes
105
+ * @property {string} classes.button Button class (e.g. monster-button-outline-primary)
106
+ * @property {string} classes.input Additional class for input
107
+ * @property {Object} features Feature toggles
108
+ * @property {boolean} features.editable Allow manual input
109
+ * @property {boolean} features.hold Press-and-hold accelerates
110
+ * @property {boolean} features.enforceBounds Clamp value when manual input is out of bounds
111
+ * @property {number} value Current value
112
+ * @property {number} min Use Number.NEGATIVE_INFINITY and Number.POSITIVE_INFINITY for no bounds
113
+ * @property {number} max Use Number.NEGATIVE_INFINITY and Number.POSITIVE_INFINITY for no bounds
114
+ * @property {number} step Increment/decrement step
115
+ * @property {number} precision Round to N decimal places (null = no explicit rounding)
116
+ * @property {boolean} disabled Disable the input field (also disables manual input)
117
+ * @property {string} placeholder Placeholder text
118
+ * @property {string} inputmode For mobile keyboards
119
+ */
120
+ get defaults() {
121
+ return Object.assign({}, super.defaults, {
122
+ templates: { main: getTemplate() },
123
+ templateMapping: {
124
+ plus: `
125
125
  <path d="M8 1a1 1 0 0 1 1 1v5h5a1 1 0 1 1 0 2H9v5a1 1 0 1 1-2 0V9H2a1 1 0 1 1 0-2h5V2a1 1 0 0 1 1-1z"/>`,
126
- minus: `
126
+ minus: `
127
127
  <path d="M2 7.5a1 1 0 0 0 0 1H14a1 1 0 1 0 0-2H2a1 1 0 0 0 0 1z"/>`,
128
- },
129
- classes: {
130
- button: "monster-button-outline-primary",
131
- input: "",
132
- },
133
- features: {
134
- editable: true,
135
- hold: true,
136
- enforceBounds: true,
137
- },
138
- value: 0,
139
- min: 0,
140
- max: Number.POSITIVE_INFINITY,
141
- step: 1,
142
- precision: null,
143
-
144
- disabled: false,
145
- placeholder: "",
146
- inputmode: "decimal",
147
- });
148
- }
149
-
150
- static getTag() {
151
- return "monster-quantity";
152
- }
153
-
154
- // If you want a stylesheet, return it here.
155
- static getCSSStyleSheet() {
156
- return [QuantityStyleSheet];
157
- }
128
+ },
129
+ classes: {
130
+ button: "monster-button-outline-primary",
131
+ input: "",
132
+ },
133
+ features: {
134
+ editable: true,
135
+ hold: true,
136
+ enforceBounds: true,
137
+ },
138
+ value: 0,
139
+ min: 0,
140
+ max: Number.POSITIVE_INFINITY,
141
+ step: 1,
142
+ precision: null,
143
+
144
+ disabled: false,
145
+ placeholder: "",
146
+ inputmode: "decimal",
147
+ });
148
+ }
149
+
150
+ static getTag() {
151
+ return "monster-quantity";
152
+ }
153
+
154
+ // If you want a stylesheet, return it here.
155
+ static getCSSStyleSheet() {
156
+ return [QuantityStyleSheet];
157
+ }
158
+ }
159
+
160
+ function getFiniteNumberOr(optionValue, fallback) {
161
+ const n = Number(optionValue);
162
+ return Number.isFinite(n) ? n : fallback;
158
163
  }
159
164
 
160
165
  /**
@@ -164,18 +169,18 @@ class Quantity extends CustomControl {
164
169
  * @return {void}
165
170
  */
166
171
  function initControlReferences() {
167
- this[controlElementSymbol] = this.shadowRoot.querySelector(
168
- `[${ATTRIBUTE_ROLE}="control"]`,
169
- );
170
- this[decrementButtonSymbol] = this.shadowRoot.querySelector(
171
- `[${ATTRIBUTE_ROLE}="decrement"]`,
172
- );
173
- this[incrementButtonSymbol] = this.shadowRoot.querySelector(
174
- `[${ATTRIBUTE_ROLE}="increment"]`,
175
- );
176
- this[inputElementSymbol] = this.shadowRoot.querySelector(
177
- `[${ATTRIBUTE_ROLE}="input"]`,
178
- );
172
+ this[controlElementSymbol] = this.shadowRoot.querySelector(
173
+ `[${ATTRIBUTE_ROLE}="control"]`,
174
+ );
175
+ this[decrementButtonSymbol] = this.shadowRoot.querySelector(
176
+ `[${ATTRIBUTE_ROLE}="decrement"]`,
177
+ );
178
+ this[incrementButtonSymbol] = this.shadowRoot.querySelector(
179
+ `[${ATTRIBUTE_ROLE}="increment"]`,
180
+ );
181
+ this[inputElementSymbol] = this.shadowRoot.querySelector(
182
+ `[${ATTRIBUTE_ROLE}="input"]`,
183
+ );
179
184
  }
180
185
 
181
186
  /**
@@ -185,150 +190,152 @@ function initControlReferences() {
185
190
  * @return {void}
186
191
  */
187
192
  function initEventHandler() {
188
- const stepOnce = (dir) => {
189
- const step = Number(this.getOption("step")) || 1;
190
- const cur = toNumberOr(this.value, 0);
191
- const next = cur + (dir > 0 ? step : -step);
192
- clampAndRender.call(this, next, {
193
- fire: true,
194
- kind: dir > 0 ? "increment" : "decrement",
195
- });
196
- };
197
-
198
- const startHold = (dir) => {
199
- if (!this.getOption("features.hold")) return;
200
- clearTimeout(this[holdTimerSymbol]);
201
- clearInterval(this[holdIntervalSymbol]);
202
-
203
- // After a short delay, repeat faster
204
- this[holdTimerSymbol] = setTimeout(() => {
205
- this[holdIntervalSymbol] = setInterval(() => stepOnce(dir), 60);
206
- }, 300);
207
- };
208
-
209
- const stopHold = () => {
210
- clearTimeout(this[holdTimerSymbol]);
211
- clearInterval(this[holdIntervalSymbol]);
212
- };
213
-
214
- // Buttons
215
- this[decrementButtonSymbol].addEventListener("click", (e) => stepOnce(-1));
216
- this[incrementButtonSymbol].addEventListener("click", (e) => stepOnce(1));
217
-
218
- // Press & hold (Mouse/Touch)
219
- ["mousedown", "pointerdown", "touchstart"].forEach((ev) => {
220
- this[decrementButtonSymbol].addEventListener(ev, () => startHold(-1));
221
- this[incrementButtonSymbol].addEventListener(ev, () => startHold(1));
222
- });
223
- ["mouseup", "mouseleave", "pointerup", "touchend", "touchcancel"].forEach(
224
- (ev) => {
225
- this[decrementButtonSymbol].addEventListener(ev, stopHold);
226
- this[incrementButtonSymbol].addEventListener(ev, stopHold);
227
- },
228
- );
229
-
230
- // Keyboard on input
231
- this[inputElementSymbol].addEventListener("keydown", (e) => {
232
- if (e.key === "ArrowUp") {
233
- e.preventDefault();
234
- stepOnce(1);
235
- } else if (e.key === "ArrowDown") {
236
- e.preventDefault();
237
- stepOnce(-1);
238
- }
239
- });
240
-
241
- // Manual input
242
- this[inputElementSymbol].addEventListener("input", () => {
243
- if (!this.getOption("features.editable")) return;
244
- // Only store temporarily, clamp on blur/enter – but update FormValue immediately
245
- const raw = this[inputElementSymbol].value;
246
- const n = normalizeNumber(raw, this.getOption("precision"));
247
- this.setOption("value", n);
248
- this.setFormValue(n);
249
- fireChanged.call(this, "input");
250
- });
251
-
252
- this[inputElementSymbol].addEventListener("blur", () => {
253
- if (!this.getOption("features.editable")) return;
254
- const n = normalizeNumber(
255
- this[inputElementSymbol].value,
256
- this.getOption("precision"),
257
- );
258
- clampAndRender.call(this, n, { fire: true, kind: "blur" });
259
- });
193
+ const stepOnce = (dir) => {
194
+ const step = Number(this.getOption("step")) || 1;
195
+ const cur = toNumberOr(this.value, 0);
196
+ const next = cur + (dir > 0 ? step : -step);
197
+ clampAndRender.call(this, next, {
198
+ fire: true,
199
+ kind: dir > 0 ? "increment" : "decrement",
200
+ });
201
+ };
202
+
203
+ const startHold = (dir) => {
204
+ if (!this.getOption("features.hold")) return;
205
+ clearTimeout(this[holdTimerSymbol]);
206
+ clearInterval(this[holdIntervalSymbol]);
207
+
208
+ // After a short delay, repeat faster
209
+ this[holdTimerSymbol] = setTimeout(() => {
210
+ this[holdIntervalSymbol] = setInterval(() => stepOnce(dir), 60);
211
+ }, 300);
212
+ };
213
+
214
+ const stopHold = () => {
215
+ clearTimeout(this[holdTimerSymbol]);
216
+ clearInterval(this[holdIntervalSymbol]);
217
+ };
218
+
219
+ // Buttons
220
+ this[decrementButtonSymbol].addEventListener("click", (e) => stepOnce(-1));
221
+ this[incrementButtonSymbol].addEventListener("click", (e) => stepOnce(1));
222
+
223
+ // Press & hold (Mouse/Touch)
224
+ ["mousedown", "pointerdown", "touchstart"].forEach((ev) => {
225
+ this[decrementButtonSymbol].addEventListener(ev, () => startHold(-1));
226
+ this[incrementButtonSymbol].addEventListener(ev, () => startHold(1));
227
+ });
228
+ ["mouseup", "mouseleave", "pointerup", "touchend", "touchcancel"].forEach(
229
+ (ev) => {
230
+ this[decrementButtonSymbol].addEventListener(ev, stopHold);
231
+ this[incrementButtonSymbol].addEventListener(ev, stopHold);
232
+ },
233
+ );
234
+
235
+ // Keyboard on input
236
+ this[inputElementSymbol].addEventListener("keydown", (e) => {
237
+ if (e.key === "ArrowUp") {
238
+ e.preventDefault();
239
+ stepOnce(1);
240
+ } else if (e.key === "ArrowDown") {
241
+ e.preventDefault();
242
+ stepOnce(-1);
243
+ }
244
+ });
245
+
246
+ // Manual input
247
+ this[inputElementSymbol].addEventListener("input", () => {
248
+ if (!this.getOption("features.editable")) return;
249
+ // Only store temporarily, clamp on blur/enter – but update FormValue immediately
250
+ const raw = this[inputElementSymbol].value;
251
+ const n = normalizeNumber(raw, this.getOption("precision"));
252
+ this.setOption("value", n);
253
+ this.setFormValue(n);
254
+ fireChanged.call(this, "input");
255
+ });
256
+
257
+ this[inputElementSymbol].addEventListener("blur", () => {
258
+ if (!this.getOption("features.editable")) return;
259
+ const n = normalizeNumber(
260
+ this[inputElementSymbol].value,
261
+ this.getOption("precision"),
262
+ );
263
+ clampAndRender.call(this, n, { fire: true, kind: "blur" });
264
+ });
260
265
  }
261
266
 
262
267
  function applyEditableState() {
263
- const editable = !!this.getOption("features.editable");
264
- this[inputElementSymbol].toggleAttribute("readonly", !editable);
265
- this[inputElementSymbol].toggleAttribute(
266
- "disabled",
267
- !!this.getOption("disabled"),
268
- );
268
+ const editable = !!this.getOption("features.editable");
269
+ this[inputElementSymbol].toggleAttribute("readonly", !editable);
270
+ this[inputElementSymbol].toggleAttribute(
271
+ "disabled",
272
+ !!this.getOption("disabled"),
273
+ );
269
274
  }
270
275
 
271
276
  function clampAndRender(n, opts = {}) {
272
- const min = Number.isFinite(this.getOption("min"))
273
- ? Number(this.getOption("min"))
274
- : Number.NEGATIVE_INFINITY;
275
- const max = Number.isFinite(this.getOption("max"))
276
- ? Number(this.getOption("max"))
277
- : Number.POSITIVE_INFINITY;
278
-
279
- let value = n;
280
- if (this.getOption("features.enforceBounds")) {
281
- value = Math.min(max, Math.max(min, toNumberOr(n, 0)));
282
- }
283
-
284
- // Precision
285
- const p = this.getOption("precision");
286
- if (Number.isInteger(p) && p >= 0) {
287
- value = Number(toFixedSafe(value, p));
288
- }
289
-
290
- // Render into input
291
- this[inputElementSymbol].value =
292
- value === null || Number.isNaN(value) ? "" : String(value);
293
-
294
- // Options + FormValue
295
- this.setOption("value", value);
296
- this.setFormValue(value);
297
-
298
- if (opts.fire) fireChanged.call(this, opts.kind || "programmatic");
277
+ const min = getFiniteNumberOr(
278
+ this.getOption("min"),
279
+ Number.NEGATIVE_INFINITY,
280
+ );
281
+ const max = getFiniteNumberOr(
282
+ this.getOption("max"),
283
+ Number.POSITIVE_INFINITY,
284
+ );
285
+
286
+ let value = n;
287
+ if (this.getOption("features.enforceBounds")) {
288
+ value = Math.min(max, Math.max(min, toNumberOr(n, 0)));
289
+ }
290
+
291
+ // Precision
292
+ const p = this.getOption("precision");
293
+ if (Number.isInteger(p) && p >= 0) {
294
+ value = Number(toFixedSafe(value, p));
295
+ }
296
+
297
+ // Render into input
298
+ this[inputElementSymbol].value =
299
+ value === null || Number.isNaN(value) ? "" : String(value);
300
+
301
+ // Options + FormValue
302
+ this.setOption("value", value);
303
+ this.setFormValue(value);
304
+
305
+ if (opts.fire) fireChanged.call(this, opts.kind || "programmatic");
299
306
  }
300
307
 
301
308
  function fireChanged(kind) {
302
- fireCustomEvent(this, "monster-quantity-change", {
303
- element: this,
304
- value: this.value,
305
- kind, // 'increment' | 'decrement' | 'input' | 'blur' | 'programmatic'
306
- });
309
+ fireCustomEvent(this, "monster-quantity-change", {
310
+ element: this,
311
+ value: this.value,
312
+ kind, // 'increment' | 'decrement' | 'input' | 'blur' | 'programmatic'
313
+ });
307
314
  }
308
315
 
309
316
  function normalizeNumber(v, precision) {
310
- if (v === null || v === undefined || v === "") return null;
311
- let n = Number(v);
312
- if (!Number.isFinite(n)) return null;
313
- if (Number.isInteger(precision) && precision >= 0) {
314
- n = Number(toFixedSafe(n, precision));
315
- }
316
- return n;
317
+ if (v === null || v === undefined || v === "") return null;
318
+ let n = Number(v);
319
+ if (!Number.isFinite(n)) return null;
320
+ if (Number.isInteger(precision) && precision >= 0) {
321
+ n = Number(toFixedSafe(n, precision));
322
+ }
323
+ return n;
317
324
  }
318
325
 
319
326
  function toNumberOr(v, dflt) {
320
- const n = Number(v);
321
- return Number.isFinite(n) ? n : dflt;
327
+ const n = Number(v);
328
+ return Number.isFinite(n) ? n : dflt;
322
329
  }
323
330
 
324
331
  function toFixedSafe(n, p) {
325
- // Prevents 1.00000000000002 effects
326
- return (Math.round(n * Math.pow(10, p)) / Math.pow(10, p)).toFixed(p);
332
+ // Prevents 1.00000000000002 effects
333
+ return (Math.round(n * Math.pow(10, p)) / Math.pow(10, p)).toFixed(p);
327
334
  }
328
335
 
329
336
  function getTemplate() {
330
- // language=HTML
331
- return `
337
+ // language=HTML
338
+ return `
332
339
  <div data-monster-role="control" part="control">
333
340
  <monster-input-group part="input-group">
334
341
  <button type="button"
@@ -21,6 +21,22 @@
21
21
  padding: 0 1rem;
22
22
  }
23
23
 
24
+ .collapse-alignment-no-padding {
25
+ padding: 0;
26
+ }
27
+
28
+ .collapse-alignment-no-padding-top {
29
+ padding: 0 1rem 1rem 1rem;
30
+ }
31
+
32
+ .collapse-alignment-no-padding-bottom {
33
+ padding: 1rem 1rem 0 1rem;
34
+ }
35
+
36
+ .collapse-alignment-left-right-only {
37
+ padding: 0 1rem;
38
+ }
39
+
24
40
  [data-monster-role=header] {
25
41
  display: flex;
26
42
  align-items: center;