@schukai/monster 4.148.2 → 4.148.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.
Files changed (27) hide show
  1. package/package.json +1 -1
  2. package/source/components/datatable/filter/date-presets.mjs +13 -0
  3. package/source/components/datatable/filter/date-range.mjs +12 -0
  4. package/source/components/datatable/filter/date-time.mjs +14 -1
  5. package/source/components/datatable/filter/date.mjs +14 -1
  6. package/source/components/datatable/filter/input.mjs +14 -1
  7. package/source/components/datatable/filter/range.mjs +11 -0
  8. package/source/components/datatable/filter/text-operator.mjs +19 -0
  9. package/source/components/datatable/filter/time.mjs +14 -1
  10. package/source/components/form/buy-box.mjs +2 -0
  11. package/source/components/form/cart-control.mjs +2 -0
  12. package/source/components/state/state.mjs +2 -0
  13. package/source/data/extend.mjs +7 -1
  14. package/source/dom/customcontrol.mjs +28 -23
  15. package/source/dom/customelement.mjs +105 -39
  16. package/source/dom/util/extract-keys.mjs +2 -1
  17. package/source/dom/util/init-options-from-attributes.mjs +1 -1
  18. package/source/dom/util/set-option-from-attribute.mjs +14 -3
  19. package/test/cases/components/datatable/filter-form-value.mjs +98 -0
  20. package/test/cases/components/form/non-value-form-association.mjs +53 -0
  21. package/test/cases/data/extend.mjs +27 -1
  22. package/test/cases/dom/customcontrol.mjs +188 -6
  23. package/test/cases/dom/customelement-initfromscripthost.mjs +62 -0
  24. package/test/cases/dom/customelement.mjs +123 -2
  25. package/test/cases/dom/util/extract-keys.mjs +14 -0
  26. package/test/cases/dom/util/init-options-from-attributes.mjs +18 -0
  27. package/test/cases/dom/util/set-option-from-attribute.mjs +151 -0
@@ -55,7 +55,7 @@ function extractKeys(
55
55
  currentKebabKeyPrefix,
56
56
  currentValuePrefix,
57
57
  ) {
58
- for (const key in currentObj) {
58
+ for (const key of Object.keys(currentObj)) {
59
59
  const compactSegment = normalizeKeySegment(key);
60
60
  const kebabSegment = toKebabCase(key);
61
61
 
@@ -73,6 +73,7 @@ function extractKeys(
73
73
  const newValuePrefix = currentValuePrefix
74
74
  ? currentValuePrefix + valueSeparator + key
75
75
  : key;
76
+ appendKeys(newCompactKeyPrefix, newKebabKeyPrefix, newValuePrefix);
76
77
  helper(
77
78
  currentObj[key],
78
79
  newCompactKeyPrefix,
@@ -84,7 +84,7 @@ function initOptionsFromAttributes(
84
84
  if (element.hasAttribute(name)) {
85
85
  let value = element.getAttribute(name);
86
86
  if (
87
- mapping.hasOwnProperty(optionName) &&
87
+ Object.prototype.hasOwnProperty.call(mapping, optionName) &&
88
88
  isFunction(mapping[optionName])
89
89
  ) {
90
90
  value = mapping[optionName](value);
@@ -24,6 +24,7 @@ import {
24
24
  } from "../../types/is.mjs";
25
25
  import { attributeObserverSymbol } from "../customelement.mjs";
26
26
  import { extractKeys } from "./extract-keys.mjs";
27
+ import { clone } from "../../util/clone.mjs";
27
28
 
28
29
  export { setOptionFromAttribute };
29
30
 
@@ -56,6 +57,7 @@ export { setOptionFromAttribute };
56
57
  * @param {Object} options - The options object to be initialized.
57
58
  * @param {Object} mapping - A mapping between the attribute value and the property value.
58
59
  * @param {string} prefix - The prefix of the attributes to be considered.
60
+ * @param {Object} defaults - The default option schema used for coercion and removal.
59
61
  * @return {Object} - The initialized options object.
60
62
  * @this HTMLElement - The context of the DOM element.
61
63
  */
@@ -65,12 +67,13 @@ function setOptionFromAttribute(
65
67
  options,
66
68
  mapping = {},
67
69
  prefix = "data-monster-option-",
70
+ defaults = options,
68
71
  ) {
69
72
  if (!(element instanceof HTMLElement)) return options;
70
- if (!element.hasAttributes()) return options;
71
73
 
72
74
  const keyMap = extractKeys(options);
73
75
  const finder = new Pathfinder(options);
76
+ const defaultFinder = new Pathfinder(defaults);
74
77
 
75
78
  // check if the attribute name is a valid option.
76
79
  // the mapping between the attribute is simple. The dash is replaced by a dot.
@@ -79,15 +82,23 @@ function setOptionFromAttribute(
79
82
  if (!finder.exists(optionName)) return;
80
83
 
81
84
  if (!element.hasAttribute(name)) {
85
+ if (defaultFinder.exists(optionName)) {
86
+ finder.setVia(optionName, clone(defaultFinder.getVia(optionName)));
87
+ }
82
88
  return options;
83
89
  }
84
90
 
85
91
  let value = element.getAttribute(name);
86
- if (mapping.hasOwnProperty(optionName) && isFunction(mapping[optionName])) {
92
+ if (
93
+ Object.prototype.hasOwnProperty.call(mapping, optionName) &&
94
+ isFunction(mapping[optionName])
95
+ ) {
87
96
  value = mapping[optionName](value);
88
97
  }
89
98
 
90
- let optionValue = finder.getVia(optionName);
99
+ let optionValue = defaultFinder.exists(optionName)
100
+ ? defaultFinder.getVia(optionName)
101
+ : finder.getVia(optionName);
91
102
  if (optionValue === null || optionValue === undefined) {
92
103
  optionValue = value;
93
104
  }
@@ -0,0 +1,98 @@
1
+ 'use strict';
2
+
3
+ import { expect } from 'chai';
4
+ import { getDocument } from '../../../../source/dom/util.mjs';
5
+ import { initJSDOM } from '../../../util/jsdom.mjs';
6
+
7
+ const cases = [
8
+ { tag: 'monster-filter-date', value: '2026-07-16' },
9
+ { tag: 'monster-filter-date-time', value: '2026-07-16T12:30' },
10
+ { tag: 'monster-filter-time', value: '12:30' },
11
+ { tag: 'monster-filter-input', value: 'search term' },
12
+ { tag: 'monster-filter-text-operator', value: ':search term', userValue: 'search term' },
13
+ { tag: 'monster-filter-date-presets', preset: true },
14
+ { tag: 'monster-filter-date-range', value: '2026-07-01-2026-07-16' },
15
+ { tag: 'monster-filter-range', value: '10-20' },
16
+ ];
17
+
18
+ describe('datatable filter form values issue #502', function () {
19
+ let document;
20
+
21
+ before(async function () {
22
+ await initJSDOM({});
23
+ await import('element-internals-polyfill');
24
+ await Promise.all([
25
+ import('../../../../source/components/datatable/filter/date.mjs'),
26
+ import('../../../../source/components/datatable/filter/date-time.mjs'),
27
+ import('../../../../source/components/datatable/filter/time.mjs'),
28
+ import('../../../../source/components/datatable/filter/input.mjs'),
29
+ import('../../../../source/components/datatable/filter/text-operator.mjs'),
30
+ import('../../../../source/components/datatable/filter/date-presets.mjs'),
31
+ import('../../../../source/components/datatable/filter/date-range.mjs'),
32
+ import('../../../../source/components/datatable/filter/range.mjs'),
33
+ ]);
34
+ document = getDocument();
35
+ });
36
+
37
+ afterEach(function () {
38
+ document.getElementById('mocks').innerHTML = '';
39
+ });
40
+
41
+ for (const definition of cases) {
42
+ it(`${definition.tag} should submit programmatic and empty values`, async function () {
43
+ const form = document.createElement('form');
44
+ const control = document.createElement(definition.tag);
45
+ control.setAttribute('name', 'filter');
46
+ form.appendChild(control);
47
+ document.getElementById('mocks').appendChild(form);
48
+
49
+ expect(new window.FormData(form).get('filter')).to.equal('');
50
+
51
+ const value = definition.preset
52
+ ? control.shadowRoot.querySelector('select').options[1].value
53
+ : definition.value;
54
+ control.value = value;
55
+
56
+ expect(new window.FormData(form).get('filter')).to.equal(control.value);
57
+
58
+ control.setAttribute('value', value);
59
+ control.value = '';
60
+ control.formResetCallback();
61
+ expect(new window.FormData(form).get('filter')).to.equal(control.value);
62
+
63
+ control.formStateRestoreCallback(value, 'restore');
64
+ expect(new window.FormData(form).get('filter')).to.equal(control.value);
65
+
66
+ control.remove();
67
+ form.appendChild(control);
68
+ expect(new window.FormData(form).get('filter')).to.equal(control.value);
69
+
70
+ control.setAttribute('disabled', '');
71
+ await new Promise((resolve) => setTimeout(resolve, 20));
72
+ if (!window.navigator.userAgent.includes('jsdom')) {
73
+ expect(new window.FormData(form).get('filter')).to.equal(null);
74
+ }
75
+ });
76
+
77
+ it(`${definition.tag} should submit user-driven values`, function () {
78
+ const form = document.createElement('form');
79
+ const control = document.createElement(definition.tag);
80
+ control.setAttribute('name', 'filter');
81
+ form.appendChild(control);
82
+ document.getElementById('mocks').appendChild(form);
83
+
84
+ const input = definition.tag === 'monster-filter-text-operator'
85
+ ? control.shadowRoot.querySelector('[data-monster-role=query]')
86
+ : definition.preset
87
+ ? control.shadowRoot.querySelector('select')
88
+ : control.shadowRoot.querySelector('[data-monster-role=query], [data-monster-role=input]');
89
+ input.value = definition.preset
90
+ ? input.options[1].value
91
+ : definition.userValue ?? definition.value;
92
+ input.dispatchEvent(new window.Event('input', { bubbles: true, composed: true }));
93
+ input.dispatchEvent(new window.Event('change', { bubbles: true, composed: true }));
94
+
95
+ expect(new window.FormData(form).get('filter')).to.equal(control.value);
96
+ });
97
+ }
98
+ });
@@ -0,0 +1,53 @@
1
+ 'use strict';
2
+
3
+ import { expect } from 'chai';
4
+ import { initJSDOM } from '../../../util/jsdom.mjs';
5
+
6
+ describe('non-value component form association issue #503', function () {
7
+ let CustomControl;
8
+ let components;
9
+
10
+ before(async function () {
11
+ await initJSDOM({});
12
+ await import('element-internals-polyfill');
13
+ ({ CustomControl } = await import('../../../../source/dom/customcontrol.mjs'));
14
+ const [{ State }, { CartControl }, { BuyBox }] = await Promise.all([
15
+ import('../../../../source/components/state/state.mjs'),
16
+ import('../../../../source/components/form/cart-control.mjs'),
17
+ import('../../../../source/components/form/buy-box.mjs'),
18
+ ]);
19
+ components = [State, CartControl, BuyBox];
20
+ });
21
+
22
+ afterEach(function () {
23
+ document.getElementById('mocks').innerHTML = '';
24
+ });
25
+
26
+ it('should opt non-value components out of form association', function () {
27
+ for (const Component of components) {
28
+ expect(Component.formAssociated, Component.getTag()).to.equal(false);
29
+ }
30
+ });
31
+
32
+ it('should preserve the public CustomControl inheritance', function () {
33
+ for (const Component of components) {
34
+ const control = document.createElement(Component.getTag());
35
+ expect(control, Component.getTag()).to.be.instanceof(CustomControl);
36
+ }
37
+ });
38
+
39
+ it('should not contribute values or throw during form reset', function () {
40
+ for (const Component of components) {
41
+ const form = document.createElement('form');
42
+ const control = document.createElement(Component.getTag());
43
+ control.setAttribute('name', Component.getTag());
44
+ form.appendChild(control);
45
+ document.getElementById('mocks').appendChild(form);
46
+
47
+ expect(() => form.reset()).not.to.throw();
48
+ expect(new window.FormData(form).get(Component.getTag())).to.equal(null);
49
+
50
+ form.remove();
51
+ }
52
+ });
53
+ });
@@ -156,7 +156,33 @@ describe('extend function', () => {
156
156
  const result = extend(target, source);
157
157
  expect(result).to.deep.equal({ a: [3, 4] });
158
158
  });
159
- });
160
159
 
160
+ it('should ignore inherited enumerable properties issue #499', () => {
161
+ const prototype = { inherited: 'unsafe' };
162
+ const source = Object.assign(Object.create(prototype), { own: 'safe' });
163
+
164
+ expect(extend({}, source)).to.deep.equal({ own: 'safe' });
165
+ });
166
+
167
+ [
168
+ '{"__proto__":{"monsterExtendPolluted":"yes"}}',
169
+ '{"nested":{"__proto__":{"monsterExtendPolluted":"yes"}}}',
170
+ '{"constructor":{"prototype":{"monsterExtendPolluted":"yes"}}}',
171
+ ].forEach((json) => {
172
+ it(`should not mutate Object.prototype for ${json} issue #499`, () => {
173
+ delete Object.prototype.monsterExtendPolluted;
174
+
175
+ try {
176
+ const result = extend({}, JSON.parse(json));
177
+
178
+ expect(Object.prototype.monsterExtendPolluted).to.equal(undefined);
179
+ expect(({}).monsterExtendPolluted).to.equal(undefined);
180
+ expect(result).to.be.an('object');
181
+ } finally {
182
+ delete Object.prototype.monsterExtendPolluted;
183
+ }
184
+ });
185
+ });
186
+ });
161
187
 
162
188
 
@@ -88,14 +88,20 @@ describe('DOM', function () {
88
88
 
89
89
  })
90
90
 
91
- it('should return undefined', function () {
91
+ it('should not reflect computed disabledness to the host attribute issue #500', function () {
92
92
  expect(element.formDisabledCallback()).to.be.undefined;
93
93
  expect(element.hasAttribute('disabled')).to.be.false;
94
94
  expect(element.formDisabledCallback(true)).to.be.undefined;
95
- expect(element.hasAttribute('disabled')).to.be.true;
96
- const d = element.getAttribute('disabled');
97
- expect(d).to.not.be.null;
98
-
95
+ expect(element.hasAttribute('disabled')).to.be.false;
96
+ expect(element.getOption('disabled')).to.be.false;
97
+ });
98
+
99
+ it('should preserve an author-provided form attribute issue #500', function () {
100
+ element.setAttribute('form', 'author-form');
101
+
102
+ element.formAssociatedCallback(null);
103
+
104
+ expect(element.getAttribute('form')).to.equal('author-form');
99
105
  });
100
106
 
101
107
  })
@@ -319,9 +325,185 @@ describe('DOM', function () {
319
325
  expect(d.value).to.equal('seed');
320
326
  });
321
327
 
328
+ it('formStateRestoreCallback() should restore through the value setter issue #501', function () {
329
+ const htmlTAG = 'monster-customcontrol-state-restore';
330
+
331
+ class StateRestoreComponent extends CustomControl {
332
+ static getTag() {
333
+ return htmlTAG;
334
+ }
335
+
336
+ get value() {
337
+ return this._value ?? '';
338
+ }
339
+
340
+ set value(value) {
341
+ this._value = value;
342
+ }
343
+ }
344
+
345
+ registerCustomElement(StateRestoreComponent);
346
+ const control = document.createElement(htmlTAG);
347
+ control.value = 'changed';
348
+
349
+ control.formStateRestoreCallback('restored', 'restore');
350
+
351
+ expect(control.value).to.equal('restored');
352
+ });
353
+
354
+ it('should normalize a removed value attribute to an empty string issue #501', async function () {
355
+ const htmlTAG = 'monster-customcontrol-value-removal';
356
+
357
+ class ValueRemovalComponent extends CustomControl {
358
+ static getTag() {
359
+ return htmlTAG;
360
+ }
361
+
362
+ get defaults() {
363
+ return Object.assign({}, super.defaults, {
364
+ shadowMode: false,
365
+ });
366
+ }
367
+
368
+ get value() {
369
+ return this._value ?? '';
370
+ }
371
+
372
+ set value(value) {
373
+ this._value = value;
374
+ this.lastAssignedValue = value;
375
+ }
376
+ }
377
+
378
+ registerCustomElement(ValueRemovalComponent);
379
+ const control = document.createElement(htmlTAG);
380
+ document.body.appendChild(control);
381
+ control.setAttribute('value', 'seed');
382
+ await new Promise((resolve) => setTimeout(resolve, 30));
383
+ control.removeAttribute('value');
384
+ await new Promise((resolve) => setTimeout(resolve, 30));
385
+
386
+ expect(control.lastAssignedValue).to.equal('');
387
+ expect(control.value).to.equal('');
388
+ control.remove();
389
+ });
390
+
391
+ it('should replay a pre-upgrade value once after assembly issue #501', async function () {
392
+ const htmlTAG = 'monster-customcontrol-pre-upgrade';
393
+ const control = document.createElement(htmlTAG);
394
+ control.value = 'assigned-before-definition';
395
+ document.body.appendChild(control);
396
+
397
+ class PreUpgradeComponent extends CustomControl {
398
+ static getTag() {
399
+ return htmlTAG;
400
+ }
401
+
402
+ get defaults() {
403
+ return Object.assign({}, super.defaults, {
404
+ shadowMode: false,
405
+ });
406
+ }
407
+
408
+ get value() {
409
+ return this._value ?? '';
410
+ }
411
+
412
+ set value(value) {
413
+ this._value = value;
414
+ this.setterCalls = (this.setterCalls ?? 0) + 1;
415
+ }
416
+ }
417
+
418
+ registerCustomElement(PreUpgradeComponent);
419
+ await customElements.whenDefined(htmlTAG);
420
+ await Promise.resolve();
421
+
422
+ expect(control).to.be.instanceof(PreUpgradeComponent);
423
+ expect(Object.prototype.hasOwnProperty.call(control, 'value')).to.be.false;
424
+ expect(control.value).to.equal('assigned-before-definition');
425
+ expect(control.setterCalls).to.equal(1);
426
+
427
+ control.remove();
428
+ document.body.appendChild(control);
429
+ await Promise.resolve();
430
+ expect(control.setterCalls).to.equal(1);
431
+ control.remove();
432
+ });
433
+
434
+ it('should preserve native fieldset and explicit form lifecycle issue #500', async function () {
435
+ if (window.navigator.userAgent.includes('jsdom')) {
436
+ this.skip();
437
+ }
438
+
439
+ const htmlTAG = 'monster-customcontrol-native-lifecycle';
440
+
441
+ class NativeLifecycleComponent extends CustomControl {
442
+ static getTag() {
443
+ return htmlTAG;
444
+ }
445
+
446
+ get defaults() {
447
+ return Object.assign({}, super.defaults, {
448
+ shadowMode: false,
449
+ });
450
+ }
451
+
452
+ get value() {
453
+ return this._value ?? '';
454
+ }
455
+
456
+ set value(value) {
457
+ this._value = value ?? '';
458
+ this.setFormValue(this._value);
459
+ }
460
+ }
461
+
462
+ registerCustomElement(NativeLifecycleComponent);
463
+
464
+ const externalForm = document.createElement('form');
465
+ externalForm.id = 'external-form-500';
466
+ document.body.appendChild(externalForm);
467
+
468
+ const externalControl = document.createElement(htmlTAG);
469
+ externalControl.setAttribute('name', 'external');
470
+ externalControl.setAttribute('form', externalForm.id);
471
+ document.body.appendChild(externalControl);
472
+ externalControl.value = 'submitted';
473
+
474
+ expect(new FormData(externalForm).get('external')).to.equal('submitted');
475
+ externalControl.remove();
476
+ expect(externalControl.getAttribute('form')).to.equal(externalForm.id);
477
+ document.body.appendChild(externalControl);
478
+ expect(externalControl.form).to.equal(externalForm);
479
+ expect(new FormData(externalForm).get('external')).to.equal('submitted');
480
+
481
+ const nestedForm = document.createElement('form');
482
+ const fieldset = document.createElement('fieldset');
483
+ const nestedControl = document.createElement(htmlTAG);
484
+ nestedControl.setAttribute('name', 'nested');
485
+ fieldset.appendChild(nestedControl);
486
+ nestedForm.appendChild(fieldset);
487
+ document.body.appendChild(nestedForm);
488
+ nestedControl.value = 'nested-value';
489
+
490
+ fieldset.disabled = true;
491
+ await Promise.resolve();
492
+ expect(nestedControl.hasAttribute('disabled')).to.be.false;
493
+ expect(new FormData(nestedForm).get('nested')).to.equal(null);
494
+
495
+ fieldset.disabled = false;
496
+ await Promise.resolve();
497
+ expect(nestedControl.hasAttribute('disabled')).to.be.false;
498
+ expect(new FormData(nestedForm).get('nested')).to.equal('nested-value');
499
+
500
+ externalControl.remove();
501
+ externalForm.remove();
502
+ nestedForm.remove();
503
+ });
504
+
322
505
 
323
506
  });
324
507
  });
325
508
 
326
509
  });
327
-
@@ -154,6 +154,68 @@ describe('DOM', function () {
154
154
  expect(control.getOption('test')).is.eql(3);
155
155
  expect(control.hasAttribute('data-monster-error')).is.false;
156
156
  });
157
+
158
+ it('should resolve a changed script host attribute issue #506', function () {
159
+ const mocks = document.getElementById('mocks');
160
+ mocks.innerHTML = `
161
+ <div id="call-back-host-a"></div>
162
+ <div id="call-back-host-b"></div>
163
+ <div id="container"></div>
164
+ `;
165
+ document.getElementById('call-back-host-a').resolveHost = () => 'a';
166
+ document.getElementById('call-back-host-b').resolveHost = () => 'b';
167
+
168
+ const control = document.createElement(randomTagNumber);
169
+ control.setAttribute('data-monster-script-host', 'call-back-host-a');
170
+ document.getElementById('container').appendChild(control);
171
+
172
+ expect(control.callCallback('resolveHost', [])).to.equal('a');
173
+ control.setAttribute('data-monster-script-host', 'call-back-host-b');
174
+ expect(control.callCallback('resolveHost', [])).to.equal('b');
175
+ });
176
+
177
+ it('should resolve a replacement host with the same id issue #506', function () {
178
+ const mocks = document.getElementById('mocks');
179
+ mocks.innerHTML = `
180
+ <div id="replaceable-host"></div>
181
+ <div id="container"></div>
182
+ `;
183
+ const original = document.getElementById('replaceable-host');
184
+ original.resolveHost = () => 'original';
185
+ const control = document.createElement(randomTagNumber);
186
+ control.setAttribute('data-monster-script-host', 'replaceable-host');
187
+ document.getElementById('container').appendChild(control);
188
+ expect(control.callCallback('resolveHost', [])).to.equal('original');
189
+
190
+ const replacement = document.createElement('div');
191
+ replacement.id = 'replaceable-host';
192
+ replacement.resolveHost = () => 'replacement';
193
+ original.replaceWith(replacement);
194
+
195
+ expect(control.callCallback('resolveHost', [])).to.equal('replacement');
196
+ });
197
+
198
+ it('should resolve the current ancestor after reparenting issue #506', function () {
199
+ const mocks = document.getElementById('mocks');
200
+ mocks.innerHTML = `
201
+ <div id="ancestor-host"><div id="first-container"></div></div>
202
+ <div><div id="second-container"></div></div>
203
+ `;
204
+ const firstHost = document.getElementById('ancestor-host');
205
+ firstHost.resolveHost = () => 'first';
206
+ const control = document.createElement(randomTagNumber);
207
+ control.setAttribute('data-monster-script-host', 'ancestor-host');
208
+ document.getElementById('first-container').appendChild(control);
209
+ expect(control.callCallback('resolveHost', [])).to.equal('first');
210
+
211
+ firstHost.removeAttribute('id');
212
+ const secondHost = document.getElementById('second-container').parentElement;
213
+ secondHost.id = 'ancestor-host';
214
+ secondHost.resolveHost = () => 'second';
215
+ document.getElementById('second-container').appendChild(control);
216
+
217
+ expect(control.callCallback('resolveHost', [])).to.equal('second');
218
+ });
157
219
  })
158
220
 
159
221
  });