@schukai/monster 4.129.7 → 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.7"}
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"}
@@ -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,7 +52,7 @@ 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";
@@ -115,7 +116,7 @@ function isPositionedPopperOpen(popperElement) {
115
116
  return popperElement.style.display === "block";
116
117
  }
117
118
 
118
- function normalizePopperConfig(options) {
119
+ function normalizePopperConfig(options, controlElement, popperElement) {
119
120
  const config = Object.assign(
120
121
  {},
121
122
  {
@@ -126,13 +127,28 @@ function normalizePopperConfig(options) {
126
127
  options,
127
128
  );
128
129
 
130
+ config.boundaryElement = resolveClippingBoundaryElement(
131
+ controlElement,
132
+ popperElement,
133
+ );
134
+ config.detectOverflowOptions = buildDetectOverflowOptions(
135
+ config.boundaryElement,
136
+ );
129
137
  config.middleware = normalizeMiddleware(config);
130
138
  config.middlewareTokens = config.middleware.filter((line) => isString(line));
131
139
  config.floatingMiddleware = buildFloatingMiddleware(
132
140
  config.middleware,
133
141
  config.placement,
142
+ config.detectOverflowOptions,
143
+ popperElement,
134
144
  );
135
145
 
146
+ if (!config.middlewareTokens.includes("size")) {
147
+ config.floatingMiddleware.push(
148
+ createAdaptiveSizeMiddleware(config.detectOverflowOptions, popperElement),
149
+ );
150
+ }
151
+
136
152
  return config;
137
153
  }
138
154
 
@@ -149,7 +165,12 @@ function normalizeMiddleware(config) {
149
165
  return [];
150
166
  }
151
167
 
152
- function buildFloatingMiddleware(middleware, placement) {
168
+ function buildFloatingMiddleware(
169
+ middleware,
170
+ placement,
171
+ detectOverflowOptions,
172
+ popperElement,
173
+ ) {
153
174
  const result = [...middleware];
154
175
 
155
176
  for (const key in result) {
@@ -169,10 +190,10 @@ function buildFloatingMiddleware(middleware, placement) {
169
190
 
170
191
  switch (fn) {
171
192
  case "flip":
172
- result[key] = flip();
193
+ result[key] = flip(detectOverflowOptions);
173
194
  break;
174
195
  case "shift":
175
- result[key] = shift();
196
+ result[key] = shift(detectOverflowOptions);
176
197
  break;
177
198
  case "autoPlacement":
178
199
  let defaultAllowedPlacements = ["top", "bottom", "left", "right"];
@@ -192,31 +213,28 @@ function buildFloatingMiddleware(middleware, placement) {
192
213
  }
193
214
  defaultAllowedPlacements.unshift(placement);
194
215
 
195
- result[key] = autoPlacement({
196
- crossAxis: true,
197
- autoAlignment: true,
198
- allowedPlacements: defaultAllowedPlacements,
199
- });
216
+ result[key] = autoPlacement(
217
+ Object.assign({}, detectOverflowOptions, {
218
+ crossAxis: true,
219
+ autoAlignment: true,
220
+ allowedPlacements: defaultAllowedPlacements,
221
+ }),
222
+ );
200
223
  break;
201
224
  case "arrow":
202
225
  result[key] = null;
203
226
  break;
204
227
  case "size":
205
- result[key] = size({
206
- apply({ availableWidth, availableHeight, elements }) {
207
- Object.assign(elements.floating.style, {
208
- boxSizing: "border-box",
209
- maxWidth: `${availableWidth}px`,
210
- maxHeight: `${availableHeight}px`,
211
- });
212
- },
213
- });
228
+ result[key] = createAdaptiveSizeMiddleware(
229
+ detectOverflowOptions,
230
+ popperElement,
231
+ );
214
232
  break;
215
233
  case "offset":
216
234
  result[key] = offset(parseInt(kv?.shift()) || 10);
217
235
  break;
218
236
  case "hide":
219
- result[key] = hide();
237
+ result[key] = hide(detectOverflowOptions);
220
238
  break;
221
239
  default:
222
240
  throw new Error(`Unknown function: ${fn}`);
@@ -226,6 +244,135 @@ function buildFloatingMiddleware(middleware, placement) {
226
244
  return result.filter(Boolean);
227
245
  }
228
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
+
229
376
  function startAutoUpdate(controlElement, popperElement, callback) {
230
377
  stopAutoUpdate(popperElement);
231
378
  autoUpdateCleanupMap.set(
@@ -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
+ });
@@ -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
  });