@schukai/monster 4.46.2 → 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,14 @@
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
+
5
13
  ## [4.46.2] - 2025-11-19
6
14
 
7
15
  ### 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.2"}
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"}
@@ -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"