@schukai/monster 4.136.8 → 4.136.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md
CHANGED
|
@@ -18,6 +18,8 @@
|
|
|
18
18
|
- **message-state-button:** distinguish overlay, prose, and wide message layouts for smart popper sizing and overflow ([#401](https://gitlab.schukai.com/oss/libraries/javascript/monster/issues/401))
|
|
19
19
|
- **popper:** support kebab-case camelCase option attributes such as `data-monster-option-popper-content-overflow` ([#401](https://gitlab.schukai.com/oss/libraries/javascript/monster/issues/401))
|
|
20
20
|
- **popper/select:** let nested `monster-select` poppers escape parent popper content wrappers without shrinking normal parent content sizing ([#416](https://gitlab.schukai.com/oss/libraries/javascript/monster/-/work_items/416))
|
|
21
|
+
- **select:** skip hidden remote info requests when `showRemoteInfo` is disabled ([#418](https://gitlab.schukai.com/oss/libraries/javascript/monster/-/work_items/418))
|
|
22
|
+
- **select:** defer remote info loading until the dropdown opens and reuse pagination totals from remote option fetches when available ([#418](https://gitlab.schukai.com/oss/libraries/javascript/monster/-/work_items/418))
|
|
21
23
|
|
|
22
24
|
### Changes
|
|
23
25
|
|
package/package.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"author":"Volker Schukai","dependencies":{"@floating-ui/dom":"^1.7.6"},"description":"Monster is a simple library for creating fast, robust and lightweight websites.","homepage":"https://monsterjs.org/","keywords":["framework","web","dom","css","sass","mobile-first","app","front-end","templates","schukai","core","shopcloud","alvine","monster","buildmap","stack","observer","observable","uuid","node","nodelist","css-in-js","logger","log","theme"],"license":"AGPL 3.0","main":"source/monster.mjs","module":"source/monster.mjs","name":"@schukai/monster","repository":{"type":"git","url":"https://gitlab.schukai.com/oss/libraries/javascript/monster.git"},"type":"module","version":"4.136.
|
|
1
|
+
{"author":"Volker Schukai","dependencies":{"@floating-ui/dom":"^1.7.6"},"description":"Monster is a simple library for creating fast, robust and lightweight websites.","homepage":"https://monsterjs.org/","keywords":["framework","web","dom","css","sass","mobile-first","app","front-end","templates","schukai","core","shopcloud","alvine","monster","buildmap","stack","observer","observable","uuid","node","nodelist","css-in-js","logger","log","theme"],"license":"AGPL 3.0","main":"source/monster.mjs","module":"source/monster.mjs","name":"@schukai/monster","repository":{"type":"git","url":"https://gitlab.schukai.com/oss/libraries/javascript/monster.git"},"type":"module","version":"4.136.10"}
|
|
@@ -306,6 +306,7 @@ const lookupCacheSymbol = Symbol("lookupCache");
|
|
|
306
306
|
const lookupInProgressSymbol = Symbol("lookupInProgress");
|
|
307
307
|
const unresolvedSelectionValuesSymbol = Symbol("unresolvedSelectionValues");
|
|
308
308
|
const fetchRequestVersionSymbol = Symbol("fetchRequestVersion");
|
|
309
|
+
const remoteInfoRequestSymbol = Symbol("remoteInfoRequest");
|
|
309
310
|
|
|
310
311
|
/**
|
|
311
312
|
* @private
|
|
@@ -458,6 +459,28 @@ class Select extends CustomControl {
|
|
|
458
459
|
/**
|
|
459
460
|
* Defines the default configuration options for the monster-control.
|
|
460
461
|
* These options can be overridden via the HTML attribute `data-monster-options`.
|
|
462
|
+
* Use JSON in `data-monster-options` when option values must keep their JSON
|
|
463
|
+
* types, for example:
|
|
464
|
+
*
|
|
465
|
+
* ```html
|
|
466
|
+
* <monster-select
|
|
467
|
+
* data-monster-options='{"empty":{"equivalents":[null,"",0]}}'>
|
|
468
|
+
* </monster-select>
|
|
469
|
+
* ```
|
|
470
|
+
*
|
|
471
|
+
* When the value comes from an updater object, bind it through
|
|
472
|
+
* `data-monster-properties`. The bound object value keeps its JavaScript type,
|
|
473
|
+
* so an array such as `[null, "", 0]` keeps the numeric `0` as a number:
|
|
474
|
+
*
|
|
475
|
+
* ```html
|
|
476
|
+
* <monster-select
|
|
477
|
+
* data-monster-properties="option:empty.equivalents path:emptyEquivalents">
|
|
478
|
+
* </monster-select>
|
|
479
|
+
* ```
|
|
480
|
+
*
|
|
481
|
+
* Do not use `data-monster-option-empty-equivalents` for typed primitive
|
|
482
|
+
* values; array option attributes are split as strings and cannot express a
|
|
483
|
+
* numeric `0`.
|
|
461
484
|
* @see {@link https://monsterjs.org/en/doc/#configurate-a-monster-control}
|
|
462
485
|
*
|
|
463
486
|
* @property {string[]} toggleEventType - Array of DOM event names (e.g., ["click", "touch"]) that toggle the dropdown.
|
|
@@ -528,7 +551,7 @@ class Select extends CustomControl {
|
|
|
528
551
|
* @property {Object} empty - Handling of empty or undefined values.
|
|
529
552
|
* @property {string} empty.defaultValueRadio - Default value for `type="radio"` when no selection exists.
|
|
530
553
|
* @property {Array} empty.defaultValueCheckbox - Default value (empty array) for `type="checkbox"`.
|
|
531
|
-
* @property {Array} empty.equivalents - Values that are considered "empty" (e.g., `undefined`, `null`, `""`) and are normalized to the default value.
|
|
554
|
+
* @property {Array} empty.equivalents - Values that are considered "empty" (e.g., `undefined`, `null`, `""`, `NaN`) and are normalized to the default value. Matching is type-safe: a configured string `"0"` only matches the string `"0"`; configure numeric `0` explicitly, via `data-monster-options` JSON, `data-monster-properties`, or `setOption()`, when the number `0` should be treated as empty.
|
|
532
555
|
* @property {Object} formatter - Functions for formatting display values.
|
|
533
556
|
* @property {Function} formatter.selection - Callback `(value, option) => string` to format the display text of selected values.
|
|
534
557
|
* @property {Object} classes - CSS classes for various elements.
|
|
@@ -698,7 +721,6 @@ class Select extends CustomControl {
|
|
|
698
721
|
checkOptionState.call(this);
|
|
699
722
|
calcAndSetOptionsDimension.call(this);
|
|
700
723
|
updatePopper.call(this);
|
|
701
|
-
initTotal.call(this);
|
|
702
724
|
})
|
|
703
725
|
.catch((e) => {
|
|
704
726
|
addErrorAttribute(this, e);
|
|
@@ -721,8 +743,6 @@ class Select extends CustomControl {
|
|
|
721
743
|
let lazyLoadFlag = self.getOption("features.lazyLoad", false);
|
|
722
744
|
const remoteFilterFlag = getFilterMode.call(this) === FILTER_MODE_REMOTE;
|
|
723
745
|
|
|
724
|
-
initTotal.call(self);
|
|
725
|
-
|
|
726
746
|
if (getFilterMode.call(this) === FILTER_MODE_REMOTE) {
|
|
727
747
|
self.setOption("features.lazyLoad", false);
|
|
728
748
|
lazyLoadFlag = false;
|
|
@@ -4113,6 +4133,9 @@ function convertSelectionToValue(selection) {
|
|
|
4113
4133
|
* @private
|
|
4114
4134
|
* @param value
|
|
4115
4135
|
* @returns {boolean}
|
|
4136
|
+
*
|
|
4137
|
+
* Empty-equivalent matching is intentionally type-safe. Consumers that want
|
|
4138
|
+
* numeric `0` to count as empty must configure `0`, not `"0"`.
|
|
4116
4139
|
*/
|
|
4117
4140
|
function isValueIsEmpty(value) {
|
|
4118
4141
|
let equivalents = this.getOption("empty.equivalents");
|
|
@@ -4123,7 +4146,7 @@ function isValueIsEmpty(value) {
|
|
|
4123
4146
|
equivalents = [equivalents];
|
|
4124
4147
|
}
|
|
4125
4148
|
|
|
4126
|
-
return equivalents.
|
|
4149
|
+
return equivalents.includes(value);
|
|
4127
4150
|
}
|
|
4128
4151
|
|
|
4129
4152
|
/**
|
|
@@ -4415,8 +4438,10 @@ function show() {
|
|
|
4415
4438
|
const shouldLoadRemoteOptions =
|
|
4416
4439
|
getFilterMode.call(self) === FILTER_MODE_REMOTE &&
|
|
4417
4440
|
getOptionElements.call(self).length === 0;
|
|
4441
|
+
const shouldLoadDefaultOptions =
|
|
4442
|
+
shouldUseDefaultOptionsUrl.call(self, getCurrentFilterValue.call(self));
|
|
4418
4443
|
|
|
4419
|
-
if (
|
|
4444
|
+
if (shouldLoadDefaultOptions) {
|
|
4420
4445
|
setTimeout(() => {
|
|
4421
4446
|
loadDefaultOptionsFromUrl.call(self).catch((e) => {
|
|
4422
4447
|
addErrorAttribute(self, e);
|
|
@@ -4429,6 +4454,8 @@ function show() {
|
|
|
4429
4454
|
addErrorAttribute(self, e);
|
|
4430
4455
|
});
|
|
4431
4456
|
}, 0);
|
|
4457
|
+
} else {
|
|
4458
|
+
initTotal.call(self);
|
|
4432
4459
|
}
|
|
4433
4460
|
calcAndSetOptionsDimension.call(this);
|
|
4434
4461
|
focusFilter.call(this);
|
|
@@ -4500,6 +4527,18 @@ function initTotal() {
|
|
|
4500
4527
|
return;
|
|
4501
4528
|
}
|
|
4502
4529
|
|
|
4530
|
+
if (this.getOption("features.showRemoteInfo") !== true) {
|
|
4531
|
+
return;
|
|
4532
|
+
}
|
|
4533
|
+
|
|
4534
|
+
if (isInteger(this.getOption("total"))) {
|
|
4535
|
+
return;
|
|
4536
|
+
}
|
|
4537
|
+
|
|
4538
|
+
if (this[remoteInfoRequestSymbol]) {
|
|
4539
|
+
return;
|
|
4540
|
+
}
|
|
4541
|
+
|
|
4503
4542
|
const url = this.getOption("remoteInfo.url");
|
|
4504
4543
|
const mappingTotal = this.getOption("mapping.total");
|
|
4505
4544
|
|
|
@@ -4509,7 +4548,7 @@ function initTotal() {
|
|
|
4509
4548
|
|
|
4510
4549
|
const fetchOptions = this.getOption("fetch", {});
|
|
4511
4550
|
|
|
4512
|
-
getGlobal()
|
|
4551
|
+
const remoteInfoRequest = getGlobal()
|
|
4513
4552
|
.fetch(url, fetchOptions)
|
|
4514
4553
|
.then((response) => {
|
|
4515
4554
|
if (!response.ok) {
|
|
@@ -4533,7 +4572,14 @@ function initTotal() {
|
|
|
4533
4572
|
})
|
|
4534
4573
|
.catch((e) => {
|
|
4535
4574
|
addErrorAttribute(this, e);
|
|
4575
|
+
})
|
|
4576
|
+
.finally(() => {
|
|
4577
|
+
if (this[remoteInfoRequestSymbol] === remoteInfoRequest) {
|
|
4578
|
+
this[remoteInfoRequestSymbol] = null;
|
|
4579
|
+
}
|
|
4536
4580
|
});
|
|
4581
|
+
|
|
4582
|
+
this[remoteInfoRequestSymbol] = remoteInfoRequest;
|
|
4537
4583
|
}
|
|
4538
4584
|
|
|
4539
4585
|
function updatePagination(total, currentPage, objectsPerPage) {
|
|
@@ -790,6 +790,155 @@ describe('Select', function () {
|
|
|
790
790
|
}, 150);
|
|
791
791
|
});
|
|
792
792
|
|
|
793
|
+
it('should not eagerly fetch remote info when showRemoteInfo is disabled', function (done) {
|
|
794
|
+
this.timeout(3000);
|
|
795
|
+
|
|
796
|
+
let mocks = document.getElementById('mocks');
|
|
797
|
+
const requests = [];
|
|
798
|
+
const previousFetch = global['fetch'];
|
|
799
|
+
|
|
800
|
+
global['fetch'] = function (url) {
|
|
801
|
+
requests.push(String(url));
|
|
802
|
+
|
|
803
|
+
return createJsonResponse({
|
|
804
|
+
items: [
|
|
805
|
+
{id: 'alpha', name: 'Alpha'}
|
|
806
|
+
],
|
|
807
|
+
pagination: {
|
|
808
|
+
total: 3,
|
|
809
|
+
page: 2,
|
|
810
|
+
perPage: 1
|
|
811
|
+
}
|
|
812
|
+
});
|
|
813
|
+
};
|
|
814
|
+
|
|
815
|
+
const select = document.createElement('monster-select');
|
|
816
|
+
select.setOption('url', 'https://example.com/items?filter={filter}&page={page}');
|
|
817
|
+
select.setOption('filter.mode', 'remote');
|
|
818
|
+
select.setOption('mapping.selector', 'items.*');
|
|
819
|
+
select.setOption('mapping.labelTemplate', '${name}');
|
|
820
|
+
select.setOption('mapping.valueTemplate', '${id}');
|
|
821
|
+
select.setOption('mapping.total', 'pagination.total');
|
|
822
|
+
select.setOption('mapping.currentPage', 'pagination.page');
|
|
823
|
+
select.setOption('mapping.objectsPerPage', 'pagination.perPage');
|
|
824
|
+
select.setOption('remoteInfo.url', 'https://example.com/remote-info');
|
|
825
|
+
select.setOption('features.showRemoteInfo', false);
|
|
826
|
+
mocks.appendChild(select);
|
|
827
|
+
|
|
828
|
+
setTimeout(() => {
|
|
829
|
+
try {
|
|
830
|
+
expect(requests).to.deep.equal([]);
|
|
831
|
+
} catch (e) {
|
|
832
|
+
global['fetch'] = previousFetch;
|
|
833
|
+
return done(e);
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
select.fetch('https://example.com/items?filter=*&page=2')
|
|
837
|
+
.then(() => {
|
|
838
|
+
try {
|
|
839
|
+
const pagination = select.shadowRoot.querySelector('[data-monster-role=pagination]');
|
|
840
|
+
|
|
841
|
+
expect(requests).to.deep.equal([
|
|
842
|
+
'https://example.com/items?filter=*&page=2'
|
|
843
|
+
]);
|
|
844
|
+
expect(select.getOption('total')).to.equal(3);
|
|
845
|
+
expect(pagination.getOption('currentPage')).to.equal(2);
|
|
846
|
+
expect(pagination.getOption('pages')).to.equal(3);
|
|
847
|
+
expect(pagination.getOption('objectsPerPage')).to.equal(1);
|
|
848
|
+
} catch (e) {
|
|
849
|
+
return done(e);
|
|
850
|
+
} finally {
|
|
851
|
+
global['fetch'] = previousFetch;
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
done();
|
|
855
|
+
})
|
|
856
|
+
.catch((e) => {
|
|
857
|
+
global['fetch'] = previousFetch;
|
|
858
|
+
done(e);
|
|
859
|
+
});
|
|
860
|
+
}, 150);
|
|
861
|
+
});
|
|
862
|
+
|
|
863
|
+
it('should defer remote info fetching until the dropdown is opened', async function () {
|
|
864
|
+
this.timeout(4000);
|
|
865
|
+
|
|
866
|
+
let mocks = document.getElementById('mocks');
|
|
867
|
+
const requests = [];
|
|
868
|
+
const remoteInfoUrl = 'https://example.com/remote-info';
|
|
869
|
+
|
|
870
|
+
global['fetch'] = function (url) {
|
|
871
|
+
requests.push(String(url));
|
|
872
|
+
|
|
873
|
+
return createJsonResponse({
|
|
874
|
+
pagination: {
|
|
875
|
+
total: 5
|
|
876
|
+
}
|
|
877
|
+
});
|
|
878
|
+
};
|
|
879
|
+
|
|
880
|
+
const select = document.createElement('monster-select');
|
|
881
|
+
select.setOption('filter.mode', 'remote');
|
|
882
|
+
select.setOption('filter.position', 'popper');
|
|
883
|
+
select.setOption('mapping.total', 'pagination.total');
|
|
884
|
+
select.setOption('remoteInfo.url', remoteInfoUrl);
|
|
885
|
+
select.setOption('options', [
|
|
886
|
+
{label: 'Alpha', value: 'alpha'}
|
|
887
|
+
]);
|
|
888
|
+
mocks.appendChild(select);
|
|
889
|
+
|
|
890
|
+
await waitForCondition(() => {
|
|
891
|
+
return select.shadowRoot.querySelector('[data-monster-role=container]') instanceof HTMLElement;
|
|
892
|
+
});
|
|
893
|
+
|
|
894
|
+
expect(requests).to.deep.equal([]);
|
|
895
|
+
|
|
896
|
+
const container = select.shadowRoot.querySelector('[data-monster-role=container]');
|
|
897
|
+
|
|
898
|
+
container.click();
|
|
899
|
+
await waitForCondition(() => requests.includes(remoteInfoUrl));
|
|
900
|
+
expect(requests.filter((url) => url === remoteInfoUrl)).to.have.length(1);
|
|
901
|
+
});
|
|
902
|
+
|
|
903
|
+
it('should keep empty equivalent matching type-safe for numeric zero', function (done) {
|
|
904
|
+
this.timeout(2000);
|
|
905
|
+
|
|
906
|
+
let mocks = document.getElementById('mocks');
|
|
907
|
+
const stringEquivalentSelect = document.createElement('monster-select');
|
|
908
|
+
stringEquivalentSelect.setOption('empty.equivalents', ['0']);
|
|
909
|
+
mocks.appendChild(stringEquivalentSelect);
|
|
910
|
+
|
|
911
|
+
const numericEquivalentSelect = document.createElement('monster-select');
|
|
912
|
+
numericEquivalentSelect.setOption('empty.equivalents', [0]);
|
|
913
|
+
mocks.appendChild(numericEquivalentSelect);
|
|
914
|
+
|
|
915
|
+
setTimeout(() => {
|
|
916
|
+
stringEquivalentSelect.value = 0;
|
|
917
|
+
numericEquivalentSelect.value = 0;
|
|
918
|
+
|
|
919
|
+
setTimeout(() => {
|
|
920
|
+
try {
|
|
921
|
+
expect(stringEquivalentSelect.value).to.equal('0');
|
|
922
|
+
expect(stringEquivalentSelect.getOption('selection')).to.deep.equal([
|
|
923
|
+
{
|
|
924
|
+
label: '0',
|
|
925
|
+
value: 0,
|
|
926
|
+
class: 'monster-badge-primary',
|
|
927
|
+
unresolved: false
|
|
928
|
+
}
|
|
929
|
+
]);
|
|
930
|
+
|
|
931
|
+
expect(numericEquivalentSelect.value).to.equal('');
|
|
932
|
+
expect(numericEquivalentSelect.getOption('selection')).to.deep.equal([]);
|
|
933
|
+
} catch (e) {
|
|
934
|
+
return done(e);
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
done();
|
|
938
|
+
}, 50);
|
|
939
|
+
}, 50);
|
|
940
|
+
});
|
|
941
|
+
|
|
793
942
|
it('should not refetch after selecting an already loaded remote option', function (done) {
|
|
794
943
|
this.timeout(3000);
|
|
795
944
|
|