@schukai/monster 4.52.0 → 4.54.0

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/CHANGELOG.md CHANGED
@@ -2,6 +2,31 @@
2
2
 
3
3
 
4
4
 
5
+ ## [4.54.0] - 2025-12-28
6
+
7
+ ### Add Features
8
+
9
+ - Update project references and add issue handling for button bar
10
+ ### Bug Fixes
11
+
12
+ - revert to jsdom 26.1
13
+ ### Changes
14
+
15
+ - close issue [#347](https://gitlab.schukai.com/oss/libraries/javascript/monster/issues/347)
16
+
17
+
18
+
19
+ ## [4.53.0] - 2025-12-28
20
+
21
+ ### Add Features
22
+
23
+ - Improve button visibility handling and optimize element width calculations
24
+ ### Bug Fixes
25
+
26
+ - Preserve scroll position when updating hash for tabs
27
+
28
+
29
+
5
30
  ## [4.52.0] - 2025-12-28
6
31
 
7
32
  ### Add Features
package/package.json CHANGED
@@ -1 +1 @@
1
- {"author":"schukai GmbH","dependencies":{"@floating-ui/dom":"^1.7.4","@popperjs/core":"^2.11.8"},"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.52.0"}
1
+ {"author":"schukai GmbH","dependencies":{"@floating-ui/dom":"^1.7.4","@popperjs/core":"^2.11.8"},"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.54.0"}
@@ -112,6 +112,12 @@ const popperSwitchEventHandler = Symbol("popperSwitchEventHandler");
112
112
  */
113
113
  const popperNavElementSymbol = Symbol("popperNavElement");
114
114
 
115
+ /**
116
+ * @private
117
+ * @type {symbol}
118
+ */
119
+ const mutationObserverSymbol = Symbol("mutationObserver");
120
+
115
121
  /**
116
122
  * @private
117
123
  * @type {symbol}
@@ -257,6 +263,9 @@ class ButtonBar extends CustomElement {
257
263
  }
258
264
 
259
265
  disconnectResizeObserver.call(this);
266
+ if (this[mutationObserverSymbol]) {
267
+ this[mutationObserverSymbol].disconnect();
268
+ }
260
269
  }
261
270
 
262
271
  /**
@@ -300,72 +309,100 @@ function initDefaultsFromAttributes(obj) {
300
309
  return obj;
301
310
  }
302
311
 
312
+ /**
313
+ * @private
314
+ * @param {HTMLElement} element
315
+ * @return {boolean}
316
+ */
317
+ function isElementTrulyVisible(element) {
318
+ if (!(element instanceof HTMLElement)) {
319
+ return false;
320
+ }
321
+ const computedStyle = getComputedStyle(element);
322
+ return computedStyle.display !== 'none' && computedStyle.visibility !== 'hidden' && computedStyle.opacity !== '0' && element.offsetWidth > 0 && element.offsetHeight > 0;
323
+ }
324
+
303
325
  /**
304
326
  * @private
305
327
  */
306
328
  function initEventHandler() {
307
329
  const self = this;
330
+
331
+ const triggerRecalculation = () => {
332
+ if (self[timerCallbackSymbol] instanceof DeadMansSwitch) {
333
+ try {
334
+ self[timerCallbackSymbol].touch();
335
+ return;
336
+ } catch (e) {
337
+ if (e.message !== "has already run") throw e;
338
+ delete self[timerCallbackSymbol];
339
+ }
340
+ }
341
+
342
+ self[timerCallbackSymbol] = new DeadMansSwitch(50, () => {
343
+ requestAnimationFrame(() => {
344
+ updatePopper.call(self);
345
+ self[dimensionsSymbol].setVia("data.calculated", false);
346
+ try {
347
+ checkAndRearrangeButtons.call(self);
348
+ } catch (error) {
349
+ addErrorAttribute(
350
+ self,
351
+ error?.message || "An error occurred while rearranging the buttons",
352
+ );
353
+ }
354
+ });
355
+ });
356
+ };
357
+
358
+ const mutationCallback = (mutationList) => {
359
+ let needsRecalc = false;
360
+ for (const mutation of mutationList) {
361
+ if (mutation.type === "attributes") {
362
+ const target = mutation.target;
363
+ if (target instanceof HTMLElement) {
364
+ const ref = target.getAttribute("data-monster-reference");
365
+ if (ref && !isElementTrulyVisible(target)) {
366
+ self[dimensionsSymbol].setVia(`data.button.${ref}`, 0);
367
+ needsRecalc = true;
368
+ }
369
+ }
370
+ }
371
+ }
372
+ if (needsRecalc) {
373
+ triggerRecalculation();
374
+ }
375
+ };
376
+
308
377
  /**
309
378
  * @param {Event} event
310
379
  */
311
380
  self[closeEventHandler] = (event) => {
312
381
  const path = event.composedPath();
313
-
314
- for (const [, element] of Object.entries(path)) {
315
- if (element === self) {
316
- return;
317
- }
382
+ for (const element of path) {
383
+ if (element === self) return;
318
384
  }
319
-
320
385
  hide.call(self);
321
386
  };
322
387
 
323
388
  if (self[buttonBarSlotElementSymbol]) {
324
- self[buttonBarSlotElementSymbol].addEventListener("slotchange", (event) => {
389
+ self[buttonBarSlotElementSymbol].addEventListener("slotchange", () => {
325
390
  checkAndRearrangeButtons.call(self);
326
391
  });
327
392
  }
328
393
 
329
394
  if (self[popperElementSymbol]) {
330
- self[popperElementSymbol].addEventListener("slotchange", (event) => {
395
+ self[popperElementSymbol].addEventListener("slotchange", () => {
331
396
  checkAndRearrangeButtons.call(self);
332
397
  });
333
398
  }
334
399
 
335
- // data-monster-options
336
400
  self[attributeObserverSymbol][ATTRIBUTE_POPPER_POSITION] = function (value) {
337
401
  self.setOption("classes.button", value);
338
402
  };
339
403
 
340
- self[resizeObserverSymbol] = new ResizeObserver((entries) => {
341
- if (self[timerCallbackSymbol] instanceof DeadMansSwitch) {
342
- try {
343
- self[timerCallbackSymbol].touch();
344
- return;
345
- } catch (e) {
346
- // catch Error("has already run");
347
- if (e.message !== "has already run") {
348
- throw e;
349
- }
350
- delete self[timerCallbackSymbol];
351
- }
352
- }
353
-
354
- self[timerCallbackSymbol] = new DeadMansSwitch(200, () => {
355
- requestAnimationFrame(() => {
356
- updatePopper.call(self);
357
- self[dimensionsSymbol].setVia("data.calculated", false);
358
- try {
359
- checkAndRearrangeButtons.call(self);
360
- } catch (error) {
361
- addErrorAttribute(
362
- this,
363
- error?.message || "An error occurred while rearranging the buttons",
364
- );
365
- }
366
- });
367
- });
368
- });
404
+ self[resizeObserverSymbol] = new ResizeObserver(triggerRecalculation);
405
+ self[mutationObserverSymbol] = new MutationObserver(mutationCallback);
369
406
 
370
407
  initSlotChangedHandler.call(self);
371
408
  }
@@ -389,16 +426,21 @@ function checkAndRearrangeButtons() {
389
426
  * @return {Object}
390
427
  */
391
428
  function rearrangeButtons() {
392
- let sum = this[switchElementSymbol].offsetWidth;
429
+ let sum = 0;
430
+ // Only add the popper switch's width if it's currently visible.
431
+ if (isElementTrulyVisible(this[switchElementSymbol])) {
432
+ sum += this[switchElementSymbol].offsetWidth;
433
+ }
393
434
  const space = this[dimensionsSymbol].getVia("data.space");
394
435
 
395
436
  const buttonReferences = this[dimensionsSymbol].getVia(
396
437
  "data.buttonReferences",
397
438
  );
398
439
 
399
- for (const ref of buttonReferences) {
400
- sum += this[dimensionsSymbol].getVia(`data.button.${ref}`);
440
+ const visibleButtonsInMainSlot = [];
441
+ const buttonsToMoveToPopper = [];
401
442
 
443
+ for (const ref of buttonReferences) {
402
444
  let elements = getSlottedElements.call(
403
445
  this,
404
446
  '[data-monster-reference="' + ref + '"]',
@@ -422,15 +464,53 @@ function rearrangeButtons() {
422
464
  continue;
423
465
  }
424
466
 
425
- if (sum > space) {
426
- element.setAttribute("slot", "popper");
467
+ let buttonWidth = 0;
468
+ try {
469
+ buttonWidth = this[dimensionsSymbol].getVia(`data.button.${ref}`);
470
+ } catch (e) {
471
+ // If the path does not exist, pathfinder throws an error.
472
+ // In this case, we assume the width is 0.
473
+ // This can happen for buttons that have never been visible.
474
+ }
475
+ const style = getComputedStyle(element);
476
+
477
+ // A user-hidden button should not participate in layout calculations.
478
+ // We will assign it to the popper to keep it out of the flow.
479
+ if (style.display === "none") {
480
+ // This button is not visible. It could be user-hidden, or it could be
481
+ // in the popper which is currently hidden.
482
+ // We should only count it towards the popper if it has a non-zero width,
483
+ // which implies it's a genuinely overflowing button, not one that is
484
+ // user-hidden from the start.
485
+ if (buttonWidth > 0) {
486
+ buttonsToMoveToPopper.push(element);
487
+ } else {
488
+ // It has 0 width. Treat it as a truly hidden element.
489
+ // Put it in the main slot, where it will be invisible and take no space.
490
+ visibleButtonsInMainSlot.push(element);
491
+ }
492
+ continue;
493
+ }
494
+
495
+ const switchWidth = this[dimensionsSymbol].getVia("data.switchWidth") || 2;
496
+ if (sum + buttonWidth > space - switchWidth) { buttonsToMoveToPopper.push(element);
427
497
  } else {
428
- element.removeAttribute("slot");
498
+ sum += buttonWidth;
499
+ visibleButtonsInMainSlot.push(element);
429
500
  }
430
501
  }
431
502
 
432
- const inVisibleButtons = getSlottedElements.call(this, ":scope", "popper"); // null ↦ o
433
- if (inVisibleButtons.size > 0) {
503
+ for (const button of buttonsToMoveToPopper) {
504
+ button.setAttribute("slot", "popper");
505
+ }
506
+
507
+ for (const button of visibleButtonsInMainSlot) {
508
+ button.removeAttribute("slot");
509
+ }
510
+
511
+ const buttonsInPopper = getSlottedElements.call(this, ":scope", "popper");
512
+
513
+ if (buttonsInPopper.size > 0) {
434
514
  this[switchElementSymbol].classList.remove("hidden");
435
515
  } else {
436
516
  this[switchElementSymbol].classList.add("hidden");
@@ -483,9 +563,8 @@ function calculateButtonBarDimensions() {
483
563
  try {
484
564
  pixel = convertToPixels(width);
485
565
  } catch (e) {
486
- addAttributeToken(
566
+ addErrorAttribute(
487
567
  this,
488
- ATTRIBUTE_ERRORMESSAGE,
489
568
  e?.message || "An error occurred while calculating the dimensions",
490
569
  );
491
570
  }
@@ -503,9 +582,8 @@ function calculateButtonBarDimensions() {
503
582
  try {
504
583
  borderWidthWithoutUnit = convertToPixels(borderWidth);
505
584
  } catch (e) {
506
- addAttributeToken(
585
+ addErrorAttribute(
507
586
  this,
508
- ATTRIBUTE_ERRORMESSAGE,
509
587
  e?.message || "An error occurred while calculating the dimensions",
510
588
  );
511
589
  }
@@ -521,35 +599,44 @@ function calculateButtonBarDimensions() {
521
599
 
522
600
  const buttonReferences = [];
523
601
 
524
- const visibleButtons = getSlottedElements.call(this, ":scope", null); // null ↦ o
602
+ // Get all buttons, regardless of their current slot
603
+ const allButtons = Array.from(getSlottedElements.call(this, ":scope", null));
604
+ const popperButtons = Array.from(getSlottedElements.call(this, ":scope", "popper"));
605
+
606
+ const combinedButtons = [...allButtons, ...popperButtons].filter((button, index, self) => {
607
+ // Filter out duplicates based on data-monster-reference if present, or element itself
608
+ return self.findIndex(b => b.dataset.monsterReference === button.dataset.monsterReference || b === button) === index;
609
+ });
610
+
611
+ for (const button of combinedButtons) {
612
+ if (!(button instanceof HTMLElement)) {
613
+ continue;
614
+ }
525
615
 
526
- for (const [, button] of visibleButtons.entries()) {
527
616
  if (!button.hasAttribute("data-monster-reference")) {
528
- button.setAttribute("data-monster-reference", new ID("vb").toString());
617
+ button.setAttribute("data-monster-reference", new ID("btn").toString());
529
618
  }
530
619
 
531
620
  const ref = button.getAttribute("data-monster-reference");
532
621
  if (ref === null) continue;
533
622
 
534
623
  buttonReferences.push(ref);
535
- this[dimensionsSymbol].setVia(
536
- `data.button.${ref}`,
537
- calcBoxWidth.call(this, button),
538
- );
539
- }
540
624
 
541
- const invisibleButtons = getSlottedElements.call(this, ":scope", "popper"); // null o
542
- for (const [, button] of invisibleButtons.entries()) {
543
- if (!button.hasAttribute("data-monster-reference")) {
544
- button.setAttribute("data-monster-reference", new ID("ib").toString());
625
+ // Only calculate width for visible buttons. Assume invisible ones
626
+ // (e.g. in popper) have their width calculated previously and stored.
627
+ if (isElementTrulyVisible(button)) {
628
+ this[dimensionsSymbol].setVia(
629
+ `data.button.${ref}`,
630
+ calcBoxWidth.call(this, button),
631
+ );
545
632
  }
633
+ }
546
634
 
547
- const ref = button.getAttribute("data-monster-reference");
548
- if (ref === null) continue;
549
-
550
- if (ref.indexOf("ib") !== 0) {
551
- buttonReferences.push(ref);
552
- }
635
+ if (this[switchElementSymbol]) {
636
+ this[dimensionsSymbol].setVia(
637
+ "data.switchWidth",
638
+ this[switchElementSymbol].offsetWidth,
639
+ );
553
640
  }
554
641
 
555
642
  this[dimensionsSymbol].setVia("data.calculated", true);
@@ -561,10 +648,19 @@ function calculateButtonBarDimensions() {
561
648
  */
562
649
  function updateResizeObserverObservation() {
563
650
  this[resizeObserverSymbol].disconnect();
651
+ if (this[mutationObserverSymbol]) {
652
+ this[mutationObserverSymbol].disconnect();
653
+ }
564
654
 
565
655
  const slottedNodes = getSlottedElements.call(this);
566
656
  slottedNodes.forEach((node) => {
567
657
  this[resizeObserverSymbol].observe(node);
658
+ if (this[mutationObserverSymbol]) {
659
+ this[mutationObserverSymbol].observe(node, {
660
+ attributes: true,
661
+ attributeFilter: ["style", "class"],
662
+ });
663
+ }
568
664
  });
569
665
 
570
666
  requestAnimationFrame(() => {
@@ -17,9 +17,9 @@ import "../form/field-set.mjs";
17
17
  import { DeadMansSwitch } from "../../util/deadmansswitch.mjs";
18
18
  import { DataSet } from "../datatable/dataset.mjs";
19
19
  import {
20
- assembleMethodSymbol,
21
- registerCustomElement,
22
- getSlottedElements,
20
+ assembleMethodSymbol,
21
+ registerCustomElement,
22
+ getSlottedElements,
23
23
  } from "../../dom/customelement.mjs";
24
24
  import { datasourceLinkedElementSymbol } from "../datatable/util.mjs";
25
25
  import { FormStyleSheet } from "./stylesheet/form.mjs";
@@ -60,116 +60,116 @@ const debounceBindSymbol = Symbol("debounceBind");
60
60
  * @fires monster-changed
61
61
  */
62
62
  class Form extends DataSet {
63
- /**
64
- * @property {Object} templates Template definitions
65
- * @property {string} templates.main Main template
66
- * @property {Object} classes Class definitions
67
- * @property {string} classes.form Form class
68
- * @property {Object} writeBack Write back definitions
69
- * @property {string[]} writeBack.events Write back events
70
- * @property {Object} bind Bind definitions
71
- * @property {Object} reportValidity Report validity definitions
72
- * @property {string} reportValidity.selector Report validity selector
73
- * @property {boolean} features.mutationObserver Mutation observer feature
74
- * @property {boolean} features.writeBack Write back feature
75
- * @property {boolean} features.bind Bind feature
76
- */
77
- get defaults() {
78
- const obj = Object.assign({}, super.defaults, {
79
- templates: {
80
- main: getTemplate(),
81
- },
82
-
83
- classes: {
84
- form: "",
85
- },
86
-
87
- writeBack: {
88
- events: ["keyup", "click", "change", "drop", "touchend", "input"],
89
- },
90
-
91
- reportValidity: {
92
- selector:
93
- "input,select,textarea,monster-select,monster-toggle-switch,monster-password",
94
- },
95
-
96
- eventProcessing: true,
97
- });
98
-
99
- obj["features"]["mutationObserver"] = false;
100
- obj["features"]["writeBack"] = true;
101
-
102
- return obj;
103
- }
104
-
105
- /**
106
- *
107
- * @return {string}
108
- */
109
- static getTag() {
110
- return "monster-form";
111
- }
112
-
113
- /**
114
- * @return {CSSStyleSheet[]}
115
- */
116
- static getCSSStyleSheet() {
117
- return [FormStyleSheet, InvalidStyleSheet];
118
- }
119
-
120
- /**
121
- *
122
- */
123
- [assembleMethodSymbol]() {
124
- const selector = this.getOption("datasource.selector");
125
-
126
- if (!selector) {
127
- this[datasourceLinkedElementSymbol] = getDocument().createElement(
128
- "monster-datasource-dom",
129
- );
130
- }
131
-
132
- super[assembleMethodSymbol]();
133
-
134
- initControlReferences.call(this);
135
- initEventHandler.call(this);
136
- initDataSourceHandler.call(this);
137
- }
138
-
139
- /**
140
- * This method is called when the component is created.
141
- * @since 3.70.0
142
- * @return {Promise}
143
- */
144
- refresh() {
145
- return this.write().then(() => {
146
- super.refresh();
147
- return this;
148
- });
149
- }
150
-
151
- /**
152
- * Run reportValidation on all child html form controls.
153
- *
154
- * @since 2.10.0
155
- * @return {boolean}
156
- */
157
- reportValidity() {
158
- let valid = true;
159
-
160
- const selector = this.getOption("reportValidity.selector");
161
- const nodes = getSlottedElements.call(this, selector);
162
-
163
- nodes.forEach((node) => {
164
- if (typeof node.reportValidity === "function") {
165
- if (node.reportValidity() === false) {
166
- valid = false;
167
- }
168
- }
169
- });
170
-
171
- return valid;
172
- }
63
+ /**
64
+ * @property {Object} templates Template definitions
65
+ * @property {string} templates.main Main template
66
+ * @property {Object} classes Class definitions
67
+ * @property {string} classes.form Form class
68
+ * @property {Object} writeBack Write back definitions
69
+ * @property {string[]} writeBack.events Write back events
70
+ * @property {Object} bind Bind definitions
71
+ * @property {Object} reportValidity Report validity definitions
72
+ * @property {string} reportValidity.selector Report validity selector
73
+ * @property {boolean} features.mutationObserver Mutation observer feature
74
+ * @property {boolean} features.writeBack Write back feature
75
+ * @property {boolean} features.bind Bind feature
76
+ */
77
+ get defaults() {
78
+ const obj = Object.assign({}, super.defaults, {
79
+ templates: {
80
+ main: getTemplate(),
81
+ },
82
+
83
+ classes: {
84
+ form: "",
85
+ },
86
+
87
+ writeBack: {
88
+ events: ["keyup", "click", "change", "drop", "touchend", "input"],
89
+ },
90
+
91
+ reportValidity: {
92
+ selector:
93
+ "input,select,textarea,monster-select,monster-toggle-switch,monster-password",
94
+ },
95
+
96
+ eventProcessing: true,
97
+ });
98
+
99
+ obj["features"]["mutationObserver"] = false;
100
+ obj["features"]["writeBack"] = true;
101
+
102
+ return obj;
103
+ }
104
+
105
+ /**
106
+ *
107
+ * @return {string}
108
+ */
109
+ static getTag() {
110
+ return "monster-form";
111
+ }
112
+
113
+ /**
114
+ * @return {CSSStyleSheet[]}
115
+ */
116
+ static getCSSStyleSheet() {
117
+ return [FormStyleSheet, InvalidStyleSheet];
118
+ }
119
+
120
+ /**
121
+ *
122
+ */
123
+ [assembleMethodSymbol]() {
124
+ const selector = this.getOption("datasource.selector");
125
+
126
+ if (!selector) {
127
+ this[datasourceLinkedElementSymbol] = getDocument().createElement(
128
+ "monster-datasource-dom",
129
+ );
130
+ }
131
+
132
+ super[assembleMethodSymbol]();
133
+
134
+ initControlReferences.call(this);
135
+ initEventHandler.call(this);
136
+ initDataSourceHandler.call(this);
137
+ }
138
+
139
+ /**
140
+ * This method is called when the component is created.
141
+ * @since 3.70.0
142
+ * @return {Promise}
143
+ */
144
+ refresh() {
145
+ return this.write().then(() => {
146
+ super.refresh();
147
+ return this;
148
+ });
149
+ }
150
+
151
+ /**
152
+ * Run reportValidation on all child html form controls.
153
+ *
154
+ * @since 2.10.0
155
+ * @return {boolean}
156
+ */
157
+ reportValidity() {
158
+ let valid = true;
159
+
160
+ const selector = this.getOption("reportValidity.selector");
161
+ const nodes = getSlottedElements.call(this, selector);
162
+
163
+ nodes.forEach((node) => {
164
+ if (typeof node.reportValidity === "function") {
165
+ if (node.reportValidity() === false) {
166
+ valid = false;
167
+ }
168
+ }
169
+ });
170
+
171
+ return valid;
172
+ }
173
173
  }
174
174
 
175
175
  function initDataSourceHandler() {}
@@ -179,47 +179,47 @@ function initDataSourceHandler() {}
179
179
  * @return {initEventHandler}
180
180
  */
181
181
  function initEventHandler() {
182
- this[debounceBindSymbol] = {};
183
-
184
- if (this.getOption("features.writeBack") === true) {
185
- setTimeout(() => {
186
- const events = this.getOption("writeBack.events");
187
- for (const event of events) {
188
- this.addEventListener(event, (e) => {
189
- if (!this.reportValidity()) {
190
- this.classList.add("invalid");
191
- setTimeout(() => {
192
- this.classList.remove("invalid");
193
- }, 1000);
194
-
195
- return;
196
- }
197
-
198
- if (this[debounceWriteBackSymbol] instanceof DeadMansSwitch) {
199
- try {
200
- this[debounceWriteBackSymbol].touch();
201
- return;
202
- } catch (e) {
203
- if (e.message !== "has already run") {
204
- throw e;
205
- }
206
- delete this[debounceWriteBackSymbol];
207
- }
208
- }
209
-
210
- this[debounceWriteBackSymbol] = new DeadMansSwitch(200, () => {
211
- setTimeout(() => {
212
- this.write().catch((e) => {
213
- addAttributeToken(this, "error", e.message || `${e}`);
214
- });
215
- }, 0);
216
- });
217
- });
218
- }
219
- }, 0);
220
- }
221
-
222
- return this;
182
+ this[debounceBindSymbol] = {};
183
+
184
+ if (this.getOption("features.writeBack") === true) {
185
+ setTimeout(() => {
186
+ const events = this.getOption("writeBack.events");
187
+ for (const event of events) {
188
+ this.addEventListener(event, (e) => {
189
+ if (!this.reportValidity()) {
190
+ this.classList.add("invalid");
191
+ setTimeout(() => {
192
+ this.classList.remove("invalid");
193
+ }, 1000);
194
+
195
+ return;
196
+ }
197
+
198
+ if (this[debounceWriteBackSymbol] instanceof DeadMansSwitch) {
199
+ try {
200
+ this[debounceWriteBackSymbol].touch();
201
+ return;
202
+ } catch (e) {
203
+ if (e.message !== "has already run") {
204
+ throw e;
205
+ }
206
+ delete this[debounceWriteBackSymbol];
207
+ }
208
+ }
209
+
210
+ this[debounceWriteBackSymbol] = new DeadMansSwitch(200, () => {
211
+ setTimeout(() => {
212
+ this.write().catch((e) => {
213
+ addAttributeToken(this, "error", e.message || `${e}`);
214
+ });
215
+ }, 0);
216
+ });
217
+ });
218
+ }
219
+ }, 0);
220
+ }
221
+
222
+ return this;
223
223
  }
224
224
 
225
225
  /**
@@ -227,10 +227,10 @@ function initEventHandler() {
227
227
  * @return {FilterButton}
228
228
  */
229
229
  function initControlReferences() {
230
- if (!this.shadowRoot) {
231
- throw new Error("no shadow-root is defined");
232
- }
233
- return this;
230
+ if (!this.shadowRoot) {
231
+ throw new Error("no shadow-root is defined");
232
+ }
233
+ return this;
234
234
  }
235
235
 
236
236
  /**
@@ -238,8 +238,8 @@ function initControlReferences() {
238
238
  * @return {string}
239
239
  */
240
240
  function getTemplate() {
241
- // language=HTML
242
- return `
241
+ // language=HTML
242
+ return `
243
243
  <div data-monster-role="control" part="control">
244
244
  <form data-monster-attributes="disabled path:disabled | if:true, class path:classes.form"
245
245
  data-monster-role="form"
@@ -108,7 +108,10 @@ export function attachTabsHashSync(
108
108
 
109
109
  const newHash = createBracketedKeyValueHash(hashObj);
110
110
  if (location.hash !== newHash) {
111
+ const scrollX = window.scrollX;
112
+ const scrollY = window.scrollY;
111
113
  history.replaceState(null, "", newHash);
114
+ window.scrollTo(scrollX, scrollY);
112
115
  }
113
116
  lastKnownActiveTabId = activeTabId;
114
117
  lastKnownAllTabIds = allTabIds;