@schukai/monster 4.136.23 → 4.136.24

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.23"}
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.24"}
@@ -41,6 +41,7 @@ import { ButtonBarStyleSheet } from "./stylesheet/button-bar.mjs";
41
41
  import { positionPopper } from "./util/floating-ui.mjs";
42
42
  import { convertToPixels } from "../../dom/dimension.mjs";
43
43
  import { addErrorAttribute } from "../../dom/error.mjs";
44
+ import { Processing } from "../../util/processing.mjs";
44
45
  export { ButtonBar };
45
46
 
46
47
  /**
@@ -126,6 +127,8 @@ const switchElementSymbol = Symbol("switchElement");
126
127
  * @type {symbol}
127
128
  */
128
129
  const layoutStateSymbol = Symbol("layoutState");
130
+ const layoutFrameSymbol = Symbol("layoutFrame");
131
+ const layoutTokenSymbol = Symbol("layoutToken");
129
132
 
130
133
  /**
131
134
  * @private
@@ -201,6 +204,7 @@ class ButtonBar extends CustomElement {
201
204
  this[dimensionsSymbol] = new Pathfinder({ data: {} });
202
205
  this[layoutStateSymbol] = {
203
206
  scheduled: false,
207
+ running: false,
204
208
  needsMeasure: true,
205
209
  needsLayout: true,
206
210
  needsObserve: true,
@@ -283,6 +287,12 @@ class ButtonBar extends CustomElement {
283
287
  document.removeEventListener(type, this[closeEventHandler]);
284
288
  }
285
289
 
290
+ if (typeof this[layoutFrameSymbol] === "number") {
291
+ cancelAnimationFrame(this[layoutFrameSymbol]);
292
+ }
293
+ delete this[layoutFrameSymbol];
294
+ this[layoutTokenSymbol] = (this[layoutTokenSymbol] || 0) + 1;
295
+
286
296
  disconnectResizeObserver.call(this);
287
297
  if (this[mutationObserverSymbol]) {
288
298
  this[mutationObserverSymbol].disconnect();
@@ -442,12 +452,27 @@ function scheduleLayout(options = {}) {
442
452
  state.needsLayout = state.needsLayout || options.layout === true;
443
453
  state.needsObserve = state.needsObserve || options.observe === true;
444
454
 
445
- if (state.scheduled) {
455
+ if (state.scheduled || state.running) {
456
+ return;
457
+ }
458
+
459
+ scheduleLayoutFrame.call(this);
460
+ }
461
+
462
+ function scheduleLayoutFrame() {
463
+ const state = this[layoutStateSymbol];
464
+ if (!state || state.scheduled || state.running) {
446
465
  return;
447
466
  }
448
467
 
449
468
  state.scheduled = true;
450
- requestAnimationFrame(() => {
469
+ const token = (this[layoutTokenSymbol] || 0) + 1;
470
+ this[layoutTokenSymbol] = token;
471
+ this[layoutFrameSymbol] = requestAnimationFrame(() => {
472
+ if (this[layoutTokenSymbol] !== token) {
473
+ return;
474
+ }
475
+ delete this[layoutFrameSymbol];
451
476
  runLayout.call(this);
452
477
  });
453
478
  }
@@ -463,36 +488,61 @@ function runLayout() {
463
488
  return;
464
489
  }
465
490
 
466
- if (state.needsObserve) {
467
- updateResizeObserverObservation.call(this);
468
- state.needsObserve = false;
491
+ if (state.running) {
492
+ return;
469
493
  }
470
494
 
471
- if (state.needsMeasure) {
472
- try {
473
- calculateButtonBarDimensions.call(this);
474
- } catch (error) {
475
- addErrorAttribute(
476
- this,
477
- error?.message || "An error occurred while calculating dimensions",
478
- );
495
+ const needsObserve = state.needsObserve;
496
+ const needsMeasure = state.needsMeasure;
497
+ const needsLayout = state.needsLayout;
498
+
499
+ state.needsObserve = false;
500
+ state.needsMeasure = false;
501
+ state.needsLayout = false;
502
+ state.running = true;
503
+
504
+ new Processing(() => {
505
+ if (needsObserve) {
506
+ updateResizeObserverObservation.call(this);
479
507
  }
480
- state.needsMeasure = false;
481
- }
482
508
 
483
- if (state.needsLayout) {
484
- try {
485
- rearrangeButtons.call(this);
486
- } catch (error) {
509
+ if (needsMeasure) {
510
+ try {
511
+ calculateButtonBarDimensions.call(this);
512
+ } catch (error) {
513
+ addErrorAttribute(
514
+ this,
515
+ error?.message || "An error occurred while calculating dimensions",
516
+ );
517
+ }
518
+ }
519
+
520
+ if (needsLayout) {
521
+ try {
522
+ rearrangeButtons.call(this);
523
+ } catch (error) {
524
+ addErrorAttribute(
525
+ this,
526
+ error?.message || "An error occurred while rearranging the buttons",
527
+ );
528
+ }
529
+ }
530
+
531
+ return updatePopper.call(this);
532
+ })
533
+ .run()
534
+ .catch((error) => {
487
535
  addErrorAttribute(
488
536
  this,
489
- error?.message || "An error occurred while rearranging the buttons",
537
+ error?.message || "An error occurred while running the button bar layout",
490
538
  );
491
- }
492
- state.needsLayout = false;
493
- }
494
-
495
- updatePopper.call(this);
539
+ })
540
+ .finally(() => {
541
+ state.running = false;
542
+ if (state.needsObserve || state.needsMeasure || state.needsLayout) {
543
+ scheduleLayoutFrame.call(this);
544
+ }
545
+ });
496
546
  }
497
547
 
498
548
  /**
@@ -25,6 +25,7 @@ import {
25
25
  CustomElement,
26
26
  registerCustomElement,
27
27
  } from "../../dom/customelement.mjs";
28
+ import { addErrorAttribute } from "../../dom/error.mjs";
28
29
  import { fireCustomEvent } from "../../dom/events.mjs";
29
30
  import {
30
31
  findElementWithSelectorUpwards,
@@ -115,6 +116,8 @@ const dismissRecordSymbol = Symbol("dismissRecord");
115
116
  * @type {symbol}
116
117
  */
117
118
  const usesHostDismissSymbol = Symbol("usesHostDismiss");
119
+ const actionQueueSymbol = Symbol("actionQueue");
120
+ const pendingActionSymbol = Symbol("pendingAction");
118
121
 
119
122
  /**
120
123
  * Popper component for displaying floating UI elements
@@ -274,7 +277,7 @@ class Popper extends CustomElement {
274
277
  * @return {Popper} The popper instance
275
278
  */
276
279
  showDialog() {
277
- show.call(this);
280
+ queuePopperAction.call(this, "show");
278
281
  return this;
279
282
  }
280
283
 
@@ -284,8 +287,7 @@ class Popper extends CustomElement {
284
287
  * @return {Popper}
285
288
  */
286
289
  recalcPopper() {
287
- applyContentOverflowMode.call(this);
288
- updatePopper.call(this);
290
+ queuePopperAction.call(this, "update");
289
291
  return this;
290
292
  }
291
293
 
@@ -326,7 +328,7 @@ class Popper extends CustomElement {
326
328
  * @return {Popper} The popper instance
327
329
  */
328
330
  hideDialog() {
329
- hide.call(this);
331
+ queuePopperAction.call(this, "hide");
330
332
  return this;
331
333
  }
332
334
 
@@ -335,11 +337,7 @@ class Popper extends CustomElement {
335
337
  * @return {Popper} The popper instance
336
338
  */
337
339
  toggleDialog() {
338
- if (isPositionedPopperOpen(this[popperElementSymbol])) {
339
- this.hideDialog();
340
- } else {
341
- this.showDialog();
342
- }
340
+ queuePopperAction.call(this, "toggle");
343
341
  return this;
344
342
  }
345
343
  }
@@ -396,6 +394,59 @@ function initEventHandler() {
396
394
  return this;
397
395
  }
398
396
 
397
+ /**
398
+ * Serializes popper state changes so open/close/update events do not interleave.
399
+ * The latest requested action wins while a queue is already running.
400
+ *
401
+ * @private
402
+ * @param {"show"|"hide"|"toggle"|"update"} action
403
+ * @return {Promise<void>}
404
+ */
405
+ function queuePopperAction(action) {
406
+ this[pendingActionSymbol] = action;
407
+
408
+ if (this[actionQueueSymbol] instanceof Promise) {
409
+ return this[actionQueueSymbol];
410
+ }
411
+
412
+ this[actionQueueSymbol] = (async () => {
413
+ while (this[pendingActionSymbol]) {
414
+ const nextAction = this[pendingActionSymbol];
415
+ delete this[pendingActionSymbol];
416
+ await Promise.resolve(runPopperAction.call(this, nextAction));
417
+ }
418
+ })()
419
+ .catch((e) => {
420
+ addErrorAttribute(this, e);
421
+ })
422
+ .finally(() => {
423
+ delete this[actionQueueSymbol];
424
+ if (this[pendingActionSymbol]) {
425
+ void queuePopperAction.call(this, this[pendingActionSymbol]);
426
+ }
427
+ });
428
+
429
+ return this[actionQueueSymbol];
430
+ }
431
+
432
+ function runPopperAction(action) {
433
+ switch (action) {
434
+ case "toggle":
435
+ if (isPositionedPopperOpen(this[popperElementSymbol])) {
436
+ return performHide.call(this);
437
+ }
438
+ return performShow.call(this);
439
+ case "hide":
440
+ return performHide.call(this);
441
+ case "show":
442
+ return performShow.call(this);
443
+ case "update":
444
+ return performUpdate.call(this);
445
+ default:
446
+ return undefined;
447
+ }
448
+ }
449
+
399
450
  function isEventInsidePopperOwner(
400
451
  owner,
401
452
  event,
@@ -545,6 +596,14 @@ function disconnectResizeObserver() {
545
596
  * @private
546
597
  */
547
598
  function hide() {
599
+ void queuePopperAction.call(this, "hide");
600
+ }
601
+
602
+ /**
603
+ * Hides the popper element
604
+ * @private
605
+ */
606
+ function performHide() {
548
607
  const self = this;
549
608
  const popperElement = self[popperElementSymbol];
550
609
  const controlElement = self[controlElementSymbol];
@@ -580,6 +639,14 @@ function hide() {
580
639
  * @private
581
640
  */
582
641
  function show() {
642
+ void queuePopperAction.call(this, "show");
643
+ }
644
+
645
+ /**
646
+ * Shows the popper element
647
+ * @private
648
+ */
649
+ function performShow() {
583
650
  const self = this;
584
651
  const popperElement = self[popperElementSymbol];
585
652
  const controlElement = self[controlElementSymbol];
@@ -615,13 +682,13 @@ function show() {
615
682
 
616
683
  addAttributeToken(controlElement, "class", "open");
617
684
  registerWithHost.call(self);
618
- updatePopper.call(self);
619
-
620
- setTimeout(() => {
621
- fireCustomEvent(self, "monster-popper-opened", {
622
- self,
623
- });
624
- }, 0);
685
+ return performUpdate.call(self).then(() => {
686
+ setTimeout(() => {
687
+ fireCustomEvent(self, "monster-popper-opened", {
688
+ self,
689
+ });
690
+ }, 0);
691
+ });
625
692
  }
626
693
 
627
694
  /**
@@ -629,6 +696,14 @@ function show() {
629
696
  * @private
630
697
  */
631
698
  function updatePopper() {
699
+ void queuePopperAction.call(this, "update");
700
+ }
701
+
702
+ /**
703
+ * Updates popper positioning
704
+ * @private
705
+ */
706
+ function performUpdate() {
632
707
  if (
633
708
  !this.isConnected ||
634
709
  !(this[controlElementSymbol] instanceof HTMLElement) ||
@@ -647,7 +722,7 @@ function updatePopper() {
647
722
 
648
723
  applyContentOverflowMode.call(this);
649
724
 
650
- positionPopper.call(
725
+ return positionPopper.call(
651
726
  this,
652
727
  this[controlElementSymbol],
653
728
  this[popperElementSymbol],
@@ -195,84 +195,97 @@ describe("PopperButton", function () {
195
195
  }, 0);
196
196
  });
197
197
 
198
- it("should move the popper through the opening appearance states", function (done) {
198
+ it("should move the popper through the opening appearance states", async function () {
199
199
  let mocks = document.getElementById("mocks");
200
200
  const button = document.createElement("monster-popper-button");
201
201
  mocks.appendChild(button);
202
202
 
203
- setTimeout(() => {
204
- try {
205
- button.showDialog();
203
+ await waitForTimeout();
206
204
 
207
- const popper = button.shadowRoot.querySelector(
208
- '[data-monster-role="popper"]',
209
- );
210
- expect(popper).to.exist;
211
- expect(popper.dataset.monsterAppearance).to.equal("opening");
205
+ button.showDialog();
212
206
 
213
- setTimeout(() => {
214
- try {
215
- expect(popper.dataset.monsterAppearance).to.equal("open");
216
-
217
- button.hideDialog();
218
-
219
- setTimeout(() => {
220
- try {
221
- expect(
222
- popper.hasAttribute("data-monster-appearance"),
223
- ).to.equal(false);
224
- done();
225
- } catch (e) {
226
- done(e);
227
- }
228
- }, 0);
229
- } catch (e) {
230
- done(e);
231
- }
232
- }, 30);
233
- } catch (e) {
234
- done(e);
235
- }
236
- }, 0);
207
+ const popper = button.shadowRoot.querySelector(
208
+ '[data-monster-role="popper"]',
209
+ );
210
+ expect(popper).to.exist;
211
+ expect(popper.dataset.monsterAppearance).to.equal("opening");
212
+
213
+ await waitForCondition(() => {
214
+ return popper.dataset.monsterAppearance === "open";
215
+ });
216
+ expect(popper.dataset.monsterAppearance).to.equal("open");
217
+
218
+ button.hideDialog();
219
+
220
+ await waitForCondition(() => {
221
+ return !popper.hasAttribute("data-monster-appearance");
222
+ });
223
+ expect(popper.hasAttribute("data-monster-appearance")).to.equal(false);
237
224
  });
238
225
 
239
- it("should finish the opening appearance state when requestAnimationFrame stalls", function (done) {
226
+ it("should finish the opening appearance state when requestAnimationFrame stalls", async function () {
240
227
  let mocks = document.getElementById("mocks");
241
228
  const button = document.createElement("monster-popper-button");
242
229
  const originalRequestAnimationFrame = globalThis.requestAnimationFrame;
243
230
  const originalCancelAnimationFrame = globalThis.cancelAnimationFrame;
244
231
 
245
- globalThis.requestAnimationFrame = () => 1;
246
- globalThis.cancelAnimationFrame = () => {};
247
- mocks.appendChild(button);
232
+ try {
233
+ globalThis.requestAnimationFrame = () => 1;
234
+ globalThis.cancelAnimationFrame = () => {};
235
+ mocks.appendChild(button);
236
+
237
+ await waitForTimeout();
238
+
239
+ button.showDialog();
240
+
241
+ const popper = button.shadowRoot.querySelector(
242
+ '[data-monster-role="popper"]',
243
+ );
244
+ expect(popper).to.exist;
245
+ expect(popper.dataset.monsterAppearance).to.equal("opening");
246
+
247
+ await waitForCondition(() => {
248
+ return popper.dataset.monsterAppearance === "open";
249
+ });
250
+ expect(popper.dataset.monsterAppearance).to.equal("open");
251
+ button.hideDialog();
252
+ } finally {
253
+ globalThis.requestAnimationFrame = originalRequestAnimationFrame;
254
+ globalThis.cancelAnimationFrame = originalCancelAnimationFrame;
255
+ }
256
+ });
257
+ });
248
258
 
249
- setTimeout(() => {
250
- try {
251
- button.showDialog();
259
+ function waitForTimeout(timeout = 0) {
260
+ return new Promise((resolve) => setTimeout(resolve, timeout));
261
+ }
252
262
 
253
- const popper = button.shadowRoot.querySelector(
254
- '[data-monster-role="popper"]',
255
- );
256
- expect(popper).to.exist;
257
- expect(popper.dataset.monsterAppearance).to.equal("opening");
263
+ function waitForCondition(check, { timeout = 1000, interval = 10 } = {}) {
264
+ const deadline = Date.now() + timeout;
258
265
 
259
- setTimeout(() => {
260
- try {
261
- expect(popper.dataset.monsterAppearance).to.equal("open");
262
- button.hideDialog();
263
- done();
264
- } catch (e) {
265
- done(e);
266
- } finally {
267
- globalThis.requestAnimationFrame = originalRequestAnimationFrame;
268
- globalThis.cancelAnimationFrame = originalCancelAnimationFrame;
269
- }
270
- }, 30);
266
+ return new Promise((resolve, reject) => {
267
+ const tick = () => {
268
+ let done = false;
269
+ try {
270
+ done = check();
271
271
  } catch (e) {
272
- globalThis.requestAnimationFrame = originalRequestAnimationFrame;
273
- globalThis.cancelAnimationFrame = originalCancelAnimationFrame;
274
- done(e);
272
+ reject(e);
273
+ return;
275
274
  }
276
- }, 0);
275
+
276
+ if (done) {
277
+ resolve();
278
+ return;
279
+ }
280
+
281
+ if (Date.now() >= deadline) {
282
+ reject(new Error("Timed out waiting for condition."));
283
+ return;
284
+ }
285
+
286
+ setTimeout(tick, interval);
287
+ };
288
+
289
+ tick();
277
290
  });
278
- });
291
+ }