@schukai/monster 4.136.3 → 4.136.4

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/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.3"}
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.4"}
@@ -1765,55 +1765,20 @@ function getTranslations() {
1765
1765
  */
1766
1766
  function lookupSelection() {
1767
1767
  const self = this;
1768
+ const IntersectionObserverImplementation =
1769
+ getGlobal().IntersectionObserver;
1768
1770
 
1769
- const observer = new IntersectionObserver(
1771
+ if (!(IntersectionObserverImplementation instanceof Function)) {
1772
+ runSelectionLookupWhenVisible(self);
1773
+ return;
1774
+ }
1775
+
1776
+ const observer = new IntersectionObserverImplementation(
1770
1777
  (entries, obs) => {
1771
1778
  for (const entry of entries) {
1772
1779
  if (entry.isIntersecting) {
1773
1780
  obs.disconnect();
1774
-
1775
- setTimeout(() => {
1776
- const selection = self.getOption("selection");
1777
- if (
1778
- selection.length === 0 ||
1779
- self[isLoadingSymbol] ||
1780
- self[lazyLoadDoneSymbol]
1781
- ) {
1782
- return;
1783
- }
1784
-
1785
- let url = self.getOption("lookup.url") || self.getOption("url");
1786
- self[cleanupOptionsListSymbol] = false;
1787
-
1788
- if (self.getOption("lookup.grouping") === true) {
1789
- const values = selection
1790
- .map((s) => s?.["value"])
1791
- .filter(
1792
- (value) => isEmptyLookupValue.call(self, value) === false,
1793
- );
1794
- if (values.length === 0) {
1795
- return;
1796
- }
1797
- filterFromRemoteByValue
1798
- .call(self, url, { filter: values.join(",") })
1799
- .catch((e) => {
1800
- addErrorAttribute(self, e);
1801
- });
1802
- return;
1803
- }
1804
-
1805
- for (const s of selection) {
1806
- const value = s?.["value"];
1807
- if (isEmptyLookupValue.call(self, value)) {
1808
- continue;
1809
- }
1810
- filterFromRemoteByValue
1811
- .call(self, url, { filter: value })
1812
- .catch((e) => {
1813
- addErrorAttribute(self, e);
1814
- });
1815
- }
1816
- }, 100);
1781
+ runSelectionLookupWhenVisible(self);
1817
1782
  }
1818
1783
  }
1819
1784
  },
@@ -1823,6 +1788,49 @@ function lookupSelection() {
1823
1788
  observer.observe(self);
1824
1789
  }
1825
1790
 
1791
+ function runSelectionLookupWhenVisible(self) {
1792
+ setTimeout(() => {
1793
+ const selection = self.getOption("selection");
1794
+ if (
1795
+ selection.length === 0 ||
1796
+ self[isLoadingSymbol] ||
1797
+ self[lazyLoadDoneSymbol]
1798
+ ) {
1799
+ return;
1800
+ }
1801
+
1802
+ let url = self.getOption("lookup.url") || self.getOption("url");
1803
+ self[cleanupOptionsListSymbol] = false;
1804
+
1805
+ if (self.getOption("lookup.grouping") === true) {
1806
+ const values = selection
1807
+ .map((s) => s?.["value"])
1808
+ .filter((value) => isEmptyLookupValue.call(self, value) === false);
1809
+ if (values.length === 0) {
1810
+ return;
1811
+ }
1812
+ filterFromRemoteByValue
1813
+ .call(self, url, { filter: values.join(",") })
1814
+ .catch((e) => {
1815
+ addErrorAttribute(self, e);
1816
+ });
1817
+ return;
1818
+ }
1819
+
1820
+ for (const s of selection) {
1821
+ const value = s?.["value"];
1822
+ if (isEmptyLookupValue.call(self, value)) {
1823
+ continue;
1824
+ }
1825
+ filterFromRemoteByValue
1826
+ .call(self, url, { filter: value })
1827
+ .catch((e) => {
1828
+ addErrorAttribute(self, e);
1829
+ });
1830
+ }
1831
+ }, 100);
1832
+ }
1833
+
1826
1834
  /**
1827
1835
  * @private
1828
1836
  * @param {*} value
@@ -467,8 +467,12 @@ function syncNestedScrollContainerHeight(contentElement, contentMaxHeight) {
467
467
  }
468
468
 
469
469
  if (Number.isFinite(contentMaxHeight) && contentMaxHeight > 0) {
470
- nestedScrollableElement.style.height = `${contentMaxHeight}px`;
471
- nestedScrollableElement.style.maxHeight = `${contentMaxHeight}px`;
470
+ const nextNestedHeight = resolveNestedScrollContainerHeight(
471
+ nestedScrollableElement,
472
+ contentMaxHeight,
473
+ );
474
+ nestedScrollableElement.style.height = `${nextNestedHeight}px`;
475
+ nestedScrollableElement.style.maxHeight = `${nextNestedHeight}px`;
472
476
  return;
473
477
  }
474
478
 
@@ -476,6 +480,41 @@ function syncNestedScrollContainerHeight(contentElement, contentMaxHeight) {
476
480
  nestedScrollableElement.style.maxHeight = "";
477
481
  }
478
482
 
483
+ function resolveNestedScrollContainerHeight(
484
+ nestedScrollableElement,
485
+ contentMaxHeight,
486
+ ) {
487
+ const declaredHeight = readDeclaredDimension(nestedScrollableElement, "height");
488
+ const declaredMaxHeight = readDeclaredDimension(
489
+ nestedScrollableElement,
490
+ "maxHeight",
491
+ );
492
+ const scrollHeight = nestedScrollableElement.scrollHeight;
493
+ const preferredHeightCandidates = [
494
+ declaredHeight,
495
+ declaredMaxHeight,
496
+ scrollHeight,
497
+ ];
498
+ const preferredHeight = preferredHeightCandidates.find((value) => {
499
+ return Number.isFinite(value) && value > 0;
500
+ });
501
+
502
+ if (Number.isFinite(preferredHeight) && preferredHeight > 0) {
503
+ return Math.min(contentMaxHeight, preferredHeight);
504
+ }
505
+
506
+ return contentMaxHeight;
507
+ }
508
+
509
+ function readDeclaredDimension(element, property) {
510
+ if (!(element instanceof HTMLElement)) {
511
+ return NaN;
512
+ }
513
+
514
+ const value = Number.parseFloat(element.style?.[property] || "");
515
+ return Number.isFinite(value) ? value : NaN;
516
+ }
517
+
479
518
  function syncPreferredFloatingWidth(floatingElement, maxWidth) {
480
519
  const preferredWidth = Number.parseFloat(
481
520
  floatingElement.dataset.monsterPreferredWidth || "",
@@ -207,4 +207,34 @@ describe("form floating-ui boundary resolution", function () {
207
207
 
208
208
  expect(content.style.maxHeight).to.equal("26px");
209
209
  });
210
+
211
+ it("should respect a smaller nested scroll container height", function () {
212
+ const mocks = document.getElementById("mocks");
213
+ const popper = document.createElement("div");
214
+ const content = document.createElement("div");
215
+ const options = document.createElement("div");
216
+
217
+ content.setAttribute("part", "content");
218
+ content.style.overflowY = "hidden";
219
+ options.style.overflowY = "auto";
220
+ options.style.height = "72px";
221
+ options.style.maxHeight = "72px";
222
+ Object.defineProperty(options, "scrollHeight", {
223
+ configurable: true,
224
+ value: 72,
225
+ });
226
+
227
+ content.appendChild(options);
228
+ popper.appendChild(content);
229
+ mocks.appendChild(popper);
230
+
231
+ applyAdaptiveFloatingElementSize(popper, {
232
+ availableWidth: 220,
233
+ availableHeight: 180,
234
+ });
235
+
236
+ expect(content.style.maxHeight).to.equal("180px");
237
+ expect(options.style.height).to.equal("72px");
238
+ expect(options.style.maxHeight).to.equal("72px");
239
+ });
210
240
  });
@@ -261,44 +261,39 @@ describe("MessageStateButton", function () {
261
261
  }, 0);
262
262
  });
263
263
 
264
- it("should resolve nested select message content to horizontal clipping only", function (done) {
264
+ it("should resolve nested select message content to horizontal clipping only", async function () {
265
265
  let mocks = document.getElementById("mocks");
266
266
  const button = document.createElement("monster-message-state-button");
267
267
  button.innerHTML = "Save";
268
268
  mocks.appendChild(button);
269
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();
270
+ const wrapper = document.createElement("div");
271
+ wrapper.appendChild(document.createElement("monster-select"));
272
+ button.setMessage(wrapper);
273
+ button.showMessage();
274
+
275
+ await waitForCondition(() => {
276
+ const content = button.shadowRoot?.querySelector('[part="content"]');
277
+ return (
278
+ content?.getAttribute("data-monster-overflow-mode") === "horizontal"
279
+ );
280
+ });
276
281
 
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);
282
+ const content = button.shadowRoot.querySelector('[part="content"]');
283
+ const message = button.shadowRoot.querySelector(
284
+ '[data-monster-role="message"]',
285
+ );
286
+
287
+ expect(content).to.exist;
288
+ expect(content.getAttribute("data-monster-overflow-mode")).to.equal(
289
+ "horizontal",
290
+ );
291
+ expect(content.getAttribute("data-monster-message-layout")).to.equal(
292
+ "overlay",
293
+ );
294
+ expect(message.getAttribute("data-monster-message-layout")).to.equal(
295
+ "overlay",
296
+ );
302
297
  });
303
298
 
304
299
  it("should resolve wide plain content to the wide layout", function (done) {
@@ -489,3 +484,30 @@ describe("MessageStateButton", function () {
489
484
  });
490
485
  });
491
486
  });
487
+
488
+ function waitForCondition(check, { timeout = 4000, interval = 25 } = {}) {
489
+ return new Promise((resolve, reject) => {
490
+ const start = Date.now();
491
+
492
+ const poll = () => {
493
+ try {
494
+ if (check()) {
495
+ resolve();
496
+ return;
497
+ }
498
+ } catch (error) {
499
+ reject(error);
500
+ return;
501
+ }
502
+
503
+ if (Date.now() - start >= timeout) {
504
+ reject(new Error("Timed out while waiting for test condition."));
505
+ return;
506
+ }
507
+
508
+ setTimeout(poll, interval);
509
+ };
510
+
511
+ poll();
512
+ });
513
+ }
@@ -549,6 +549,46 @@ describe('Select', function () {
549
549
  }, 50);
550
550
  });
551
551
 
552
+ it('should not throw when IntersectionObserver is unavailable', function (done) {
553
+ this.timeout(2000);
554
+
555
+ let mocks = document.getElementById('mocks');
556
+ const savedIntersectionObserver = global.IntersectionObserver;
557
+ global.IntersectionObserver = undefined;
558
+ window.IntersectionObserver = undefined;
559
+ const failures = [];
560
+ const onError = (event) => {
561
+ failures.push(event?.error || event);
562
+ };
563
+ window.addEventListener('error', onError);
564
+
565
+ const select = document.createElement('monster-select');
566
+ select.setOption('url', 'https://example.com/items?filter={filter}&page={page}');
567
+ select.setOption('filter.mode', 'remote');
568
+ select.setOption('mapping.selector', 'items.*');
569
+ select.setOption('mapping.labelTemplate', '${name}');
570
+ select.setOption('mapping.valueTemplate', '${id}');
571
+ select.setOption('mapping.total', 'pagination.total');
572
+ select.setOption('mapping.currentPage', 'pagination.page');
573
+ select.setOption('mapping.objectsPerPage', 'pagination.perPage');
574
+ select.setOption('selection', [{value: 'alpha'}]);
575
+ mocks.appendChild(select);
576
+
577
+ setTimeout(() => {
578
+ try {
579
+ expect(failures).to.have.length(0);
580
+ } catch (e) {
581
+ return done(e);
582
+ } finally {
583
+ window.removeEventListener('error', onError);
584
+ global.IntersectionObserver = savedIntersectionObserver;
585
+ window.IntersectionObserver = savedIntersectionObserver;
586
+ }
587
+
588
+ done();
589
+ }, 250);
590
+ });
591
+
552
592
  });
553
593
 
554
594
 
@@ -14,6 +14,7 @@ export function setupIntersectionObserverMock(
14
14
  } = {}) {
15
15
 
16
16
  const savedImplementation = window.IntersectionObserver;
17
+ const savedGlobalImplementation = global.IntersectionObserver;
17
18
 
18
19
  let lastObject;
19
20
 
@@ -61,9 +62,10 @@ export function setupIntersectionObserverMock(
61
62
  return {
62
63
  restore: function () {
63
64
  window.IntersectionObserver = savedImplementation;
65
+ global.IntersectionObserver = savedGlobalImplementation;
64
66
  },
65
67
  getInstance: function () {
66
68
  return lastObject;
67
69
  }
68
70
  }
69
- }
71
+ }