@schukai/monster 4.128.3 → 4.129.1

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.
Files changed (32) hide show
  1. package/CHANGELOG.md +3 -0
  2. package/package.json +1 -1
  3. package/source/components/datatable/datatable.mjs +4 -1
  4. package/source/components/datatable/pagination.mjs +39 -1
  5. package/source/components/form/login.mjs +197 -0
  6. package/source/components/form/message-state-button.mjs +233 -18
  7. package/source/components/form/select.mjs +4 -4
  8. package/source/components/form/style/message-state-button.pcss +46 -3
  9. package/source/components/form/stylesheet/message-state-button.mjs +7 -14
  10. package/source/components/form/stylesheet/popper-button.mjs +7 -14
  11. package/source/components/form/tree-select.mjs +2 -2
  12. package/source/components/layout/popper.mjs +72 -1
  13. package/source/components/layout/stylesheet/popper.mjs +7 -14
  14. package/source/components/style/floating-ui.css +1 -49
  15. package/source/components/style/floating-ui.pcss +10 -0
  16. package/source/components/stylesheet/floating-ui.mjs +7 -14
  17. package/source/dom/customelement.mjs +182 -2
  18. package/source/dom/util/extract-keys.mjs +46 -10
  19. package/source/dom/util/init-options-from-attributes.mjs +4 -2
  20. package/source/dom/util/set-option-from-attribute.mjs +4 -2
  21. package/source/types/version.mjs +1 -1
  22. package/test/cases/components/form/login.mjs +168 -0
  23. package/test/cases/components/form/message-state-button.mjs +272 -0
  24. package/test/cases/components/form/popper-button.mjs +89 -0
  25. package/test/cases/components/form/select.mjs +24 -0
  26. package/test/cases/components/form/tree-select.mjs +22 -1
  27. package/test/cases/dom/util/extract-keys.mjs +34 -23
  28. package/test/cases/dom/util/init-options-from-attributes.mjs +21 -0
  29. package/test/cases/monster.mjs +1 -1
  30. package/test/web/import.js +1 -0
  31. package/test/web/test.html +2 -2
  32. package/test/web/tests.js +9300 -6050
@@ -216,4 +216,276 @@ describe("MessageStateButton", function () {
216
216
  }, 0);
217
217
  });
218
218
  });
219
+
220
+ describe("popper content presentation", function () {
221
+ afterEach(() => {
222
+ let mocks = document.getElementById("mocks");
223
+ mocks.innerHTML = "";
224
+ });
225
+
226
+ it("should resolve plain prose content to default popper clipping", function (done) {
227
+ let mocks = document.getElementById("mocks");
228
+ const button = document.createElement("monster-message-state-button");
229
+ button.innerHTML = "Save";
230
+ mocks.appendChild(button);
231
+
232
+ setTimeout(() => {
233
+ try {
234
+ button.setMessage("<div><strong>Saved</strong><p>plain html</p></div>");
235
+ button.showMessage();
236
+
237
+ setTimeout(() => {
238
+ try {
239
+ const content = button.shadowRoot.querySelector('[part="content"]');
240
+ const message = button.shadowRoot.querySelector(
241
+ '[data-monster-role="message"]',
242
+ );
243
+ expect(content).to.exist;
244
+ expect(content.getAttribute("data-monster-overflow-mode")).to.equal(
245
+ "both",
246
+ );
247
+ expect(
248
+ content.getAttribute("data-monster-message-layout"),
249
+ ).to.equal("prose");
250
+ expect(
251
+ message.getAttribute("data-monster-message-layout"),
252
+ ).to.equal("prose");
253
+ done();
254
+ } catch (e) {
255
+ done(e);
256
+ }
257
+ }, 0);
258
+ } catch (e) {
259
+ done(e);
260
+ }
261
+ }, 0);
262
+ });
263
+
264
+ it("should resolve nested select message content to horizontal clipping only", function (done) {
265
+ let mocks = document.getElementById("mocks");
266
+ const button = document.createElement("monster-message-state-button");
267
+ button.innerHTML = "Save";
268
+ mocks.appendChild(button);
269
+
270
+ setTimeout(() => {
271
+ try {
272
+ const wrapper = document.createElement("div");
273
+ wrapper.appendChild(document.createElement("monster-select"));
274
+ button.setMessage(wrapper);
275
+ button.showMessage();
276
+
277
+ setTimeout(() => {
278
+ try {
279
+ const content = button.shadowRoot.querySelector('[part="content"]');
280
+ const message = button.shadowRoot.querySelector(
281
+ '[data-monster-role="message"]',
282
+ );
283
+ expect(content).to.exist;
284
+ expect(content.getAttribute("data-monster-overflow-mode")).to.equal(
285
+ "horizontal",
286
+ );
287
+ expect(
288
+ content.getAttribute("data-monster-message-layout"),
289
+ ).to.equal("overlay");
290
+ expect(
291
+ message.getAttribute("data-monster-message-layout"),
292
+ ).to.equal("overlay");
293
+ done();
294
+ } catch (e) {
295
+ done(e);
296
+ }
297
+ }, 0);
298
+ } catch (e) {
299
+ done(e);
300
+ }
301
+ }, 0);
302
+ });
303
+
304
+ it("should resolve wide plain content to the wide layout", function (done) {
305
+ let mocks = document.getElementById("mocks");
306
+ const button = document.createElement("monster-message-state-button");
307
+ button.innerHTML = "Save";
308
+ mocks.appendChild(button);
309
+
310
+ setTimeout(() => {
311
+ try {
312
+ const wrapper = document.createElement("div");
313
+ const line = document.createElement("div");
314
+ line.setAttribute(
315
+ "style",
316
+ "white-space: nowrap; overflow-x: auto;",
317
+ );
318
+ line.textContent =
319
+ "this is intentionally a single long line to trigger wide layout";
320
+ wrapper.appendChild(line);
321
+
322
+ button.setMessage(wrapper);
323
+ button.showMessage();
324
+
325
+ setTimeout(() => {
326
+ try {
327
+ const content = button.shadowRoot.querySelector('[part="content"]');
328
+ const message = button.shadowRoot.querySelector(
329
+ '[data-monster-role="message"]',
330
+ );
331
+ expect(content).to.exist;
332
+ expect(content.getAttribute("data-monster-overflow-mode")).to.equal(
333
+ "both",
334
+ );
335
+ expect(
336
+ content.getAttribute("data-monster-message-layout"),
337
+ ).to.equal("wide");
338
+ expect(
339
+ message.getAttribute("data-monster-message-layout"),
340
+ ).to.equal("wide");
341
+ done();
342
+ } catch (e) {
343
+ done(e);
344
+ }
345
+ }, 0);
346
+ } catch (e) {
347
+ done(e);
348
+ }
349
+ }, 0);
350
+ });
351
+ });
352
+
353
+ describe("message width behavior", function () {
354
+ let originalInnerWidth;
355
+ let originalGetBoundingClientRect;
356
+
357
+ beforeEach(() => {
358
+ originalInnerWidth = window.innerWidth;
359
+ originalGetBoundingClientRect = HTMLElement.prototype.getBoundingClientRect;
360
+ });
361
+
362
+ afterEach(() => {
363
+ Object.defineProperty(window, "innerWidth", {
364
+ configurable: true,
365
+ writable: true,
366
+ value: originalInnerWidth,
367
+ });
368
+ HTMLElement.prototype.getBoundingClientRect = originalGetBoundingClientRect;
369
+ let mocks = document.getElementById("mocks");
370
+ mocks.innerHTML = "";
371
+ });
372
+
373
+ it("should keep prose content on a readable max width", function (done) {
374
+ let mocks = document.getElementById("mocks");
375
+ const button = document.createElement("monster-message-state-button");
376
+ button.innerHTML = "Save";
377
+ mocks.appendChild(button);
378
+
379
+ Object.defineProperty(window, "innerWidth", {
380
+ configurable: true,
381
+ writable: true,
382
+ value: 800,
383
+ });
384
+ HTMLElement.prototype.getBoundingClientRect = function () {
385
+ if (this?.getAttribute?.("data-measurement") === "true") {
386
+ return {
387
+ width: 900,
388
+ height: 120,
389
+ top: 0,
390
+ left: 0,
391
+ right: 900,
392
+ bottom: 120,
393
+ };
394
+ }
395
+
396
+ return {
397
+ width: 100,
398
+ height: 40,
399
+ top: 0,
400
+ left: 0,
401
+ right: 100,
402
+ bottom: 40,
403
+ };
404
+ };
405
+
406
+ setTimeout(() => {
407
+ try {
408
+ button.setMessage(
409
+ "<div><p>This long prose content should wrap instead of forcing the message popper to use the full viewport width.</p></div>",
410
+ );
411
+ button.showMessage();
412
+
413
+ setTimeout(() => {
414
+ try {
415
+ const popper = button.shadowRoot.querySelector(
416
+ '[data-monster-role="popper"]',
417
+ );
418
+ expect(popper.style.width).to.equal("512px");
419
+ expect(popper.style.maxWidth).to.equal("512px");
420
+ done();
421
+ } catch (e) {
422
+ done(e);
423
+ }
424
+ }, 0);
425
+ } catch (e) {
426
+ done(e);
427
+ }
428
+ }, 0);
429
+ });
430
+
431
+ it("should allow wide content to grow until the viewport limit", function (done) {
432
+ let mocks = document.getElementById("mocks");
433
+ const button = document.createElement("monster-message-state-button");
434
+ button.innerHTML = "Save";
435
+ mocks.appendChild(button);
436
+
437
+ Object.defineProperty(window, "innerWidth", {
438
+ configurable: true,
439
+ writable: true,
440
+ value: 800,
441
+ });
442
+ HTMLElement.prototype.getBoundingClientRect = function () {
443
+ if (this?.getAttribute?.("data-measurement") === "true") {
444
+ return {
445
+ width: 900,
446
+ height: 120,
447
+ top: 0,
448
+ left: 0,
449
+ right: 900,
450
+ bottom: 120,
451
+ };
452
+ }
453
+
454
+ return {
455
+ width: 100,
456
+ height: 40,
457
+ top: 0,
458
+ left: 0,
459
+ right: 100,
460
+ bottom: 40,
461
+ };
462
+ };
463
+
464
+ setTimeout(() => {
465
+ try {
466
+ const wrapper = document.createElement("div");
467
+ wrapper.setAttribute("data-monster-message-layout", "wide");
468
+ wrapper.textContent =
469
+ "wide content placeholder that should grow until the viewport edge";
470
+ button.setMessage(wrapper);
471
+ button.showMessage();
472
+
473
+ setTimeout(() => {
474
+ try {
475
+ const popper = button.shadowRoot.querySelector(
476
+ '[data-monster-role="popper"]',
477
+ );
478
+ expect(popper.style.width).to.equal("768px");
479
+ expect(popper.style.maxWidth).to.equal("768px");
480
+ done();
481
+ } catch (e) {
482
+ done(e);
483
+ }
484
+ }, 0);
485
+ } catch (e) {
486
+ done(e);
487
+ }
488
+ }, 0);
489
+ });
490
+ });
219
491
  });
@@ -0,0 +1,89 @@
1
+ import * as chai from "chai";
2
+ import { chaiDom } from "../../../util/chai-dom.mjs";
3
+ import { initJSDOM } from "../../../util/jsdom.mjs";
4
+
5
+ let expect = chai.expect;
6
+ chai.use(chaiDom);
7
+
8
+ let PopperButton;
9
+
10
+ describe("PopperButton", function () {
11
+ before(function (done) {
12
+ initJSDOM()
13
+ .then(() => {
14
+ return import("../../../../source/components/form/popper-button.mjs");
15
+ })
16
+ .then((m) => {
17
+ PopperButton = m["PopperButton"];
18
+ done();
19
+ })
20
+ .catch((e) => done(e));
21
+ });
22
+
23
+ afterEach(() => {
24
+ let mocks = document.getElementById("mocks");
25
+ mocks.innerHTML = "";
26
+ });
27
+
28
+ it("should instance of popper-button", function () {
29
+ expect(document.createElement("monster-popper-button")).is.instanceof(
30
+ PopperButton,
31
+ );
32
+ });
33
+
34
+ it("should apply content overflow mode to the rendered content wrapper", function (done) {
35
+ let mocks = document.getElementById("mocks");
36
+ const button = document.createElement("monster-popper-button");
37
+ mocks.appendChild(button);
38
+
39
+ setTimeout(() => {
40
+ try {
41
+ const content = button.shadowRoot.querySelector('[part="content"]');
42
+ expect(content).to.exist;
43
+ expect(content.getAttribute("data-monster-overflow-mode")).to.equal(
44
+ "both",
45
+ );
46
+
47
+ button.setOption("popper.contentOverflow", "horizontal");
48
+
49
+ setTimeout(() => {
50
+ try {
51
+ expect(
52
+ content.getAttribute("data-monster-overflow-mode"),
53
+ ).to.equal("horizontal");
54
+ done();
55
+ } catch (e) {
56
+ done(e);
57
+ }
58
+ }, 0);
59
+ } catch (e) {
60
+ done(e);
61
+ }
62
+ }, 0);
63
+ });
64
+
65
+ it("should apply content overflow mode from the HTML attribute", function (done) {
66
+ let mocks = document.getElementById("mocks");
67
+ mocks.innerHTML = `
68
+ <monster-popper-button
69
+ data-monster-option-popper-content-overflow="horizontal"
70
+ ></monster-popper-button>
71
+ `;
72
+
73
+ setTimeout(() => {
74
+ try {
75
+ const button = mocks.querySelector("monster-popper-button");
76
+ const content = button.shadowRoot.querySelector('[part="content"]');
77
+ expect(button.getOption("popper.contentOverflow")).to.equal(
78
+ "horizontal",
79
+ );
80
+ expect(content.getAttribute("data-monster-overflow-mode")).to.equal(
81
+ "horizontal",
82
+ );
83
+ done();
84
+ } catch (e) {
85
+ done(e);
86
+ }
87
+ }, 0);
88
+ });
89
+ });
@@ -371,6 +371,30 @@ describe('Select', function () {
371
371
 
372
372
  });
373
373
 
374
+ it('should expose styling parts for container, filter messages and pagination', function (done) {
375
+
376
+ let mocks = document.getElementById('mocks');
377
+ const select = document.createElement('monster-select');
378
+ select.setOption('filter.mode', 'remote');
379
+ mocks.appendChild(select);
380
+
381
+ setTimeout(() => {
382
+ try {
383
+ const container = select.shadowRoot.querySelector('[data-monster-role=container]');
384
+ const selectionMessages = select.shadowRoot.querySelector('[data-monster-role=selection-messages]');
385
+ const pagination = select.shadowRoot.querySelector('[data-monster-role=pagination]');
386
+
387
+ expect(container.getAttribute('part')).to.equal('container');
388
+ expect(selectionMessages.getAttribute('part')).to.equal('selection-messages');
389
+ expect(pagination.getAttribute('part')).to.equal('pagination');
390
+ } catch (e) {
391
+ return done(e);
392
+ }
393
+
394
+ done();
395
+ }, 0)
396
+ });
397
+
374
398
  it('should normalize options without throwing', function (done) {
375
399
  this.timeout(2000);
376
400
 
@@ -212,7 +212,28 @@ describe('Treeselect', function () {
212
212
 
213
213
  });
214
214
 
215
+ it('should expose styling parts for container and popper filter control', function (done) {
216
+
217
+ let mocks = document.getElementById('mocks');
218
+ const select = document.createElement('monster-tree-select');
219
+ mocks.appendChild(select);
220
+
221
+ setTimeout(() => {
222
+ try {
223
+ const container = select.shadowRoot.querySelector('[data-monster-role=container]');
224
+ const popperFilterControl = select.shadowRoot.querySelector('.option-filter-control');
225
+
226
+ expect(container.getAttribute('part')).to.equal('container');
227
+ expect(popperFilterControl.getAttribute('part')).to.equal('popper-filter-control');
228
+ } catch (e) {
229
+ return done(e);
230
+ }
231
+
232
+ done();
233
+ }, 0)
234
+ });
235
+
215
236
  });
216
237
 
217
238
 
218
- });
239
+ });
@@ -13,16 +13,14 @@ describe('extractKeys', () => {
13
13
  },
14
14
  };
15
15
 
16
- const expected = new Map([
17
- ['firstname', 'firstName'],
18
- ['lastname', 'lastName'],
19
- ['address-street', 'address.street'],
20
- ['address-city', 'address.city'],
21
- ]);
22
-
23
16
  const result = extractKeys(obj);
24
17
 
25
- expect(JSON.stringify(Array.from(result))).to.equal(JSON.stringify(Array.from(expected)));
18
+ expect(result.get('firstname')).to.equal('firstName');
19
+ expect(result.get('first-name')).to.equal('firstName');
20
+ expect(result.get('lastname')).to.equal('lastName');
21
+ expect(result.get('last-name')).to.equal('lastName');
22
+ expect(result.get('address-street')).to.equal('address.street');
23
+ expect(result.get('address-city')).to.equal('address.city');
26
24
  });
27
25
 
28
26
  it('should use custom key and value separators', () => {
@@ -31,14 +29,12 @@ describe('extractKeys', () => {
31
29
  lastName: 'Doe',
32
30
  };
33
31
 
34
- const expected = new Map([
35
- ['prefix+firstname', 'prefix+firstName'],
36
- ['prefix+lastname', 'prefix+lastName'],
37
- ]);
38
-
39
32
  const result = extractKeys(obj, 'prefix', '+', '+');
40
33
 
41
- expect(JSON.stringify(Array.from(result))).to.equal(JSON.stringify(Array.from(expected)));
34
+ expect(result.get('prefix+firstname')).to.equal('prefix+firstName');
35
+ expect(result.get('prefix+first-name')).to.equal('prefix+firstName');
36
+ expect(result.get('prefix+lastname')).to.equal('prefix+lastName');
37
+ expect(result.get('prefix+last-name')).to.equal('prefix+lastName');
42
38
  });
43
39
 
44
40
  it('check if value is null', () => {
@@ -48,17 +44,32 @@ describe('extractKeys', () => {
48
44
  address: null,
49
45
  };
50
46
 
51
- const expected = new Map([
52
- ['firstname', 'firstName'],
53
- ['lastname', 'lastName'],
54
- ['address', 'address'],
55
-
56
- ]);
57
-
58
47
  const result = extractKeys(obj);
59
48
 
60
- expect(JSON.stringify(Array.from(result))).to.equal(JSON.stringify(Array.from(expected)));
49
+ expect(result.get('firstname')).to.equal('firstName');
50
+ expect(result.get('first-name')).to.equal('firstName');
51
+ expect(result.get('lastname')).to.equal('lastName');
52
+ expect(result.get('last-name')).to.equal('lastName');
53
+ expect(result.get('address')).to.equal('address');
61
54
  });
62
55
 
63
- // Add more test cases as needed
56
+ it('should expose kebab-case aliases for camelCase nested keys', () => {
57
+ const obj = {
58
+ popper: {
59
+ contentOverflow: 'both',
60
+ },
61
+ message: {
62
+ width: {
63
+ viewportRatio: 0.7,
64
+ },
65
+ },
66
+ };
67
+
68
+ const result = extractKeys(obj);
69
+
70
+ expect(result.get('popper-contentoverflow')).to.equal('popper.contentOverflow');
71
+ expect(result.get('popper-content-overflow')).to.equal('popper.contentOverflow');
72
+ expect(result.get('message-width-viewportratio')).to.equal('message.width.viewportRatio');
73
+ expect(result.get('message-width-viewport-ratio')).to.equal('message.width.viewportRatio');
74
+ });
64
75
  });
@@ -164,4 +164,25 @@ describe('initOptionsFromAttributes', () => {
164
164
  expect(result.key.caseSensitive).to.equal(false);
165
165
  });
166
166
 
167
+ it('should support kebab-case aliases for camelCase option names', () => {
168
+ options = {
169
+ popper: {
170
+ contentOverflow: 'both',
171
+ },
172
+ message: {
173
+ width: {
174
+ viewportRatio: 0.7,
175
+ },
176
+ },
177
+ };
178
+
179
+ element.setAttribute('data-monster-option-popper-content-overflow', 'horizontal');
180
+ element.setAttribute('data-monster-option-message-width-viewport-ratio', '0.9');
181
+
182
+ const result = initOptionsFromAttributes(element, options);
183
+
184
+ expect(result.popper.contentOverflow).to.equal('horizontal');
185
+ expect(result.message.width.viewportRatio).to.equal(0.9);
186
+ });
187
+
167
188
  });
@@ -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.128.2")
10
+ monsterVersion = new Version("4.128.3")
11
11
 
12
12
  let m = getMonsterVersion();
13
13
 
@@ -10,6 +10,7 @@ import "../cases/components/form/button-bar.mjs";
10
10
  import "../cases/components/form/reload.mjs";
11
11
  import "../cases/components/form/state-button.mjs";
12
12
  import "../cases/components/form/select.mjs";
13
+ import "../cases/components/form/login.mjs";
13
14
  import "../cases/components/form/confirm-button.mjs";
14
15
  import "../cases/components/form/form.mjs";
15
16
  import "../cases/components/form/tree-select.mjs";
@@ -9,8 +9,8 @@
9
9
  </head>
10
10
  <body>
11
11
  <div id="headline" style="display: flex;align-items: center;justify-content: center;flex-direction: column;">
12
- <h1 style='margin-bottom: 0.1em;'>Monster 4.128.2</h1>
13
- <div id="lastupdate" style='font-size:0.7em'>last update Do 26. Mär 19:17:38 CET 2026</div>
12
+ <h1 style='margin-bottom: 0.1em;'>Monster 4.128.3</h1>
13
+ <div id="lastupdate" style='font-size:0.7em'>last update So 5. Apr 19:19:33 CEST 2026</div>
14
14
  </div>
15
15
  <div id="mocha-errors"
16
16
  style="color: red;font-weight: bold;display: flex;align-items: center;justify-content: center;flex-direction: column;margin:20px;"></div>