@schukai/monster 4.83.0 → 4.85.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,25 @@
2
2
 
3
3
 
4
4
 
5
+ ## [4.85.0] - 2026-01-08
6
+
7
+ ### Add Features
8
+
9
+ - Improve spinner visibility control in DatasourceStatus component
10
+
11
+
12
+
13
+ ## [4.84.0] - 2026-01-08
14
+
15
+ ### Add Features
16
+
17
+ - Add initial implementation for issue [#366](https://gitlab.schukai.com/oss/libraries/javascript/monster/issues/366) and [#367](https://gitlab.schukai.com/oss/libraries/javascript/monster/issues/367) with accompanying HTML and MJS files
18
+ ### Bug Fixes
19
+
20
+ - Enhance message-state-button synchronization for disabled state
21
+
22
+
23
+
5
24
  ## [4.83.0] - 2026-01-07
6
25
 
7
26
  ### Add Features
package/package.json CHANGED
@@ -1 +1 @@
1
- {"author":"Volker Schukai","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.83.0"}
1
+ {"author":"Volker Schukai","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.85.0"}
@@ -122,6 +122,11 @@ const copyAllElementSymbol = Symbol("copyAllElement");
122
122
  * @type {symbol}
123
123
  */
124
124
  const resizeObserverSymbol = Symbol("resizeObserver");
125
+ /**
126
+ * @private
127
+ * @type {symbol}
128
+ */
129
+ const suppressColumnConfigSaveSymbol = Symbol("suppressColumnConfigSave");
125
130
 
126
131
  /**
127
132
  * A DataTable
@@ -696,7 +701,11 @@ function updateColumnBar() {
696
701
  });
697
702
  }
698
703
 
704
+ this[suppressColumnConfigSaveSymbol] = true;
699
705
  this[columnBarElementSymbol].setOption("columns", columns);
706
+ queueMicrotask(() => {
707
+ this[suppressColumnConfigSaveSymbol] = false;
708
+ });
700
709
  }
701
710
 
702
711
  /**
@@ -870,7 +879,9 @@ function initEventHandler() {
870
879
  new Observer(() => {
871
880
  updateHeaderFromColumnBar.call(self);
872
881
  updateGrid.call(self);
873
- updateConfigColumnBar.call(self);
882
+ if (!self[suppressColumnConfigSaveSymbol]) {
883
+ updateConfigColumnBar.call(self);
884
+ }
874
885
  }),
875
886
  );
876
887
  }
@@ -45,6 +45,7 @@ const errorElementSymbol = Symbol.for("errorElement");
45
45
  * @type {symbol}
46
46
  */
47
47
  const datasourceLinkedElementSymbol = Symbol("datasourceLinkedElement");
48
+ const spinnerElementSymbol = Symbol("spinnerElement");
48
49
 
49
50
  /**
50
51
  * A simple dataset status component
@@ -105,6 +106,7 @@ class DatasourceStatus extends CustomElement {
105
106
 
106
107
  timeouts: {
107
108
  message: 4000,
109
+ spinnerMin: 200,
108
110
  },
109
111
 
110
112
  state: {
@@ -164,6 +166,7 @@ function initControlReferences() {
164
166
  this[errorElementSymbol] = this.shadowRoot.querySelector(
165
167
  "monster-context-error",
166
168
  );
169
+ this[spinnerElementSymbol] = this.shadowRoot.querySelector(".monster-spinner");
167
170
  }
168
171
 
169
172
  /**
@@ -183,9 +186,44 @@ function initEventHandler() {
183
186
  throw new TypeError("the element must be a datasource");
184
187
  }
185
188
 
186
- let fadeOutTimer = null;
189
+ let hideTimer = null;
190
+ let lastShowAt = 0;
191
+ let requestVersion = 0;
192
+
193
+ const setSpinnerState = (state) => {
194
+ self.setOption("state.spinner", state);
195
+ const spinner = self[spinnerElementSymbol];
196
+ if (spinner) {
197
+ spinner.setAttribute("data-monster-state-loader", state);
198
+ }
199
+ };
200
+ const clearHideTimer = () => {
201
+ if (hideTimer) {
202
+ clearTimeout(hideTimer);
203
+ hideTimer = null;
204
+ }
205
+ };
206
+ const getSpinnerMinTimeout = () => {
207
+ const value = Number(self.getOption("timeouts.spinnerMin", 0));
208
+ return Number.isFinite(value) ? Math.max(0, value) : 0;
209
+ };
210
+ const scheduleHide = (version) => {
211
+ clearHideTimer();
212
+ const elapsed = Date.now() - lastShowAt;
213
+ const delay = Math.max(0, getSpinnerMinTimeout() - elapsed);
214
+ hideTimer = setTimeout(() => {
215
+ hideTimer = null;
216
+ if (version !== requestVersion) {
217
+ return;
218
+ }
219
+ setSpinnerState("hide");
220
+ }, delay);
221
+ };
187
222
  const hideSpinner = () => {
188
- self.setOption("state.spinner", "hide");
223
+ setSpinnerState("hide");
224
+ };
225
+ const showSpinner = () => {
226
+ setSpinnerState("show");
189
227
  };
190
228
 
191
229
  this[datasourceLinkedElementSymbol] = element;
@@ -194,36 +232,22 @@ function initEventHandler() {
194
232
  if (typeof self[errorElementSymbol]?.resetErrorMessage === "function") {
195
233
  self[errorElementSymbol].resetErrorMessage();
196
234
  }
197
- if (fadeOutTimer) {
198
- clearTimeout(fadeOutTimer);
199
- fadeOutTimer = null;
200
- }
201
- fadeOutTimer = setTimeout(() => {
202
- fadeOutTimer = null;
203
- hideSpinner();
204
- }, 800);
235
+ scheduleHide(requestVersion);
205
236
  });
206
237
 
207
238
  element.addEventListener("monster-datasource-fetch", function () {
208
- if (fadeOutTimer) {
209
- clearTimeout(fadeOutTimer);
210
- fadeOutTimer = null;
211
- }
212
-
239
+ requestVersion += 1;
240
+ lastShowAt = Date.now();
241
+ clearHideTimer();
213
242
  if (typeof self[errorElementSymbol]?.resetErrorMessage === "function") {
214
243
  self[errorElementSymbol].resetErrorMessage();
215
244
  }
216
245
 
217
- self.setOption("state.spinner", "show");
246
+ showSpinner();
218
247
  });
219
248
 
220
249
  element.addEventListener("monster-datasource-error", function (event) {
221
- if (fadeOutTimer) {
222
- clearTimeout(fadeOutTimer);
223
- fadeOutTimer = null;
224
- }
225
-
226
- hideSpinner();
250
+ scheduleHide(requestVersion);
227
251
 
228
252
  const timeout = self.getOption("timeouts.message", 4000);
229
253
  let msg = "Cannot load data";
@@ -13,9 +13,10 @@
13
13
  */
14
14
 
15
15
  import { instanceSymbol } from "../../constants.mjs";
16
- import { ATTRIBUTE_ROLE } from "../../dom/constants.mjs";
16
+ import { ATTRIBUTE_DISABLED, ATTRIBUTE_ROLE } from "../../dom/constants.mjs";
17
17
  import {
18
18
  assembleMethodSymbol,
19
+ attributeObserverSymbol,
19
20
  registerCustomElement,
20
21
  } from "../../dom/customelement.mjs";
21
22
  import { isArray, isString } from "../../types/is.mjs";
@@ -411,9 +412,31 @@ function initDisabledSync() {
411
412
  if (self.getOption("features.disableButton", false) !== disabled) {
412
413
  self.setOption("features.disableButton", disabled);
413
414
  }
415
+
416
+ const button = self[buttonElementSymbol];
417
+ if (!button) {
418
+ return;
419
+ }
420
+
421
+ if (disabled) {
422
+ button.setAttribute(ATTRIBUTE_DISABLED, "");
423
+ } else {
424
+ button.removeAttribute(ATTRIBUTE_DISABLED);
425
+ }
426
+
427
+ if (isFunction(button.setOption)) {
428
+ button.setOption("disabled", disabled);
429
+ }
414
430
  };
415
431
 
416
432
  syncDisabled();
433
+ const existingObserver = self[attributeObserverSymbol]?.[ATTRIBUTE_DISABLED];
434
+ if (existingObserver) {
435
+ self[attributeObserverSymbol][ATTRIBUTE_DISABLED] = () => {
436
+ existingObserver.call(self);
437
+ syncDisabled();
438
+ };
439
+ }
417
440
  self.attachObserver(new Observer(syncDisabled));
418
441
  }
419
442
 
@@ -1039,7 +1039,7 @@ function initOptionObserver() {
1039
1039
  for (const list of updaters) {
1040
1040
  for (const updater of list) {
1041
1041
  const d = clone(self[internalSymbol].getRealSubject()["options"]);
1042
- Object.assign(updater.getSubject(), d);
1042
+ syncUpdaterSubject(updater.getSubject(), d);
1043
1043
  }
1044
1044
  }
1045
1045
  }),
@@ -1068,6 +1068,39 @@ function initOptionObserver() {
1068
1068
  };
1069
1069
  }
1070
1070
 
1071
+ /**
1072
+ * @private
1073
+ * @param {object} target
1074
+ * @param {object} source
1075
+ * @return {void}
1076
+ */
1077
+ function syncUpdaterSubject(target, source) {
1078
+ if (!isObject(source)) {
1079
+ return;
1080
+ }
1081
+
1082
+ for (const [key, value] of Object.entries(source)) {
1083
+ if (isArray(value)) {
1084
+ if (!isArray(target?.[key])) {
1085
+ target[key] = [];
1086
+ }
1087
+ target[key].length = 0;
1088
+ target[key].push(...clone(value));
1089
+ continue;
1090
+ }
1091
+
1092
+ if (isObject(value)) {
1093
+ if (!isObject(target?.[key]) || isArray(target?.[key])) {
1094
+ target[key] = {};
1095
+ }
1096
+ syncUpdaterSubject(target[key], value);
1097
+ continue;
1098
+ }
1099
+
1100
+ target[key] = value;
1101
+ }
1102
+ }
1103
+
1071
1104
  /**
1072
1105
  * @private
1073
1106
  * @return {object}
@@ -0,0 +1,134 @@
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 html1 = `
13
+ <div id="test1">
14
+ </div>
15
+ `;
16
+
17
+ let html2 = `
18
+ <div id="test2">
19
+ <monster-message-state-button data-monster-option-labels-button="Save">
20
+ Save
21
+ </monster-message-state-button>
22
+ </div>
23
+ `;
24
+
25
+ let MessageStateButton;
26
+
27
+ describe("MessageStateButton", function () {
28
+ before(function (done) {
29
+ initJSDOM().then(() => {
30
+ import("element-internals-polyfill").catch((e) => done(e));
31
+
32
+ if (!global.ResizeObserver) {
33
+ global.ResizeObserver = ResizeObserverMock;
34
+ }
35
+
36
+ import("../../../../source/components/form/message-state-button.mjs")
37
+ .then((m) => {
38
+ MessageStateButton = m["MessageStateButton"];
39
+ done();
40
+ })
41
+ .catch((e) => done(e));
42
+ });
43
+ });
44
+
45
+ describe("new MessageStateButton", function () {
46
+ beforeEach(() => {
47
+ let mocks = document.getElementById("mocks");
48
+ mocks.innerHTML = html1;
49
+ });
50
+
51
+ afterEach(() => {
52
+ let mocks = document.getElementById("mocks");
53
+ mocks.innerHTML = "";
54
+ });
55
+
56
+ describe("create from template", function () {
57
+ beforeEach(() => {
58
+ let mocks = document.getElementById("mocks");
59
+ mocks.innerHTML = html2;
60
+ });
61
+
62
+ afterEach(() => {
63
+ let mocks = document.getElementById("mocks");
64
+ mocks.innerHTML = "";
65
+ });
66
+
67
+ it("should contain monster-message-state-button", function () {
68
+ expect(document.getElementById("test2")).contain.html(
69
+ "<monster-message-state-button",
70
+ );
71
+ });
72
+ });
73
+
74
+ describe("document.createElement", function () {
75
+ it("should instance of message-state-button", function () {
76
+ expect(document.createElement("monster-message-state-button")).is
77
+ .instanceof(MessageStateButton);
78
+ });
79
+ });
80
+ });
81
+
82
+ describe("disabled toggle", function () {
83
+ afterEach(() => {
84
+ let mocks = document.getElementById("mocks");
85
+ mocks.innerHTML = "";
86
+ });
87
+
88
+ it("should sync disabled attribute to inner button", function (done) {
89
+ let mocks = document.getElementById("mocks");
90
+ const button = document.createElement("monster-message-state-button");
91
+ button.innerHTML = "Save";
92
+ mocks.appendChild(button);
93
+
94
+ setTimeout(() => {
95
+ try {
96
+ const inner = button.shadowRoot.querySelector(
97
+ "monster-state-button",
98
+ );
99
+ expect(inner).to.exist;
100
+
101
+ button.setAttribute("disabled", "");
102
+ setTimeout(() => {
103
+ try {
104
+ expect(inner.hasAttribute("disabled")).to.be.true;
105
+
106
+ button.removeAttribute("disabled");
107
+ setTimeout(() => {
108
+ try {
109
+ expect(inner.hasAttribute("disabled")).to.be.false;
110
+
111
+ button.setAttribute("disabled", "");
112
+ setTimeout(() => {
113
+ try {
114
+ expect(inner.hasAttribute("disabled")).to.be.true;
115
+ done();
116
+ } catch (e) {
117
+ done(e);
118
+ }
119
+ }, 0);
120
+ } catch (e) {
121
+ done(e);
122
+ }
123
+ }, 0);
124
+ } catch (e) {
125
+ done(e);
126
+ }
127
+ }, 0);
128
+ } catch (e) {
129
+ done(e);
130
+ }
131
+ }, 0);
132
+ });
133
+ });
134
+ });
@@ -7,6 +7,7 @@ import "../cases/components/form/buy-box.mjs";
7
7
  import "../cases/components/form/button-bar.mjs";
8
8
  import "../cases/components/form/reload.mjs";
9
9
  import "../cases/components/form/state-button.mjs";
10
+ import "../cases/components/form/message-state-button.mjs";
10
11
  import "../cases/components/form/select.mjs";
11
12
  import "../cases/components/form/confirm-button.mjs";
12
13
  import "../cases/components/form/form.mjs";