@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.
- package/CHANGELOG.md +29 -0
- package/package.json +1 -1
- package/source/components/content/camera-capture.mjs +41 -8
- package/source/components/content/stylesheet/camera-capture.mjs +13 -6
- package/source/components/data/metric-graph.mjs +356 -358
- package/source/components/data/metric.mjs +92 -94
- package/source/components/data/stylesheet/metric-graph.mjs +13 -6
- package/source/components/data/stylesheet/metric.mjs +13 -6
- package/source/components/datatable/filter.mjs +22 -0
- package/source/components/form/message-state-button.mjs +26 -22
- package/source/components/layout/popper.mjs +75 -68
- package/source/components/layout/split-panel.mjs +15 -8
- package/source/i18n/util.mjs +41 -0
- package/source/monster.mjs +3 -0
- package/source/text/generate-range-comparison-expression.mjs +109 -39
- package/source/types/version.mjs +1 -1
- package/test/cases/monster.mjs +1 -1
- package/test/cases/text/generate-range-comparison-expression.mjs +94 -0
- package/test/cases/text/util.mjs +3 -3
- package/test/web/import.js +1 -0
- package/test/web/test.html +2 -2
- package/test/web/tests.js +313 -132
@@ -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
|
-
*
|
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
|
-
*
|
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
|
-
*
|
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
|
-
*
|
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
|
-
*
|
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
|
93
|
-
* @fires monster-popper-hide
|
94
|
-
* @fires monster-popper-hidden
|
95
|
-
* @fires monster-popper-open
|
96
|
-
* @fires monster-popper-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
|
-
*
|
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
|
-
*
|
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
|
114
|
-
* @property {string} templates.main
|
115
|
-
* @property {string} mode
|
116
|
-
* @property {string} content
|
117
|
-
* @property {
|
118
|
-
* @property {string} popper.placement
|
119
|
-
* @property {
|
120
|
-
* @property {Object} features
|
121
|
-
* @property {boolean} features.preventOpenEventSent
|
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
|
-
*
|
142
|
-
*
|
143
|
-
* @
|
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
|
-
*
|
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
|
-
*
|
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
|
-
*
|
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
|
-
*
|
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
|
-
*
|
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
|
-
*
|
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
|
-
*
|
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
|
-
|
477
|
-
|
478
|
-
|
479
|
-
|
480
|
-
|
481
|
-
|
482
|
-
|
483
|
-
|
484
|
-
|
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 =
|
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 =
|
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 =
|
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 =
|
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 =
|
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 =
|
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", () => {
|
package/source/i18n/util.mjs
CHANGED
@@ -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
|
*
|
package/source/monster.mjs
CHANGED
@@ -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
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
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
|
-
}
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
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
|
-
|
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
|
-
|
117
|
-
|
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
|
-
|
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
|
}
|
package/source/types/version.mjs
CHANGED
package/test/cases/monster.mjs
CHANGED