@schukai/monster 4.129.6 → 4.129.8

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.129.6"}
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.129.8"}
@@ -98,6 +98,7 @@ class ConfirmButton extends PopperButton {
98
98
  },
99
99
  });
100
100
 
101
+ obj["popper"]["strategy"] = "fixed";
101
102
  obj["classes"]["confirmButton"] = "monster-button-primary";
102
103
  obj["classes"]["cancelButton"] = "monster-button-secondary";
103
104
  obj["actions"]["cancel"] = (e) => {
@@ -28,6 +28,7 @@ import { Processing } from "../../../util/processing.mjs";
28
28
 
29
29
  export {
30
30
  closePositionedPopper,
31
+ resolveClippingBoundaryElement,
31
32
  isPositionedPopperOpen,
32
33
  openPositionedPopper,
33
34
  positionPopper,
@@ -43,7 +44,7 @@ const autoUpdateCleanupMap = new WeakMap();
43
44
  * @return {Promise|*}
44
45
  */
45
46
  function positionPopper(controlElement, popperElement, options) {
46
- const config = normalizePopperConfig(options);
47
+ const config = normalizePopperConfig(options, controlElement, popperElement);
47
48
 
48
49
  return new Processing(() => {
49
50
  enableFloatingPositioning(controlElement, popperElement, config);
@@ -51,15 +52,15 @@ function positionPopper(controlElement, popperElement, options) {
51
52
  }
52
53
 
53
54
  function openPositionedPopper(controlElement, popperElement, options) {
54
- const config = normalizePopperConfig(options);
55
+ const config = normalizePopperConfig(options, controlElement, popperElement);
55
56
 
56
57
  stopAutoUpdate(popperElement);
57
58
  popperElement.style.display = "block";
59
+ popperElement.style.position = config.strategy;
58
60
  popperElement.style.removeProperty("transform");
59
61
 
60
62
  // Keep the call signature stable even though only Floating UI is used now.
61
63
  void controlElement;
62
- void config;
63
64
  }
64
65
 
65
66
  function enableFloatingPositioning(controlElement, popperElement, config) {
@@ -93,6 +94,7 @@ function syncFloatingPopover(controlElement, popperElement, config) {
93
94
  Object.assign(popperElement.style, {
94
95
  top: "0",
95
96
  left: "0",
97
+ position: config.strategy,
96
98
  transform: `translate(${roundByDPR(x)}px,${roundByDPR(y)}px)`,
97
99
  });
98
100
 
@@ -106,6 +108,7 @@ function closePositionedPopper(popperElement) {
106
108
  stopAutoUpdate(popperElement);
107
109
  popperElement.style.display = "none";
108
110
  popperElement.style.removeProperty("visibility");
111
+ popperElement.style.removeProperty("position");
109
112
  popperElement.style.removeProperty("transform");
110
113
  }
111
114
 
@@ -113,23 +116,39 @@ function isPositionedPopperOpen(popperElement) {
113
116
  return popperElement.style.display === "block";
114
117
  }
115
118
 
116
- function normalizePopperConfig(options) {
119
+ function normalizePopperConfig(options, controlElement, popperElement) {
117
120
  const config = Object.assign(
118
121
  {},
119
122
  {
120
123
  placement: "top",
121
124
  engine: "floating",
125
+ strategy: "absolute",
122
126
  },
123
127
  options,
124
128
  );
125
129
 
130
+ config.boundaryElement = resolveClippingBoundaryElement(
131
+ controlElement,
132
+ popperElement,
133
+ );
134
+ config.detectOverflowOptions = buildDetectOverflowOptions(
135
+ config.boundaryElement,
136
+ );
126
137
  config.middleware = normalizeMiddleware(config);
127
138
  config.middlewareTokens = config.middleware.filter((line) => isString(line));
128
139
  config.floatingMiddleware = buildFloatingMiddleware(
129
140
  config.middleware,
130
141
  config.placement,
142
+ config.detectOverflowOptions,
143
+ popperElement,
131
144
  );
132
145
 
146
+ if (!config.middlewareTokens.includes("size")) {
147
+ config.floatingMiddleware.push(
148
+ createAdaptiveSizeMiddleware(config.detectOverflowOptions, popperElement),
149
+ );
150
+ }
151
+
133
152
  return config;
134
153
  }
135
154
 
@@ -146,7 +165,12 @@ function normalizeMiddleware(config) {
146
165
  return [];
147
166
  }
148
167
 
149
- function buildFloatingMiddleware(middleware, placement) {
168
+ function buildFloatingMiddleware(
169
+ middleware,
170
+ placement,
171
+ detectOverflowOptions,
172
+ popperElement,
173
+ ) {
150
174
  const result = [...middleware];
151
175
 
152
176
  for (const key in result) {
@@ -166,10 +190,10 @@ function buildFloatingMiddleware(middleware, placement) {
166
190
 
167
191
  switch (fn) {
168
192
  case "flip":
169
- result[key] = flip();
193
+ result[key] = flip(detectOverflowOptions);
170
194
  break;
171
195
  case "shift":
172
- result[key] = shift();
196
+ result[key] = shift(detectOverflowOptions);
173
197
  break;
174
198
  case "autoPlacement":
175
199
  let defaultAllowedPlacements = ["top", "bottom", "left", "right"];
@@ -189,31 +213,28 @@ function buildFloatingMiddleware(middleware, placement) {
189
213
  }
190
214
  defaultAllowedPlacements.unshift(placement);
191
215
 
192
- result[key] = autoPlacement({
193
- crossAxis: true,
194
- autoAlignment: true,
195
- allowedPlacements: defaultAllowedPlacements,
196
- });
216
+ result[key] = autoPlacement(
217
+ Object.assign({}, detectOverflowOptions, {
218
+ crossAxis: true,
219
+ autoAlignment: true,
220
+ allowedPlacements: defaultAllowedPlacements,
221
+ }),
222
+ );
197
223
  break;
198
224
  case "arrow":
199
225
  result[key] = null;
200
226
  break;
201
227
  case "size":
202
- result[key] = size({
203
- apply({ availableWidth, availableHeight, elements }) {
204
- Object.assign(elements.floating.style, {
205
- boxSizing: "border-box",
206
- maxWidth: `${availableWidth}px`,
207
- maxHeight: `${availableHeight}px`,
208
- });
209
- },
210
- });
228
+ result[key] = createAdaptiveSizeMiddleware(
229
+ detectOverflowOptions,
230
+ popperElement,
231
+ );
211
232
  break;
212
233
  case "offset":
213
234
  result[key] = offset(parseInt(kv?.shift()) || 10);
214
235
  break;
215
236
  case "hide":
216
- result[key] = hide();
237
+ result[key] = hide(detectOverflowOptions);
217
238
  break;
218
239
  default:
219
240
  throw new Error(`Unknown function: ${fn}`);
@@ -223,6 +244,135 @@ function buildFloatingMiddleware(middleware, placement) {
223
244
  return result.filter(Boolean);
224
245
  }
225
246
 
247
+ function createAdaptiveSizeMiddleware(detectOverflowOptions, popperElement) {
248
+ return size(
249
+ Object.assign({}, detectOverflowOptions, {
250
+ apply({ availableWidth, availableHeight, elements }) {
251
+ const floatingElement = elements?.floating || popperElement;
252
+ if (!(floatingElement instanceof HTMLElement)) {
253
+ return;
254
+ }
255
+
256
+ const maxWidth = clampAvailableDimension(
257
+ availableWidth,
258
+ readMaxDimension(floatingElement, "maxWidth"),
259
+ );
260
+ const maxHeight = clampAvailableDimension(
261
+ availableHeight,
262
+ readMaxDimension(floatingElement, "maxHeight"),
263
+ );
264
+ const nextStyle = {
265
+ boxSizing: "border-box",
266
+ };
267
+
268
+ if (Number.isFinite(maxWidth) && maxWidth > 0) {
269
+ nextStyle.maxWidth = `${maxWidth}px`;
270
+ }
271
+
272
+ if (Number.isFinite(maxHeight) && maxHeight > 0) {
273
+ nextStyle.maxHeight = `${maxHeight}px`;
274
+ }
275
+
276
+ Object.assign(floatingElement.style, nextStyle);
277
+ },
278
+ }),
279
+ );
280
+ }
281
+
282
+ function buildDetectOverflowOptions(boundaryElement) {
283
+ const result = {
284
+ rootBoundary: "viewport",
285
+ padding: 0,
286
+ };
287
+
288
+ if (boundaryElement instanceof HTMLElement) {
289
+ result.boundary = boundaryElement;
290
+ }
291
+
292
+ return result;
293
+ }
294
+
295
+ function resolveClippingBoundaryElement(...elements) {
296
+ for (const element of elements) {
297
+ const clippingBoundary = findNearestClippingContainer(element);
298
+ if (clippingBoundary instanceof HTMLElement) {
299
+ return clippingBoundary;
300
+ }
301
+ }
302
+
303
+ return null;
304
+ }
305
+
306
+ function findNearestClippingContainer(element) {
307
+ let current = getComposedParent(element);
308
+
309
+ while (current) {
310
+ if (
311
+ current instanceof HTMLElement &&
312
+ isClippingContainer(getComputedStyle(current))
313
+ ) {
314
+ return current;
315
+ }
316
+
317
+ current = getComposedParent(current);
318
+ }
319
+
320
+ return null;
321
+ }
322
+
323
+ function getComposedParent(node) {
324
+ if (!node) {
325
+ return null;
326
+ }
327
+
328
+ if (node instanceof ShadowRoot) {
329
+ return node.host || null;
330
+ }
331
+
332
+ if (node.parentElement instanceof HTMLElement) {
333
+ return node.parentElement;
334
+ }
335
+
336
+ const rootNode = node.getRootNode?.();
337
+ if (rootNode instanceof ShadowRoot) {
338
+ return rootNode.host || null;
339
+ }
340
+
341
+ return node.parentNode || null;
342
+ }
343
+
344
+ function isClippingContainer(style) {
345
+ if (!style) {
346
+ return false;
347
+ }
348
+
349
+ return [style.overflow, style.overflowX, style.overflowY].some((value) => {
350
+ return ["hidden", "auto", "scroll", "clip"].includes(value);
351
+ });
352
+ }
353
+
354
+ function readMaxDimension(element, property) {
355
+ const rawValue = getComputedStyle(element)?.[property];
356
+ if (!rawValue || rawValue === "none") {
357
+ return Infinity;
358
+ }
359
+
360
+ const value = Number.parseFloat(rawValue);
361
+ return Number.isFinite(value) ? value : Infinity;
362
+ }
363
+
364
+ function clampAvailableDimension(available, configuredMax) {
365
+ if (!Number.isFinite(available) || available <= 0) {
366
+ return null;
367
+ }
368
+
369
+ if (!Number.isFinite(configuredMax)) {
370
+ return available;
371
+ }
372
+
373
+ return Math.min(available, configuredMax);
374
+ }
375
+
226
376
  function startAutoUpdate(controlElement, popperElement, callback) {
227
377
  stopAutoUpdate(popperElement);
228
378
  autoUpdateCleanupMap.set(
@@ -15,10 +15,11 @@ let html1, options, html2, ConfirmButton;
15
15
 
16
16
  describe('ConfirmButton', function () {
17
17
 
18
- before(function (done) {
18
+ before(async function () {
19
+
20
+ await initJSDOM();
21
+ await import("element-internals-polyfill");
19
22
 
20
- import("element-internals-polyfill").catch(e => done(e));
21
-
22
23
  if(!global.ResizeObserver) {
23
24
  global.ResizeObserver = ResizeObserverMock;
24
25
  }
@@ -43,16 +44,8 @@ describe('ConfirmButton', function () {
43
44
  </div>
44
45
  `
45
46
 
46
-
47
- initJSDOM().then(() => {
48
-
49
- import("../../../../source/components/form/confirm-button.mjs").then((m) => {
50
- ConfirmButton = m['ConfirmButton'];
51
- done()
52
- }).catch(e => done(e))
53
-
54
-
55
- });
47
+ const m = await import("../../../../source/components/form/confirm-button.mjs");
48
+ ConfirmButton = m['ConfirmButton'];
56
49
  })
57
50
 
58
51
  describe('new ConfirmButton', function () {
@@ -120,8 +113,31 @@ describe('ConfirmButton', function () {
120
113
  }, 0)
121
114
 
122
115
 
116
+ });
117
+
118
+ it('should use fixed positioning by default', function (done) {
119
+
120
+ let mocks = document.getElementById('mocks');
121
+ const button = document.createElement('monster-confirm-button');
122
+ mocks.appendChild(button);
123
+
124
+ setTimeout(() => {
125
+ try {
126
+ button.showDialog();
127
+
128
+ const popper = button.shadowRoot.querySelector('[data-monster-role="popper"]');
129
+ expect(popper).to.exist;
130
+ expect(popper.style.position).to.equal('fixed');
131
+ } catch (e) {
132
+ return done(e);
133
+ }
134
+
135
+ done();
136
+ }, 0)
137
+
138
+
123
139
  });
124
140
  });
125
141
 
126
142
 
127
- });
143
+ });
@@ -0,0 +1,71 @@
1
+ import { getGlobal } from "../../../../source/types/global.mjs";
2
+ import * as chai from "chai";
3
+ import { chaiDom } from "../../../util/chai-dom.mjs";
4
+ import { initJSDOM } from "../../../util/jsdom.mjs";
5
+ import { ResizeObserverMock } from "../../../util/resize-observer.mjs";
6
+
7
+ let expect = chai.expect;
8
+ chai.use(chaiDom);
9
+
10
+ const global = getGlobal();
11
+
12
+ let ContextHelp;
13
+ let resolveClippingBoundaryElement;
14
+
15
+ describe("ContextHelp", function () {
16
+ before(function (done) {
17
+ initJSDOM().then(() => {
18
+ import("element-internals-polyfill").catch((e) => done(e));
19
+
20
+ if (!global.ResizeObserver) {
21
+ global.ResizeObserver = ResizeObserverMock;
22
+ }
23
+
24
+ Promise.all([
25
+ import("../../../../source/components/form/context-help.mjs"),
26
+ import("../../../../source/components/form/util/floating-ui.mjs"),
27
+ ])
28
+ .then(([contextHelpModule, floatingUiModule]) => {
29
+ ContextHelp = contextHelpModule.ContextHelp;
30
+ resolveClippingBoundaryElement =
31
+ floatingUiModule.resolveClippingBoundaryElement;
32
+ done();
33
+ })
34
+ .catch((e) => done(e));
35
+ });
36
+ });
37
+
38
+ afterEach(() => {
39
+ let mocks = document.getElementById("mocks");
40
+ mocks.innerHTML = "";
41
+ });
42
+
43
+ it("should resolve the nearest clipping parent across the shadow boundary", function (done) {
44
+ let mocks = document.getElementById("mocks");
45
+ const outer = document.createElement("div");
46
+ const wrapper = document.createElement("div");
47
+ const help = document.createElement("monster-context-help");
48
+
49
+ outer.style.overflow = "visible";
50
+ wrapper.style.overflow = "hidden";
51
+ wrapper.appendChild(help);
52
+ outer.appendChild(wrapper);
53
+ mocks.appendChild(outer);
54
+ help.innerHTML = "<p>Inline help</p>";
55
+
56
+ setTimeout(() => {
57
+ try {
58
+ expect(help).to.be.instanceof(ContextHelp);
59
+
60
+ const popper = help.shadowRoot.querySelector(
61
+ '[data-monster-role="popper"]',
62
+ );
63
+ expect(popper).to.exist;
64
+ expect(resolveClippingBoundaryElement(help, popper)).to.equal(wrapper);
65
+ done();
66
+ } catch (e) {
67
+ done(e);
68
+ }
69
+ }, 0);
70
+ });
71
+ });
@@ -86,4 +86,25 @@ describe("PopperButton", function () {
86
86
  }
87
87
  }, 0);
88
88
  });
89
+
90
+ it("should use absolute positioning by default", function (done) {
91
+ let mocks = document.getElementById("mocks");
92
+ const button = document.createElement("monster-popper-button");
93
+ mocks.appendChild(button);
94
+
95
+ setTimeout(() => {
96
+ try {
97
+ button.showDialog();
98
+
99
+ const popper = button.shadowRoot.querySelector(
100
+ '[data-monster-role="popper"]',
101
+ );
102
+ expect(popper).to.exist;
103
+ expect(popper.style.position).to.equal("absolute");
104
+ done();
105
+ } catch (e) {
106
+ done(e);
107
+ }
108
+ }, 0);
109
+ });
89
110
  });
@@ -254,87 +254,41 @@ describe('Select', function () {
254
254
 
255
255
  const pagination = () => select.shadowRoot.querySelector('[data-monster-role=pagination]');
256
256
 
257
- const triggerFilter = (value) => {
258
- const filterInput = select.shadowRoot.querySelector('[data-monster-role=filter]');
259
- filterInput.value = value;
260
- filterInput.dispatchEvent(new InputEvent('input', {
261
- bubbles: true,
262
- composed: true,
263
- data: value
264
- }));
265
- filterInput.dispatchEvent(new KeyboardEvent('keydown', {
266
- code: 'KeyA',
267
- key: 'a',
268
- bubbles: true,
269
- composed: true
270
- }));
271
- };
272
-
273
- const startedAt = Date.now();
274
- const pollLoadedState = () => {
275
- try {
276
- const options = select.getOption('options');
277
- const pager = pagination();
278
-
279
- if (
280
- options?.length !== 1 ||
281
- !pager ||
282
- pager.getOption('currentPage') !== 1 ||
283
- pager.getOption('pages') !== 2 ||
284
- pager.getOption('objectsPerPage') !== 1
285
- ) {
286
- if (Date.now() - startedAt < 3000) {
287
- return setTimeout(pollLoadedState, 50);
288
- }
289
- }
290
-
291
- expect(options.length).to.equal(1);
292
- expect(pager.getOption('currentPage')).to.equal(1);
293
- expect(pager.getOption('pages')).to.equal(2);
294
- expect(pager.getOption('objectsPerPage')).to.equal(1);
295
-
296
- triggerFilter('b');
297
- setTimeout(pollErrorState, 50);
298
- } catch (e) {
299
- done(e);
300
- }
301
- };
302
-
303
- const pollErrorState = () => {
304
- try {
305
- const optionsAfterError = select.getOption('options');
306
- const pager = pagination();
307
-
308
- if (
309
- optionsAfterError?.length !== 0 ||
310
- !pager ||
311
- pager.getOption('currentPage') !== null ||
312
- pager.getOption('pages') !== null ||
313
- pager.getOption('objectsPerPage') !== null ||
314
- select.getOption('total') !== null ||
315
- select.getOption('messages.total') !== ""
316
- ) {
317
- if (Date.now() - startedAt < 4500) {
318
- return setTimeout(pollErrorState, 50);
319
- }
320
- }
321
-
322
- expect(optionsAfterError.length).to.equal(0);
323
- expect(pager.getOption('currentPage')).to.equal(null);
324
- expect(pager.getOption('pages')).to.equal(null);
325
- expect(pager.getOption('objectsPerPage')).to.equal(null);
326
- expect(select.getOption('total')).to.equal(null);
327
- expect(select.getOption('messages.total')).to.equal("");
328
- } catch (e) {
329
- return done(e);
330
- }
331
-
332
- done();
257
+ const fetchRemotePage = (value) => {
258
+ return select.fetch(`https://example.com/items?filter=${encodeURIComponent(value)}&page=1`);
333
259
  };
334
260
 
335
261
  setTimeout(() => {
336
- triggerFilter('a');
337
- setTimeout(pollLoadedState, 50);
262
+ fetchRemotePage('a')
263
+ .then(() => {
264
+ const options = select.getOption('options');
265
+ const pager = pagination();
266
+
267
+ expect(options.length).to.equal(1);
268
+ expect(pager.getOption('currentPage')).to.equal(1);
269
+ expect(pager.getOption('pages')).to.equal(2);
270
+ expect(pager.getOption('objectsPerPage')).to.equal(1);
271
+
272
+ return fetchRemotePage('b')
273
+ .then(() => Promise.reject(new Error('Expected remote fetch to fail')))
274
+ .catch((e) => {
275
+ if (e.message === 'Expected remote fetch to fail') {
276
+ throw e;
277
+ }
278
+
279
+ const optionsAfterError = select.getOption('options');
280
+ const pager = pagination();
281
+
282
+ expect(optionsAfterError.length).to.equal(0);
283
+ expect(pager.getOption('currentPage')).to.equal(null);
284
+ expect(pager.getOption('pages')).to.equal(null);
285
+ expect(pager.getOption('objectsPerPage')).to.equal(null);
286
+ expect(select.getOption('total')).to.equal(null);
287
+ expect(select.getOption('messages.total')).to.equal("");
288
+ });
289
+ })
290
+ .then(() => done())
291
+ .catch((e) => done(e));
338
292
  }, 50);
339
293
  });
340
294
  });
@@ -289,18 +289,19 @@ describe("DOM", function () {
289
289
  firedDisconnected: 0,
290
290
  },
291
291
  };
292
+ static pendingCallbacks = [];
292
293
 
293
294
  connectedCallback() {
294
295
  const mode = this.getAttribute("data-mode") || "replace";
295
296
  MonsterTestRaceItem.stats[mode].connected++;
296
297
 
297
- setTimeout(() => {
298
+ MonsterTestRaceItem.pendingCallbacks.push(() => {
298
299
  if (this.isConnected) {
299
300
  MonsterTestRaceItem.stats[mode].firedConnected++;
300
301
  } else {
301
302
  MonsterTestRaceItem.stats[mode].firedDisconnected++;
302
303
  }
303
- }, 10);
304
+ });
304
305
  }
305
306
 
306
307
  disconnectedCallback() {
@@ -308,6 +309,14 @@ describe("DOM", function () {
308
309
  MonsterTestRaceItem.stats[mode].disconnected++;
309
310
  }
310
311
 
312
+ static flushPendingCallbacks() {
313
+ const pending = [...MonsterTestRaceItem.pendingCallbacks];
314
+ MonsterTestRaceItem.pendingCallbacks = [];
315
+ for (const callback of pending) {
316
+ callback();
317
+ }
318
+ }
319
+
311
320
  static resetStats() {
312
321
  MonsterTestRaceItem.stats = {
313
322
  replace: {
@@ -323,6 +332,7 @@ describe("DOM", function () {
323
332
  firedDisconnected: 0,
324
333
  },
325
334
  };
335
+ MonsterTestRaceItem.pendingCallbacks = [];
326
336
  }
327
337
  }
328
338
 
@@ -671,8 +681,9 @@ describe("DOM", function () {
671
681
 
672
682
  setTimeout(() => {
673
683
  try {
674
- const raceStats =
675
- customElements.get("monster-test-race-item").stats;
684
+ const raceItem = customElements.get("monster-test-race-item");
685
+ raceItem.flushPendingCallbacks();
686
+ const raceStats = raceItem.stats;
676
687
  expect(raceStats.patch.firedDisconnected).to.equal(0);
677
688
  expect(raceStats.replace.firedDisconnected).to.be.greaterThan(
678
689
  0,