@schukai/monster 4.140.2 → 4.140.3

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.140.2"}
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.140.3"}
@@ -36,11 +36,13 @@ import { getDocument } from "../../dom/util.mjs";
36
36
  import { getGlobal } from "../../types/global.mjs";
37
37
  import { ID } from "../../types/id.mjs";
38
38
  import { Observer } from "../../types/observer.mjs";
39
+ import { Queue } from "../../types/queue.mjs";
39
40
  import { STYLE_DISPLAY_MODE_BLOCK } from "./constants.mjs";
40
41
  import { ControlBarStyleSheet } from "./stylesheet/control-bar.mjs";
41
42
  import { positionPopper } from "./util/floating-ui.mjs";
42
43
  import { convertToPixels } from "../../dom/dimension.mjs";
43
44
  import { addErrorAttribute } from "../../dom/error.mjs";
45
+ import { DeadMansSwitch } from "../../util/deadmansswitch.mjs";
44
46
  import { Processing } from "../../util/processing.mjs";
45
47
  export { ControlBar };
46
48
 
@@ -132,6 +134,11 @@ const layoutTokenSymbol = Symbol("layoutToken");
132
134
  const observedLayoutNodesSignatureSymbol = Symbol(
133
135
  "observedLayoutNodesSignature",
134
136
  );
137
+ const layoutChangedEventQueueSymbol = Symbol("layoutChangedEventQueue");
138
+ const layoutChangedEventSwitchSymbol = Symbol("layoutChangedEventSwitch");
139
+ const layoutChangedEventLastSignatureSymbol = Symbol(
140
+ "layoutChangedEventLastSignature",
141
+ );
135
142
 
136
143
  /**
137
144
  * @private
@@ -152,6 +159,38 @@ const ATTRIBUTE_POPPER_POSITION = "data-monster-popper-position";
152
159
  */
153
160
  const ATTRIBUTE_LAYOUT_ALIGNMENT = "data-monster-layout-alignment";
154
161
 
162
+ /**
163
+ * @private
164
+ * @type {string}
165
+ */
166
+ const ATTRIBUTE_LAYOUT_STACKED = "data-monster-layout-stacked";
167
+
168
+ /**
169
+ * @private
170
+ * @type {string}
171
+ */
172
+ const ATTRIBUTE_OPTION_LAYOUT_ALIGNMENT =
173
+ "data-monster-option-layout-alignment";
174
+
175
+ /**
176
+ * @private
177
+ * @type {string}
178
+ */
179
+ const ATTRIBUTE_OPTION_LAYOUT_STACKED_ALIGNMENT =
180
+ "data-monster-option-layout-stacked-alignment";
181
+
182
+ /**
183
+ * @private
184
+ * @type {string}
185
+ */
186
+ const EVENT_LAYOUT_CHANGED = "monster-control-bar-layout-changed";
187
+
188
+ /**
189
+ * @private
190
+ * @type {number}
191
+ */
192
+ const LAYOUT_CHANGED_EVENT_DELAY = 20;
193
+
155
194
  /**
156
195
  * A control bar control.
157
196
  *
@@ -193,6 +232,7 @@ class ControlBar extends CustomElement {
193
232
  labels: {},
194
233
  layout: {
195
234
  alignment: "left",
235
+ stackedAlignment: undefined,
196
236
  },
197
237
  popper: {
198
238
  placement: "left",
@@ -228,7 +268,9 @@ class ControlBar extends CustomElement {
228
268
  // setup structure
229
269
  initControlBar.call(this);
230
270
  initPopperSwitch.call(this);
271
+ setLayoutStackedState.call(this, false);
231
272
  applyLayoutAlignment.call(this);
273
+ this[layoutChangedEventQueueSymbol] = new Queue();
232
274
  this.attachObserver(
233
275
  new Observer(() => {
234
276
  applyLayoutAlignment.call(this);
@@ -236,6 +278,21 @@ class ControlBar extends CustomElement {
236
278
  );
237
279
  }
238
280
 
281
+ /**
282
+ * Set option and sync layout state for reactive layout options.
283
+ *
284
+ * @param {string} path
285
+ * @param {*} value
286
+ * @return {ControlBar}
287
+ */
288
+ setOption(path, value) {
289
+ super.setOption(path, value);
290
+ if (path === "layout.alignment" || path === "layout.stackedAlignment") {
291
+ syncLayoutState.call(this);
292
+ }
293
+ return this;
294
+ }
295
+
239
296
  /**
240
297
  * This method is called internal and should not be called directly.
241
298
  *
@@ -280,6 +337,8 @@ class ControlBar extends CustomElement {
280
337
  static get observedAttributes() {
281
338
  const attributes = super.observedAttributes;
282
339
  attributes.push(ATTRIBUTE_POPPER_POSITION);
340
+ attributes.push(ATTRIBUTE_OPTION_LAYOUT_ALIGNMENT);
341
+ attributes.push(ATTRIBUTE_OPTION_LAYOUT_STACKED_ALIGNMENT);
283
342
  return attributes;
284
343
  }
285
344
 
@@ -305,6 +364,7 @@ class ControlBar extends CustomElement {
305
364
  this[layoutTokenSymbol] = (this[layoutTokenSymbol] || 0) + 1;
306
365
 
307
366
  disconnectResizeObserver.call(this);
367
+ defuseLayoutChangedEvent.call(this);
308
368
  if (this[mutationObserverSymbol]) {
309
369
  this[mutationObserverSymbol].disconnect();
310
370
  }
@@ -457,11 +517,20 @@ function initEventHandler() {
457
517
  });
458
518
  }
459
519
 
460
- self[attributeObserverSymbol][ATTRIBUTE_POPPER_POSITION] = function (value) {
520
+ self[attributeObserverSymbol][ATTRIBUTE_POPPER_POSITION] = (value) => {
461
521
  self.setOption("popper.placement", value);
462
522
  updatePopper.call(self);
463
523
  };
464
524
 
525
+ self[attributeObserverSymbol][ATTRIBUTE_OPTION_LAYOUT_ALIGNMENT] = () => {
526
+ syncLayoutState.call(self);
527
+ };
528
+
529
+ self[attributeObserverSymbol][ATTRIBUTE_OPTION_LAYOUT_STACKED_ALIGNMENT] =
530
+ () => {
531
+ syncLayoutState.call(self);
532
+ };
533
+
465
534
  self[resizeObserverSymbol] = new ResizeObserver(() => {
466
535
  scheduleLayout.call(self, { measure: true, layout: true });
467
536
  });
@@ -628,7 +697,7 @@ function rearrangeItems() {
628
697
  let itemWidth = 0;
629
698
  try {
630
699
  itemWidth = this[dimensionsSymbol].getVia(`data.item.${ref}`);
631
- } catch (e) {
700
+ } catch {
632
701
  // If the path does not exist, pathfinder throws an error.
633
702
  // In this case, we assume the width is 0.
634
703
  // This can happen for items that have never been visible.
@@ -676,6 +745,7 @@ function rearrangeItems() {
676
745
  layout.itemsToMoveToPopper.length > 0 && hasItems;
677
746
 
678
747
  suppressLayoutFeedback.call(this);
748
+ setLayoutStackedState.call(this, shouldShowSwitch);
679
749
 
680
750
  for (const item of layout.itemsToMoveToPopper) {
681
751
  if (item.getAttribute("slot") !== "popper") {
@@ -692,6 +762,7 @@ function rearrangeItems() {
692
762
  setSwitchVisible.call(this, shouldShowSwitch);
693
763
  updateControlSizing.call(this, layout, shouldShowSwitch);
694
764
  updateJoinedBorders.call(this, layout, shouldShowSwitch);
765
+ applyLayoutAlignment.call(this);
695
766
  if (!shouldShowSwitch) {
696
767
  hide.call(this);
697
768
  }
@@ -1398,17 +1469,172 @@ function applyLayoutAlignment() {
1398
1469
  return;
1399
1470
  }
1400
1471
 
1401
- const alignment = this.getOption("layout.alignment", "left");
1472
+ const layoutState = this[layoutStateSymbol] || {};
1473
+ const stackedAlignment = this.getOption("layout.stackedAlignment");
1474
+ let alignment = this.getOption("layout.alignment", "left");
1475
+ if (
1476
+ layoutState.stacked === true &&
1477
+ typeof stackedAlignment === "string" &&
1478
+ stackedAlignment !== ""
1479
+ ) {
1480
+ alignment = stackedAlignment;
1481
+ }
1402
1482
 
1403
1483
  if (alignment === "right") {
1404
- this[controlBarElementSymbol].setAttribute(
1484
+ if (setAttributeIfChanged(
1485
+ this[controlBarElementSymbol],
1405
1486
  ATTRIBUTE_LAYOUT_ALIGNMENT,
1406
1487
  "right",
1407
- );
1488
+ )) {
1489
+ queueLayoutChangedEvent.call(this);
1490
+ }
1408
1491
  return;
1409
1492
  }
1410
1493
 
1411
- this[controlBarElementSymbol].setAttribute(ATTRIBUTE_LAYOUT_ALIGNMENT, "left");
1494
+ if (setAttributeIfChanged(
1495
+ this[controlBarElementSymbol],
1496
+ ATTRIBUTE_LAYOUT_ALIGNMENT,
1497
+ "left",
1498
+ )) {
1499
+ queueLayoutChangedEvent.call(this);
1500
+ }
1501
+ }
1502
+
1503
+ /**
1504
+ * @private
1505
+ * @return {void}
1506
+ */
1507
+ function syncLayoutState() {
1508
+ applyLayoutAlignment.call(this);
1509
+ scheduleLayout.call(this, { measure: true, layout: true });
1510
+ }
1511
+
1512
+ /**
1513
+ * @private
1514
+ * @param {boolean} stacked
1515
+ * @return {void}
1516
+ */
1517
+ function setLayoutStackedState(stacked) {
1518
+ if (!this[layoutStateSymbol]) {
1519
+ return;
1520
+ }
1521
+ this[layoutStateSymbol].stacked = stacked === true;
1522
+
1523
+ if (!(this[controlBarElementSymbol] instanceof HTMLElement)) {
1524
+ return;
1525
+ }
1526
+
1527
+ if (setAttributeIfChanged(
1528
+ this[controlBarElementSymbol],
1529
+ ATTRIBUTE_LAYOUT_STACKED,
1530
+ this[layoutStateSymbol].stacked ? "true" : "false",
1531
+ )) {
1532
+ queueLayoutChangedEvent.call(this);
1533
+ }
1534
+ }
1535
+
1536
+ /**
1537
+ * @private
1538
+ * @return {void}
1539
+ */
1540
+ function queueLayoutChangedEvent() {
1541
+ if (!(this[layoutChangedEventQueueSymbol] instanceof Queue)) {
1542
+ return;
1543
+ }
1544
+
1545
+ this[layoutChangedEventQueueSymbol].add(getLayoutChangedEventDetail.call(this));
1546
+
1547
+ if (this[layoutChangedEventSwitchSymbol] instanceof DeadMansSwitch) {
1548
+ this[layoutChangedEventSwitchSymbol].touch();
1549
+ return;
1550
+ }
1551
+
1552
+ this[layoutChangedEventSwitchSymbol] = new DeadMansSwitch(
1553
+ LAYOUT_CHANGED_EVENT_DELAY,
1554
+ () => {
1555
+ this[layoutChangedEventSwitchSymbol] = undefined;
1556
+ dispatchQueuedLayoutChangedEvent.call(this);
1557
+ },
1558
+ );
1559
+ }
1560
+
1561
+ /**
1562
+ * @private
1563
+ * @return {void}
1564
+ */
1565
+ function dispatchQueuedLayoutChangedEvent() {
1566
+ if (!(this[layoutChangedEventQueueSymbol] instanceof Queue)) {
1567
+ return;
1568
+ }
1569
+
1570
+ let detail;
1571
+ while (!this[layoutChangedEventQueueSymbol].isEmpty()) {
1572
+ detail = this[layoutChangedEventQueueSymbol].poll();
1573
+ }
1574
+
1575
+ if (!detail) {
1576
+ return;
1577
+ }
1578
+
1579
+ const signature = JSON.stringify(detail);
1580
+ if (this[layoutChangedEventLastSignatureSymbol] === signature) {
1581
+ return;
1582
+ }
1583
+ this[layoutChangedEventLastSignatureSymbol] = signature;
1584
+
1585
+ this.dispatchEvent(
1586
+ new CustomEvent(EVENT_LAYOUT_CHANGED, {
1587
+ bubbles: true,
1588
+ composed: true,
1589
+ detail,
1590
+ }),
1591
+ );
1592
+ }
1593
+
1594
+ /**
1595
+ * @private
1596
+ * @return {void}
1597
+ */
1598
+ function defuseLayoutChangedEvent() {
1599
+ if (this[layoutChangedEventSwitchSymbol] instanceof DeadMansSwitch) {
1600
+ this[layoutChangedEventSwitchSymbol].defuse();
1601
+ this[layoutChangedEventSwitchSymbol] = undefined;
1602
+ }
1603
+ }
1604
+
1605
+ /**
1606
+ * @private
1607
+ * @return {Object}
1608
+ */
1609
+ function getLayoutChangedEventDetail() {
1610
+ const layoutState = this[layoutStateSymbol] || {};
1611
+ const stacked = layoutState.stacked === true;
1612
+ const alignment = this[controlBarElementSymbol]?.getAttribute(
1613
+ ATTRIBUTE_LAYOUT_ALIGNMENT,
1614
+ );
1615
+
1616
+ return {
1617
+ alignment: alignment || this.getOption("layout.alignment", "left"),
1618
+ configuredAlignment: this.getOption("layout.alignment", "left"),
1619
+ stacked,
1620
+ stackedAlignment: this.getOption("layout.stackedAlignment"),
1621
+ };
1622
+ }
1623
+
1624
+ /**
1625
+ * @private
1626
+ * @param {HTMLElement} element
1627
+ * @param {string} attribute
1628
+ * @param {string} value
1629
+ * @return {boolean}
1630
+ */
1631
+ function setAttributeIfChanged(element, attribute, value) {
1632
+ if (element.getAttribute(attribute) !== value) {
1633
+ element.setAttribute(attribute, value);
1634
+ return true;
1635
+ }
1636
+
1637
+ return false;
1412
1638
  }
1413
1639
 
1414
1640
  /**
@@ -3,7 +3,7 @@ import { chaiDom } from "../../../util/chai-dom.mjs";
3
3
  import { initJSDOM } from "../../../util/jsdom.mjs";
4
4
  import { ResizeObserverMock } from "../../../util/resize-observer.mjs";
5
5
 
6
- let expect = chai.expect;
6
+ const expect = chai.expect;
7
7
  chai.use(chaiDom);
8
8
 
9
9
  const html = `
@@ -103,6 +103,67 @@ describe("ControlBar", function () {
103
103
  }, 50);
104
104
  });
105
105
 
106
+ it("should update the rendered layout alignment when the option changes", async function () {
107
+ const bar = document.getElementById("bar-right");
108
+
109
+ await new Promise((resolve) => setTimeout(resolve, 0));
110
+
111
+ const controlBar = bar.shadowRoot.querySelector(
112
+ '[data-monster-role="control-bar"]',
113
+ );
114
+
115
+ expect(controlBar.getAttribute("data-monster-layout-alignment")).to.equal(
116
+ "right",
117
+ );
118
+
119
+ bar.setOption("layout.alignment", "left");
120
+ await new Promise((resolve) => setTimeout(resolve, 0));
121
+
122
+ expect(controlBar.getAttribute("data-monster-layout-alignment")).to.equal(
123
+ "left",
124
+ );
125
+ });
126
+
127
+ it("should update the rendered layout alignment when the host option attribute changes", async function () {
128
+ const bar = document.getElementById("bar");
129
+ const controlBar = bar.shadowRoot.querySelector(
130
+ '[data-monster-role="control-bar"]',
131
+ );
132
+
133
+ bar.setAttribute("data-monster-option-layout-alignment", "right");
134
+ await new Promise((resolve) => setTimeout(resolve, 0));
135
+
136
+ expect(bar.getOption("layout.alignment")).to.equal("right");
137
+ expect(controlBar.getAttribute("data-monster-layout-alignment")).to.equal(
138
+ "right",
139
+ );
140
+ });
141
+
142
+ it("should dispatch one final layout changed event for rapid alignment changes", async function () {
143
+ const bar = document.getElementById("bar-right");
144
+ const events = [];
145
+
146
+ await new Promise((resolve) => setTimeout(resolve, 0));
147
+
148
+ bar.addEventListener("monster-control-bar-layout-changed", (event) => {
149
+ events.push(event.detail);
150
+ });
151
+
152
+ bar.setOption("layout.alignment", "left");
153
+ bar.setOption("layout.alignment", "right");
154
+ bar.setOption("layout.alignment", "left");
155
+
156
+ await new Promise((resolve) => setTimeout(resolve, 60));
157
+
158
+ expect(events).to.have.length(1);
159
+ expect(events[0]).to.deep.equal({
160
+ alignment: "left",
161
+ configuredAlignment: "left",
162
+ stacked: false,
163
+ stackedAlignment: undefined,
164
+ });
165
+ });
166
+
106
167
  it("should map popper position attributes to the popper placement option", function () {
107
168
  const bar = document.getElementById("bar");
108
169
 
@@ -515,7 +576,11 @@ describe("ControlBar", function () {
515
576
  const mocks = document.getElementById("mocks");
516
577
  mocks.innerHTML = `
517
578
  <div id="control-bar-wrapper">
518
- <monster-control-bar id="overflow-bar">
579
+ <monster-control-bar
580
+ id="overflow-bar"
581
+ data-monster-option-layout-alignment="right"
582
+ data-monster-option-layout-stacked-alignment="left"
583
+ >
519
584
  <input id="overflow-input" type="search">
520
585
  <select id="overflow-select"><option>One</option></select>
521
586
  <button id="overflow-button">Run</button>
@@ -567,12 +632,21 @@ describe("ControlBar", function () {
567
632
  configurable: true,
568
633
  value: 20,
569
634
  });
635
+ const controlBar = bar.shadowRoot.querySelector(
636
+ '[data-monster-role="control-bar"]',
637
+ );
570
638
 
571
639
  await flushFrames();
572
640
  await new Promise((resolve) => setTimeout(resolve, 0));
573
641
  await new Promise((resolve) => setTimeout(resolve, 0));
574
642
 
575
643
  expect(switchButton.hasAttribute("hidden")).to.be.false;
644
+ expect(controlBar.getAttribute("data-monster-layout-stacked")).to.equal(
645
+ "true",
646
+ );
647
+ expect(controlBar.getAttribute("data-monster-layout-alignment")).to.equal(
648
+ "left",
649
+ );
576
650
  expect(document.getElementById("overflow-input").hasAttribute("slot")).to
577
651
  .be.false;
578
652
  expect(document.getElementById("overflow-select").getAttribute("slot")).to