@schukai/monster 4.11.0 → 4.12.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.
@@ -34,53 +34,60 @@ import { isArray } from "../../types/is.mjs";
34
34
  export { Popper };
35
35
 
36
36
  /**
37
+ * Symbol for timer callback reference
37
38
  * @private
38
39
  * @type {symbol}
39
40
  */
40
41
  const timerCallbackSymbol = Symbol("timerCallback");
41
42
 
42
43
  /**
43
- * local symbol
44
+ * Symbol for resize observer reference
44
45
  * @private
45
46
  * @type {symbol}
46
47
  */
47
48
  const resizeObserverSymbol = Symbol("resizeObserver");
48
49
 
49
50
  /**
50
- * local symbol
51
+ * Symbol for close event handler reference
51
52
  * @private
52
53
  * @type {symbol}
53
54
  */
54
55
  const closeEventHandler = Symbol("closeEventHandler");
55
56
 
56
57
  /**
58
+ * Symbol for control element reference
57
59
  * @private
58
60
  * @type {symbol}
59
61
  */
60
62
  const controlElementSymbol = Symbol("controlElement");
61
63
 
62
64
  /**
65
+ * Symbol for button element reference
63
66
  * @private
64
67
  * @type {symbol}
65
68
  */
66
69
  const buttonElementSymbol = Symbol("buttonElement");
67
70
 
68
71
  /**
69
- * local symbol
72
+ * Symbol for popper element reference
70
73
  * @private
71
74
  * @type {symbol}
72
75
  */
73
76
  const popperElementSymbol = Symbol("popperElement");
74
77
 
75
78
  /**
76
- * local symbol
79
+ * Symbol for arrow element reference
77
80
  * @private
78
81
  * @type {symbol}
79
82
  */
80
83
  const arrowElementSymbol = Symbol("arrowElement");
81
84
 
82
85
  /**
83
- * A Popper is a floating UI element that can be shown or hidden.
86
+ * Popper component for displaying floating UI elements
87
+ *
88
+ * The Popper class creates a floating overlay element that can be shown/hidden
89
+ * and positioned relative to a trigger element. It supports different interaction
90
+ * modes like click, hover, focus etc.
84
91
  *
85
92
  * @fragments /fragments/components/layout/popper/
86
93
  *
@@ -89,36 +96,34 @@ const arrowElementSymbol = Symbol("arrowElement");
89
96
  *
90
97
  * @since 1.65.0
91
98
  * @copyright schukai GmbH
92
- * @summary A beautiful popper that can make your life easier and also looks good.
93
- * @fires monster-popper-hide fired when the popper is hide.
94
- * @fires monster-popper-hidden fired when the popper is hidden.
95
- * @fires monster-popper-open fired when the popper is open.
96
- * @fires monster-popper-opened fired when the popper is opened.
99
+ * @summary Floating overlay component with flexible positioning and interaction modes
100
+ * @fires monster-popper-hide - Fired when popper starts hiding
101
+ * @fires monster-popper-hidden - Fired when popper is fully hidden
102
+ * @fires monster-popper-open - Fired when popper starts opening
103
+ * @fires monster-popper-opened - Fired when popper is fully opened
97
104
  */
98
105
  class Popper extends CustomElement {
99
106
  /**
100
- * This method is called by the `instanceof` operator.
101
- * @return {symbol}
107
+ * Gets the instance symbol for type checking
108
+ * @return {symbol} The instance type symbol
102
109
  */
103
110
  static get [instanceSymbol]() {
104
111
  return Symbol.for("@schukai/monster/components/layout/popper@@instance");
105
112
  }
106
113
 
107
114
  /**
108
- * To set the options via the HTML tag, the attribute `data-monster-options` must be used.
109
- * @see {@link https://monsterjs.org/en/doc/#configurate-a-monster-control}
110
- *
111
- * The individual configuration values can be found in the table.
115
+ * Default configuration options for the popper
112
116
  *
113
- * @property {Object} templates The templates for the control.
114
- * @property {string} templates.main The main template.
115
- * @property {string} mode The mode of the popper. Possible values are `click`, `enter`, `manual`, `focus`, "auto" or a combination of them.
116
- * @property {string} content The content of the popper.
117
- * @property {object} popper The popper options.
118
- * @property {string} popper.placement The placement of the popper. Possible values are `top`, `bottom`, `left` and `right`.
119
- * @property {function[]} popper.middleware The middleware functions of the popper.
120
- * @property {Object} features The features of the popper.
121
- * @property {boolean} features.preventOpenEventSent Prevents the open event from being sent.
117
+ * @property {Object} templates - Template configuration
118
+ * @property {string} templates.main - Main template HTML
119
+ * @property {string} mode - Interaction mode(s): click|enter|manual|focus|auto
120
+ * @property {string} content - Content template
121
+ * @property {Object} popper - Positioning options
122
+ * @property {string} popper.placement - Placement: top|bottom|left|right
123
+ * @property {Array} popper.middleware - Positioning middleware functions
124
+ * @property {Object} features - Feature flags
125
+ * @property {boolean} features.preventOpenEventSent - Prevent open event
126
+ * @returns {Object} Default options merged with parent defaults
122
127
  */
123
128
  get defaults() {
124
129
  return Object.assign({}, super.defaults, {
@@ -138,9 +143,9 @@ class Popper extends CustomElement {
138
143
  }
139
144
 
140
145
  /**
141
- * This method is called by the `connectedCallback` method on the first call.
142
- *
143
- * @return {void}
146
+ * Initialize the component
147
+ * Called on first connection to DOM
148
+ * @private
144
149
  */
145
150
  [assembleMethodSymbol]() {
146
151
  super[assembleMethodSymbol]();
@@ -149,27 +154,24 @@ class Popper extends CustomElement {
149
154
  }
150
155
 
151
156
  /**
152
- * This method returns the tag name of the element.
153
- *
154
- * @return {string}
157
+ * Gets the custom element tag name
158
+ * @return {string} The tag name
155
159
  */
156
160
  static getTag() {
157
161
  return "monster-popper";
158
162
  }
159
163
 
160
164
  /**
161
- * This method returns the css styles of the element.
162
- *
163
- * @return {CSSStyleSheet[]}
165
+ * Gets component stylesheets
166
+ * @return {CSSStyleSheet[]} Array of stylesheets
164
167
  */
165
168
  static getCSSStyleSheet() {
166
169
  return [PopperStyleSheet];
167
170
  }
168
171
 
169
172
  /**
170
- * This method is called when the element is connected to the dom.
171
- *
172
- * @return {void}
173
+ * Lifecycle callback when element connects to DOM
174
+ * Sets up event listeners and initializes popper
173
175
  */
174
176
  connectedCallback() {
175
177
  super.connectedCallback();
@@ -177,7 +179,6 @@ class Popper extends CustomElement {
177
179
  const document = getDocument();
178
180
 
179
181
  for (const [, type] of Object.entries(["click", "touch"])) {
180
- // close on outside ui-events
181
182
  document.addEventListener(type, this[closeEventHandler]);
182
183
  }
183
184
 
@@ -186,14 +187,12 @@ class Popper extends CustomElement {
186
187
  }
187
188
 
188
189
  /**
189
- * This method is called when the element is disconnected from the dom.
190
- *
191
- * @return {void}
190
+ * Lifecycle callback when element disconnects from DOM
191
+ * Cleans up event listeners and observers
192
192
  */
193
193
  disconnectedCallback() {
194
194
  super.disconnectedCallback();
195
195
 
196
- // close on outside ui-events
197
196
  for (const [, type] of Object.entries(["click", "touch"])) {
198
197
  document.removeEventListener(type, this[closeEventHandler]);
199
198
  }
@@ -202,9 +201,8 @@ class Popper extends CustomElement {
202
201
  }
203
202
 
204
203
  /**
205
- * With this method, you can show the popper.
206
- *
207
- * @return {Popper}
204
+ * Shows the popper element
205
+ * @return {Popper} The popper instance
208
206
  */
209
207
  showDialog() {
210
208
  show.call(this);
@@ -212,9 +210,8 @@ class Popper extends CustomElement {
212
210
  }
213
211
 
214
212
  /**
215
- * With this method you can hide the popper.
216
- *
217
- * @return {Popper}
213
+ * Hides the popper element
214
+ * @return {Popper} The popper instance
218
215
  */
219
216
  hideDialog() {
220
217
  hide.call(this);
@@ -222,9 +219,8 @@ class Popper extends CustomElement {
222
219
  }
223
220
 
224
221
  /**
225
- * With this method you can toggle the popper.
226
- *
227
- * @return {Popper}
222
+ * Toggles popper visibility
223
+ * @return {Popper} The popper instance
228
224
  */
229
225
  toggleDialog() {
230
226
  if (this[popperElementSymbol].style.display === STYLE_DISPLAY_MODE_BLOCK) {
@@ -237,8 +233,9 @@ class Popper extends CustomElement {
237
233
  }
238
234
 
239
235
  /**
236
+ * Initializes event handlers for popper interactivity
240
237
  * @private
241
- * @return {Popper}
238
+ * @return {Popper} The popper instance
242
239
  */
243
240
  function initEventHandler() {
244
241
  this[closeEventHandler] = (event) => {
@@ -276,10 +273,11 @@ function initEventHandler() {
276
273
  }
277
274
 
278
275
  /**
276
+ * Sets up event handlers for specific interaction mode
279
277
  * @private
280
- * @param mode
281
- * @return {Popper}
282
- * @throws Error
278
+ * @param {string} mode - Interaction mode to initialize
279
+ * @return {Popper} The popper instance
280
+ * @throws {Error} For unknown modes
283
281
  */
284
282
  function initEventHandlerByMode(mode) {
285
283
  switch (mode) {
@@ -338,10 +336,10 @@ function initEventHandlerByMode(mode) {
338
336
  }
339
337
 
340
338
  /**
339
+ * Sets up resize observer for popper repositioning
341
340
  * @private
342
341
  */
343
342
  function attachResizeObserver() {
344
- // against flickering
345
343
  this[resizeObserverSymbol] = new ResizeObserver((entries) => {
346
344
  if (this[timerCallbackSymbol] instanceof DeadMansSwitch) {
347
345
  try {
@@ -369,6 +367,10 @@ function attachResizeObserver() {
369
367
  });
370
368
  }
371
369
 
370
+ /**
371
+ * Disconnects resize observer
372
+ * @private
373
+ */
372
374
  function disconnectResizeObserver() {
373
375
  if (this[resizeObserverSymbol] instanceof ResizeObserver) {
374
376
  this[resizeObserverSymbol].disconnect();
@@ -376,6 +378,7 @@ function disconnectResizeObserver() {
376
378
  }
377
379
 
378
380
  /**
381
+ * Hides the popper element
379
382
  * @private
380
383
  */
381
384
  function hide() {
@@ -396,6 +399,7 @@ function hide() {
396
399
  }
397
400
 
398
401
  /**
402
+ * Shows the popper element
399
403
  * @private
400
404
  */
401
405
  function show() {
@@ -427,6 +431,7 @@ function show() {
427
431
  }
428
432
 
429
433
  /**
434
+ * Updates popper positioning
430
435
  * @private
431
436
  */
432
437
  function updatePopper() {
@@ -447,8 +452,9 @@ function updatePopper() {
447
452
  }
448
453
 
449
454
  /**
455
+ * Initializes references to DOM elements
450
456
  * @private
451
- * @return {Popper}
457
+ * @return {Popper} The popper instance
452
458
  */
453
459
  function initControlReferences() {
454
460
  this[controlElementSymbol] = this.shadowRoot.querySelector(
@@ -467,22 +473,23 @@ function initControlReferences() {
467
473
  }
468
474
 
469
475
  /**
476
+ * Gets the main template HTML
470
477
  * @private
471
- * @return {string}
478
+ * @return {string} Template HTML
472
479
  */
473
480
  function getTemplate() {
474
481
  // language=HTML
475
482
  return `
476
- <div data-monster-role="control" part="control">
477
- <slot name="button" data-monster-role="button"></slot>
478
-
479
- <div data-monster-role="popper" part="popper" tabindex="-1" class="monster-color-primary-1">
480
- <div data-monster-role="arrow"></div>
481
- <div part="content" class="flex" data-monster-replace="path:content">
482
- </div>
483
- </div>
484
- </div>
485
- `;
483
+ <div data-monster-role="control" part="control">
484
+ <slot name="button" data-monster-role="button"></slot>
485
+
486
+ <div data-monster-role="popper" part="popper" tabindex="-1" class="monster-color-primary-1">
487
+ <div data-monster-role="arrow"></div>
488
+ <div part="content" class="flex" data-monster-replace="path:content">
489
+ </div>
490
+ </div>
491
+ </div>
492
+ `;
486
493
  }
487
494
 
488
495
  registerCustomElement(Popper);
@@ -297,7 +297,6 @@ function initEventHandler() {
297
297
  }
298
298
 
299
299
  this[draggerElementSymbol].addEventListener("touchstart", (startEvent) => {
300
-
301
300
  startEvent.preventDefault();
302
301
  self[internalSymbol].getSubject().isDragging = true;
303
302
 
@@ -308,7 +307,10 @@ function initEventHandler() {
308
307
  if (!touch) return;
309
308
 
310
309
  // identical logic as in mousemove - but with touch.clientX/clientY
311
- let draggerWidth = getComputedStyle(self[draggerElementSymbol]).getPropertyValue("--monster-dragger-width") || "0";
310
+ let draggerWidth =
311
+ getComputedStyle(self[draggerElementSymbol]).getPropertyValue(
312
+ "--monster-dragger-width",
313
+ ) || "0";
312
314
 
313
315
  if (self.getOption("splitType") === TYPE_HORIZONTAL) {
314
316
  const containerOffsetTop = self[splitScreenElementSymbol].offsetTop;
@@ -316,30 +318,36 @@ function initEventHandler() {
316
318
 
317
319
  const min = this.getOption("dimension").min;
318
320
  const max = this.getOption("dimension").max;
319
- const topAsPercent = (newTopHeight / this[splitScreenElementSymbol].offsetHeight) * 100;
321
+ const topAsPercent =
322
+ (newTopHeight / this[splitScreenElementSymbol].offsetHeight) * 100;
320
323
 
321
324
  if (parseInt(min) > topAsPercent) newTopHeight = min;
322
325
  else if (parseInt(max) < topAsPercent) newTopHeight = max;
323
326
  else newTopHeight = topAsPercent + "%";
324
327
 
325
- const newTopHeightPx = (parseInt(newTopHeight) / 100) * this[splitScreenElementSymbol].offsetHeight;
328
+ const newTopHeightPx =
329
+ (parseInt(newTopHeight) / 100) *
330
+ this[splitScreenElementSymbol].offsetHeight;
326
331
 
327
332
  self[startPanelElementSymbol].style.height = `${newTopHeightPx}px`;
328
- self[endPanelElementSymbol].style.height = `calc(100% - ${newTopHeightPx}px - ${draggerWidth})`;
333
+ self[endPanelElementSymbol].style.height =
334
+ `calc(100% - ${newTopHeightPx}px - ${draggerWidth})`;
329
335
  } else {
330
336
  const containerOffsetLeft = self[splitScreenElementSymbol].offsetLeft;
331
337
  let newLeftWidth = touch.clientX - containerOffsetLeft;
332
338
 
333
339
  const min = this.getOption("dimension").min;
334
340
  const max = this.getOption("dimension").max;
335
- const leftAsPercent = (newLeftWidth / this[splitScreenElementSymbol].offsetWidth) * 100;
341
+ const leftAsPercent =
342
+ (newLeftWidth / this[splitScreenElementSymbol].offsetWidth) * 100;
336
343
 
337
344
  if (parseInt(min) > leftAsPercent) newLeftWidth = min;
338
345
  else if (parseInt(max) < leftAsPercent) newLeftWidth = max;
339
346
  else newLeftWidth = leftAsPercent + "%";
340
347
 
341
348
  self[startPanelElementSymbol].style.width = `${newLeftWidth}`;
342
- self[endPanelElementSymbol].style.width = `calc(100% - ${newLeftWidth} - ${draggerWidth})`;
349
+ self[endPanelElementSymbol].style.width =
350
+ `calc(100% - ${newLeftWidth} - ${draggerWidth})`;
343
351
  }
344
352
  };
345
353
 
@@ -353,7 +361,6 @@ function initEventHandler() {
353
361
  document.addEventListener("touchend", dragTouchEnd);
354
362
  });
355
363
 
356
-
357
364
  let userSelectDefault = getDocument().body.style.userSelect;
358
365
 
359
366
  this[draggerElementSymbol].addEventListener("mousedown", () => {
@@ -14,6 +14,47 @@
14
14
 
15
15
  import { languages } from "./map/languages.mjs";
16
16
 
17
+ /**
18
+ * Normalizes a number represented as a string by converting it into a valid floating-point number format,
19
+ * based on the provided or detected locale. It accounts for different decimal and a thousand separator conventions.
20
+ *
21
+ * @param {string} input - The string representation of the number to normalize. May include locale-specific formatting.
22
+ * @param {string} [locale=navigator.language] - The locale used to determine the decimal separator convention. Defaults to the user's browser locale.
23
+ * @return {number} The normalized number as a floating-point value. Returns NaN if the input is not a parsable number.
24
+ */
25
+ export function normalizeNumber(input, locale = navigator.language) {
26
+ if (input === null || input === undefined) return NaN;
27
+ if (typeof input === "number") return input; // If input is already a number, return it directly
28
+ if (typeof input !== "string") return NaN;
29
+
30
+ const cleaned = input.trim().replace(/\s/g, "");
31
+ const decimalSeparator = getDecimalSeparator(locale);
32
+ let normalized = cleaned;
33
+
34
+ if (decimalSeparator === "," && cleaned.includes(".")) {
35
+ normalized = cleaned.replace(/\./g, "").replace(",", ".");
36
+ } else if (decimalSeparator === "." && cleaned.includes(",")) {
37
+ normalized = cleaned.replace(/,/g, "").replace(".", ".");
38
+ } else if (decimalSeparator === "," && cleaned.includes(",")) {
39
+ normalized = cleaned.replace(",", ".");
40
+ }
41
+
42
+ const result = parseFloat(normalized);
43
+ return Number.isNaN(result) ? NaN : result;
44
+ }
45
+
46
+ /**
47
+ * Retrieves the decimal separator for a given locale.
48
+ *
49
+ * @param {string} [locale=navigator.language] - The locale identifier to determine the decimal separator. Defaults to the user's current locale if not provided.
50
+ * @return {string} The decimal separator used in the specified locale.
51
+ */
52
+ function getDecimalSeparator(locale = navigator.language) {
53
+ const numberWithDecimal = 1.1;
54
+ const formatted = new Intl.NumberFormat(locale).format(numberWithDecimal);
55
+ return formatted.replace(/\d/g, "")[0]; // z.B. "," oder "."
56
+ }
57
+
17
58
  /**
18
59
  * Determines the user's preferred language based on browser settings and available language options.
19
60
  *
@@ -21,6 +21,7 @@
21
21
  export * from "./components/layout/collapse.mjs";
22
22
  export * from "./components/layout/iframe.mjs";
23
23
  export * from "./components/layout/tabs.mjs";
24
+ export * from "./components/layout/full-screen.mjs";
24
25
  export * from "./components/layout/split-panel.mjs";
25
26
  export * from "./components/layout/overlay.mjs";
26
27
  export * from "./components/layout/popper.mjs";
@@ -108,6 +109,8 @@ export * from "./components/state/log/entry.mjs";
108
109
  export * from "./components/state/state.mjs";
109
110
  export * from "./components/state/log.mjs";
110
111
  export * from "./components/navigation/table-of-content.mjs";
112
+ export * from "./components/data/metric-graph.mjs";
113
+ export * from "./components/data/metric.mjs";
111
114
  export * from "./components/constants.mjs";
112
115
  export * from "./components/constants.mjs";
113
116
  export * from "./text/formatter.mjs";
@@ -15,7 +15,7 @@
15
15
  export { generateRangeComparisonExpression };
16
16
 
17
17
  /**
18
- * The `generateRangeComparisonExpression()` function is function that generates a string representation
18
+ * The `generateRangeComparisonExpression()` function is a function that generates a string representation
19
19
  * of a comparison expression based on a range of values. It takes three arguments:
20
20
  *
21
21
  * - expression (required): a string representation of a range of values in the format of start1-end1,start2-end2,value3....
@@ -33,6 +33,8 @@ export { generateRangeComparisonExpression };
33
33
  * eqOp (string, default: '=='): the equality operator to use in the expression.
34
34
  * geOp (string, default: '>='): the greater than or equal to operator to use in the expression.
35
35
  * leOp (string, default: '<='): the less than or equal to operator to use in the expression.
36
+ * gtOp (string, default: '>'): the greater than operator to use in the expression.
37
+ * ltOp (string, default: '<'): the less than operator to use in the expression.
36
38
  *
37
39
  * Examples
38
40
  *
@@ -58,10 +60,16 @@ export { generateRangeComparisonExpression };
58
60
  * @param {string} [options.eqOp='=='] - The comparison operator for equality to use.
59
61
  * @param {string} [options.geOp='>='] - The comparison operator for greater than or equal to to use.
60
62
  * @param {string} [options.leOp='<='] - The comparison operator for less than or equal to to use.
63
+ * @param {string} [options.gtOp='>'] - The comparison operator for greater than to use.
64
+ * @param {string} [options.ltOp='<'] - The comparison operator for less than to use.
61
65
  * @return {string} The generated comparison expression.
62
66
  * @throws {Error} If the input is invalid.
63
67
  * @summary Generates a comparison expression based on a range of values.
64
68
  */
69
+ /**
70
+ * Copyright © schukai GmbH and all contributing authors, {{copyRightYear}}. All rights reserved.
71
+ * SPDX-License-Identifier: AGPL-3.0
72
+ */
65
73
  function generateRangeComparisonExpression(
66
74
  expression,
67
75
  valueName,
@@ -74,48 +82,110 @@ function generateRangeComparisonExpression(
74
82
  eqOp = "==",
75
83
  geOp = ">=",
76
84
  leOp = "<=",
85
+ gtOp = ">",
86
+ ltOp = "<",
77
87
  } = options;
78
- const ranges = expression.split(",");
79
- let comparison = "";
80
- for (let i = 0; i < ranges.length; i++) {
81
- const range = ranges[i].trim();
82
- if (range === "") {
88
+
89
+ if (
90
+ typeof expression !== "string" ||
91
+ typeof valueName !== "string" ||
92
+ !expression.trim()
93
+ ) {
94
+ throw new Error("Invalid input");
95
+ }
96
+
97
+ const encode = (s) => (urlEncode ? encodeURIComponent(s) : s);
98
+ const and = encode(andOp);
99
+ const or = encode(orOp);
100
+ const space = urlEncode ? "%20" : " ";
101
+
102
+ function parseRange(range) {
103
+ range = range.trim();
104
+ if (!range) throw new Error("Empty range");
105
+
106
+ // Sonderfall: nur Bindestriche wie "--"
107
+ if (/^-+$/.test(range)) {
83
108
  throw new Error(`Invalid range '${range}'`);
84
- } else if (range.includes("-")) {
85
- const [start, end] = range
86
- .split("-")
87
- .map((s) => (s === "" ? null : parseFloat(s)));
88
- if ((start !== null && isNaN(start)) || (end !== null && isNaN(end))) {
89
- throw new Error(`Invalid value in range '${range}'`);
90
- }
91
- if (start !== null && end !== null && start > end) {
92
- throw new Error(`Invalid range '${range}'`);
93
- }
94
- const compStart =
95
- start !== null
96
- ? `${valueName}${urlEncode ? encodeURIComponent(geOp) : geOp}${start}`
97
- : "";
98
- const compEnd =
99
- end !== null
100
- ? `${valueName}${urlEncode ? encodeURIComponent(leOp) : leOp}${end}`
101
- : "";
102
- const compRange = `${compStart}${
103
- compStart && compEnd ? ` ${andOp} ` : ""
104
- }${compEnd}`;
105
- comparison += ranges.length > 1 ? `(${compRange})` : compRange;
106
- } else {
107
- const value = parseFloat(range);
108
- if (isNaN(value)) {
109
+ }
110
+
111
+ // Bereich: z.B. -10--5, 4-6
112
+ const rangeMatch = range.match(
113
+ /^(-?\d+(?:\.\d+)?)\s*-\s*(-?\d+(?:\.\d+)?)/,
114
+ );
115
+ if (rangeMatch) {
116
+ const sNum = parseFloat(rangeMatch[1]);
117
+ const eNum = parseFloat(rangeMatch[2]);
118
+ if (isNaN(sNum) || isNaN(eNum))
109
119
  throw new Error(`Invalid value '${range}'`);
110
- }
111
- const compValue = `${valueName}${
112
- urlEncode ? encodeURIComponent(eqOp) : eqOp
113
- }${value}`;
114
- comparison += ranges.length > 1 ? `(${compValue})` : compValue;
120
+ if (sNum > eNum) throw new Error(`Invalid range '${range}'`);
121
+ return `${valueName}${encode(geOp)}${sNum}${space}${and}${space}${valueName}${encode(leOp)}${eNum}`;
115
122
  }
116
- if (i < ranges.length - 1) {
117
- comparison += ` ${orOp} `;
123
+
124
+ // Exklusiver Bereich: 4~6 → x>4 && x<6
125
+ const exclMatch = range.match(/^(-?\d+(?:\.\d+)?)\s*~\s*(-?\d+(?:\.\d+)?)/);
126
+ if (exclMatch) {
127
+ const sNum = parseFloat(exclMatch[1]);
128
+ const eNum = parseFloat(exclMatch[2]);
129
+ if (isNaN(sNum) || isNaN(eNum))
130
+ throw new Error(`Invalid value '${range}'`);
131
+ return `${valueName}${encode(gtOp)}${sNum}${space}${and}${space}${valueName}${encode(ltOp)}${eNum}`;
118
132
  }
133
+
134
+ // Offene Intervalle exklusiv: 4~ → x>4, ~6 → x<6
135
+ const openRightExclMatch = range.match(/^(-?\d+(?:\.\d+)?)~$/);
136
+ if (openRightExclMatch) {
137
+ const sNum = parseFloat(openRightExclMatch[1]);
138
+ if (isNaN(sNum)) throw new Error(`Invalid value in '${range}'`);
139
+ return `${valueName}${encode(gtOp)}${sNum}`;
140
+ }
141
+ const openLeftExclMatch = range.match(/^~(-?\d+(?:\.\d+)?)$/);
142
+ if (openLeftExclMatch) {
143
+ const eNum = parseFloat(openLeftExclMatch[1]);
144
+ if (isNaN(eNum)) throw new Error(`Invalid value in '${range}'`);
145
+ return `${valueName}${encode(ltOp)}${eNum}`;
146
+ }
147
+
148
+ // Offene Intervalle inklusiv: 4- → x>=4, -6 → x<=6
149
+ const openRightInclMatch = range.match(/^(-?\d+(?:\.\d+)?)\-$/);
150
+ if (openRightInclMatch) {
151
+ const sNum = parseFloat(openRightInclMatch[1]);
152
+ if (isNaN(sNum)) throw new Error(`Invalid value in '${range}'`);
153
+ return `${valueName}${encode(geOp)}${sNum}`;
154
+ }
155
+ const openLeftInclMatch = range.match(/^\-(-?\d+(?:\.\d+)?)$/);
156
+ if (openLeftInclMatch) {
157
+ const eNum = parseFloat(openLeftInclMatch[1]);
158
+ if (isNaN(eNum)) throw new Error(`Invalid value in '${range}'`);
159
+ return `${valueName}${encode(leOp)}${eNum}`;
160
+ }
161
+
162
+ // <n oder >n
163
+ if (range.startsWith("<")) {
164
+ const n = parseFloat(range.slice(1));
165
+ if (isNaN(n)) throw new Error(`Invalid value in '${range}'`);
166
+ return `${valueName}${encode(ltOp)}${n}`;
167
+ }
168
+ if (range.startsWith(">")) {
169
+ const n = parseFloat(range.slice(1));
170
+ if (isNaN(n)) throw new Error(`Invalid value in '${range}'`);
171
+ return `${valueName}${encode(gtOp)}${n}`;
172
+ }
173
+
174
+ // Einzelwert NUR, wenn wirklich reine Zahl
175
+ if (/^-?\d+(\.\d+)?$/.test(range)) {
176
+ const value = parseFloat(range);
177
+ return `${valueName}${encode(eqOp)}${value}`;
178
+ }
179
+
180
+ throw new Error(`Invalid value '${range}'`);
119
181
  }
120
- return comparison;
182
+
183
+ const parts = expression
184
+ .split(",")
185
+ .map((r) => r.trim())
186
+ .filter(Boolean)
187
+ .map(parseRange);
188
+
189
+ const sep = space + or + space;
190
+ return parts.length > 1 ? parts.map((p) => `(${p})`).join(sep) : parts[0];
121
191
  }
@@ -156,7 +156,7 @@ function getMonsterVersion() {
156
156
  }
157
157
 
158
158
  /** don't touch, replaced by make with package.json version */
159
- monsterVersion = new Version("4.10.0");
159
+ monsterVersion = new Version("4.11.1");
160
160
 
161
161
  return monsterVersion;
162
162
  }
@@ -7,7 +7,7 @@ describe('Monster', function () {
7
7
  let monsterVersion
8
8
 
9
9
  /** don´t touch, replaced by make with package.json version */
10
- monsterVersion = new Version("4.10.0")
10
+ monsterVersion = new Version("4.11.1")
11
11
 
12
12
  let m = getMonsterVersion();
13
13