@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.8"}
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.indexOf(value) !== -1;
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 (shouldUseDefaultOptionsUrl.call(self, getCurrentFilterValue.call(self))) {
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