@schukai/monster 4.89.0 → 4.91.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,23 @@
2
2
 
3
3
 
4
4
 
5
+ ## [4.91.0] - 2026-01-12
6
+
7
+ ### Add Features
8
+
9
+ - Update records and improve diff functionality [#372](https://gitlab.schukai.com/oss/libraries/javascript/monster/issues/372)
10
+ - Add new dynamic form element with repeatable fields [#372](https://gitlab.schukai.com/oss/libraries/javascript/monster/issues/372)
11
+
12
+
13
+
14
+ ## [4.90.0] - 2026-01-11
15
+
16
+ ### Add Features
17
+
18
+ - Enhance origin values management in SaveButton component
19
+
20
+
21
+
5
22
  ## [4.89.0] - 2026-01-11
6
23
 
7
24
  ### 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.89.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.91.0"}
@@ -32,7 +32,7 @@ import {
32
32
  findElementWithSelectorUpwards,
33
33
  getDocument,
34
34
  } from "../../dom/util.mjs";
35
- import { isString, isArray } from "../../types/is.mjs";
35
+ import { isString, isArray, isObject } from "../../types/is.mjs";
36
36
  import { Observer } from "../../types/observer.mjs";
37
37
  import { TokenList } from "../../types/tokenlist.mjs";
38
38
  import { clone } from "../../util/clone.mjs";
@@ -62,7 +62,9 @@ const stateButtonElementSymbol = Symbol("stateButtonElement");
62
62
  * @private
63
63
  * @type {symbol}
64
64
  */
65
- const originValuesSymbol = Symbol("originValues");
65
+ const originValuesSymbol = Symbol.for(
66
+ "@schukai/monster/components/datatable/save-button@@originValues",
67
+ );
66
68
 
67
69
  /**
68
70
  * @private
@@ -72,6 +74,7 @@ const badgeElementSymbol = Symbol("badgeElement");
72
74
  const saveInFlightSymbol = Symbol("saveInFlight");
73
75
  const pendingResetSymbol = Symbol("pendingReset");
74
76
  const fetchInFlightSymbol = Symbol("fetchInFlight");
77
+ const originInitializedSymbol = Symbol("originInitialized");
75
78
 
76
79
  /**
77
80
  * A save button component
@@ -133,7 +136,7 @@ class SaveButton extends CustomElement {
133
136
  selector: null,
134
137
  },
135
138
 
136
- changes: "0",
139
+ changes: "",
137
140
 
138
141
  ignoreChanges: [],
139
142
 
@@ -192,22 +195,21 @@ class SaveButton extends CustomElement {
192
195
  }
193
196
 
194
197
  if (element instanceof RestDatasource) {
195
- element.addEventListener("monster-datasource-fetch", (event) => {
198
+ element.addEventListener("monster-datasource-fetch", () => {
196
199
  if (self[saveInFlightSymbol]) {
197
200
  self[pendingResetSymbol] = true;
198
201
  return;
199
202
  }
200
203
  self[fetchInFlightSymbol] = true;
201
- self[originValuesSymbol] = null;
204
+ clearOriginValues.call(self);
202
205
  });
203
206
  element.addEventListener("monster-datasource-fetched", () => {
204
207
  self[fetchInFlightSymbol] = false;
205
- if (!self[originValuesSymbol]) {
206
- self[originValuesSymbol] = clone(
207
- self[datasourceLinkedElementSymbol].data,
208
- );
209
- updateChangesState.call(self);
210
- }
208
+ setOriginValues.call(
209
+ self,
210
+ clone(self[datasourceLinkedElementSymbol].data),
211
+ );
212
+ updateChangesState.call(self);
211
213
  });
212
214
  element.addEventListener("monster-datasource-error", () => {
213
215
  self[fetchInFlightSymbol] = false;
@@ -219,22 +221,25 @@ class SaveButton extends CustomElement {
219
221
  new Observer(handleDataSourceChanges.bind(this)),
220
222
  );
221
223
 
222
- self[originValuesSymbol] = null;
224
+ clearOriginValues.call(self);
223
225
 
224
226
  element.datasource.attachObserver(
225
227
  new Observer(function () {
226
228
  if (self[fetchInFlightSymbol] === true) {
227
229
  return;
228
230
  }
229
- if (!self[originValuesSymbol]) {
230
- self[originValuesSymbol] = clone(
231
- self[datasourceLinkedElementSymbol].data,
231
+ if (!getOriginValues.call(self)) {
232
+ setOriginValues.call(
233
+ self,
234
+ clone(self[datasourceLinkedElementSymbol].data),
232
235
  );
233
236
  }
234
237
 
235
238
  updateChangesState.call(self);
236
239
  }),
237
240
  );
241
+
242
+ syncOriginValues.call(self);
238
243
  }
239
244
 
240
245
  this.attachObserver(
@@ -253,6 +258,55 @@ class SaveButton extends CustomElement {
253
258
  }
254
259
  }
255
260
 
261
+ /**
262
+ * @private
263
+ */
264
+ function syncOriginValues() {
265
+ if (getOriginValues.call(this)) {
266
+ return;
267
+ }
268
+ const data = this[datasourceLinkedElementSymbol]?.data;
269
+ if (!data) {
270
+ return;
271
+ }
272
+ setOriginValues.call(this, clone(data));
273
+ updateChangesState.call(this);
274
+ }
275
+
276
+ /**
277
+ * @private
278
+ * @return {*}
279
+ */
280
+ function getOriginValues() {
281
+ const datasource = this[datasourceLinkedElementSymbol];
282
+ return datasource ? datasource[originValuesSymbol] : null;
283
+ }
284
+
285
+ /**
286
+ * @private
287
+ * @param {*} value
288
+ * @return {void}
289
+ */
290
+ function setOriginValues(value) {
291
+ const datasource = this[datasourceLinkedElementSymbol];
292
+ if (datasource) {
293
+ datasource[originValuesSymbol] = value;
294
+ }
295
+ this[originInitializedSymbol] = true;
296
+ }
297
+
298
+ /**
299
+ * @private
300
+ * @return {void}
301
+ */
302
+ function clearOriginValues() {
303
+ const datasource = this[datasourceLinkedElementSymbol];
304
+ if (datasource) {
305
+ datasource[originValuesSymbol] = null;
306
+ }
307
+ this[originInitializedSymbol] = false;
308
+ }
309
+
256
310
  function getTranslations() {
257
311
  const locale = getLocaleOfDocument();
258
312
  switch (locale.language) {
@@ -321,19 +375,9 @@ function initControlReferences() {
321
375
 
322
376
  if (this[stateButtonElementSymbol]) {
323
377
  queueMicrotask(() => {
324
- const states = {
325
- changed: new State(
326
- "changed",
327
- '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-cloud-arrow-up" viewBox="0 0 16 16">' +
328
- '<path fill-rule="evenodd" d="M7.646 5.146a.5.5 0 0 1 .708 0l2 2a.5.5 0 0 1-.708.708L8.5 6.707V10.5a.5.5 0 0 1-1 0V6.707L6.354 7.854a.5.5 0 1 1-.708-.708z"/>' +
329
- '<path d="M4.406 3.342A5.53 5.53 0 0 1 8 2c2.69 0 4.923 2 5.166 4.579C14.758 6.804 16 8.137 16 9.773 16 11.569 14.502 13 12.687 13H3.781C1.708 13 0 11.366 0 9.318c0-1.763 1.266-3.223 2.942-3.593.143-.863.698-1.723 1.464-2.383m.653.757c-.757.653-1.153 1.44-1.153 2.056v.448l-.445.049C2.064 6.805 1 7.952 1 9.318 1 10.785 2.23 12 3.781 12h8.906C13.98 12 15 10.988 15 9.773c0-1.216-1.02-2.228-2.313-2.228h-.5v-.5C12.188 4.825 10.328 3 8 3a4.53 4.53 0 0 0-2.941 1.1z"/>' +
330
- "</svg>",
331
- ),
332
- };
333
-
378
+ ensureChangedState.call(this);
334
379
  this[stateButtonElementSymbol].removeState();
335
380
  this[stateButtonElementSymbol].setOption("disabled", "disabled");
336
- this[stateButtonElementSymbol].setOption("states", states);
337
381
  this[stateButtonElementSymbol].setOption(
338
382
  "labels.button",
339
383
  this.getOption("labels.button"),
@@ -356,16 +400,18 @@ function initEventHandler() {
356
400
  this[saveInFlightSymbol] = true;
357
401
  this[stateButtonElementSymbol].setOption("disabled", true);
358
402
 
359
- flushLinkedForms.call(this)
403
+ flushLinkedForms
404
+ .call(this)
360
405
  .then(() => this[datasourceLinkedElementSymbol].write())
361
406
  .then(() => {
362
- this[originValuesSymbol] = null;
363
- this[originValuesSymbol] = clone(
364
- this[datasourceLinkedElementSymbol].data,
407
+ clearOriginValues.call(this);
408
+ setOriginValues.call(
409
+ this,
410
+ clone(this[datasourceLinkedElementSymbol].data),
365
411
  );
366
412
  this[stateButtonElementSymbol].removeState();
367
413
  this[stateButtonElementSymbol].setOption("disabled", true);
368
- this.setOption("changes", 0);
414
+ this.setOption("changes", "");
369
415
  this.setOption(
370
416
  "classes.badge",
371
417
  new TokenList(this.getOption("classes.badge"))
@@ -381,7 +427,7 @@ function initEventHandler() {
381
427
  this[saveInFlightSymbol] = false;
382
428
  if (this[pendingResetSymbol]) {
383
429
  this[pendingResetSymbol] = false;
384
- this[originValuesSymbol] = null;
430
+ clearOriginValues.call(this);
385
431
  }
386
432
  });
387
433
  });
@@ -463,17 +509,33 @@ function flushLinkedForms() {
463
509
  function updateChangesState() {
464
510
  const currentValues = this[datasourceLinkedElementSymbol]?.datasource?.get();
465
511
  const ignoreChanges = this.getOption("ignoreChanges");
466
- let result = diff(this[originValuesSymbol], currentValues);
512
+ const originValues = getOriginValues.call(this);
513
+
514
+ if (
515
+ this[originInitializedSymbol] !== true ||
516
+ this[fetchInFlightSymbol] === true ||
517
+ originValues === null ||
518
+ originValues === undefined ||
519
+ (isObject(originValues) &&
520
+ Object.keys(originValues).length === 0 &&
521
+ isObject(currentValues) &&
522
+ Object.keys(currentValues).length === 0)
523
+ ) {
524
+ this.setOption("changes", "");
525
+ this.setOption(
526
+ "classes.badge",
527
+ new TokenList(this.getOption("classes.badge")).add("hidden").toString(),
528
+ );
529
+ return;
530
+ }
531
+ let result = diff(originValues, currentValues);
467
532
 
468
533
  if (
469
534
  this.getOption("logLevel") === "debug" ||
470
535
  location.search.includes("logLevel=debug")
471
536
  ) {
472
537
  console.groupCollapsed("SaveButton");
473
- console.log(
474
- "originValues",
475
- JSON.parse(JSON.stringify(this[originValuesSymbol])),
476
- );
538
+ console.log("originValues", JSON.parse(JSON.stringify(originValues)));
477
539
  console.log("currentValues", JSON.parse(JSON.stringify(currentValues)));
478
540
  console.log("result of diff", result);
479
541
  console.log("ignoreChanges", ignoreChanges);
@@ -521,10 +583,12 @@ function updateChangesState() {
521
583
  }
522
584
  }
523
585
 
524
- if (isArray(result) && result.length > 0) {
586
+ const changeCount = countChanges(result);
587
+ if (changeCount > 0) {
588
+ ensureChangedState.call(this);
525
589
  this[stateButtonElementSymbol].setState("changed");
526
590
  this[stateButtonElementSymbol].setOption("disabled", false);
527
- this.setOption("changes", result.length);
591
+ this.setOption("changes", changeCount);
528
592
  this.setOption(
529
593
  "classes.badge",
530
594
  new TokenList(this.getOption("classes.badge"))
@@ -534,7 +598,7 @@ function updateChangesState() {
534
598
  } else {
535
599
  this[stateButtonElementSymbol].removeState();
536
600
  this[stateButtonElementSymbol].setOption("disabled", true);
537
- this.setOption("changes", 0);
601
+ this.setOption("changes", "");
538
602
  this.setOption(
539
603
  "classes.badge",
540
604
  new TokenList(this.getOption("classes.badge")).add("hidden").toString(),
@@ -542,6 +606,117 @@ function updateChangesState() {
542
606
  }
543
607
  }
544
608
 
609
+ /**
610
+ * @private
611
+ * @param {Array} changes
612
+ * @return {number}
613
+ */
614
+ function countChanges(changes) {
615
+ if (!isArray(changes) || changes.length === 0) {
616
+ return 0;
617
+ }
618
+
619
+ let total = 0;
620
+ for (const change of changes) {
621
+ if (change?.operator === "add") {
622
+ const nonEmpty = countNonEmptyLeafValues(change?.second?.value);
623
+ total += Math.max(1, nonEmpty);
624
+ continue;
625
+ }
626
+
627
+ if (change?.operator === "remove") {
628
+ const nonEmpty = countNonEmptyLeafValues(change?.first?.value);
629
+ total += Math.max(1, nonEmpty);
630
+ continue;
631
+ }
632
+
633
+ total += 1;
634
+ }
635
+
636
+ return total;
637
+ }
638
+
639
+ /**
640
+ * @private
641
+ * @param {*} value
642
+ * @return {number}
643
+ */
644
+ function countNonEmptyLeafValues(value) {
645
+ if (value === null || value === undefined) {
646
+ return 0;
647
+ }
648
+
649
+ if (value instanceof Date) {
650
+ return 1;
651
+ }
652
+
653
+ if (typeof value === "string") {
654
+ return value.trim() === "" ? 0 : 1;
655
+ }
656
+
657
+ if (typeof value === "number") {
658
+ return Number.isFinite(value) ? 1 : 0;
659
+ }
660
+
661
+ if (typeof value === "boolean") {
662
+ return 1;
663
+ }
664
+
665
+ if (isArray(value)) {
666
+ if (value.length === 0) {
667
+ return 0;
668
+ }
669
+ return value.reduce((sum, item) => sum + countNonEmptyLeafValues(item), 0);
670
+ }
671
+
672
+ if (isObject(value)) {
673
+ const keys = Object.keys(value);
674
+ if (keys.length === 0) {
675
+ return 0;
676
+ }
677
+ return keys.reduce(
678
+ (sum, key) => sum + countNonEmptyLeafValues(value[key]),
679
+ 0,
680
+ );
681
+ }
682
+
683
+ return 1;
684
+ }
685
+
686
+ /**
687
+ * @private
688
+ * @return {void}
689
+ */
690
+ function ensureChangedState() {
691
+ const stateButton = this[stateButtonElementSymbol];
692
+ if (!stateButton || typeof stateButton.getOption !== "function") {
693
+ return;
694
+ }
695
+
696
+ if (stateButton.getOption("states.changed")) {
697
+ return;
698
+ }
699
+
700
+ const states = Object.assign({}, stateButton.getOption("states") || {}, {
701
+ changed: getChangedState(),
702
+ });
703
+ stateButton.setOption("states", states);
704
+ }
705
+
706
+ /**
707
+ * @private
708
+ * @return {State}
709
+ */
710
+ function getChangedState() {
711
+ return new State(
712
+ "changed",
713
+ '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-cloud-arrow-up" viewBox="0 0 16 16">' +
714
+ '<path fill-rule="evenodd" d="M7.646 5.146a.5.5 0 0 1 .708 0l2 2a.5.5 0 0 1-.708.708L8.5 6.707V10.5a.5.5 0 0 1-1 0V6.707L6.354 7.854a.5.5 0 1 1-.708-.708z"/>' +
715
+ '<path d="M4.406 3.342A5.53 5.53 0 0 1 8 2c2.69 0 4.923 2 5.166 4.579C14.758 6.804 16 8.137 16 9.773 16 11.569 14.502 13 12.687 13H3.781C1.708 13 0 11.366 0 9.318c0-1.763 1.266-3.223 2.942-3.593.143-.863.698-1.723 1.464-2.383m.653.757c-.757.653-1.153 1.44-1.153 2.056v.448l-.445.049C2.064 6.805 1 7.952 1 9.318 1 10.785 2.23 12 3.781 12h8.906C13.98 12 15 10.988 15 9.773c0-1.216-1.02-2.228-2.313-2.228h-.5v-.5C12.188 4.825 10.328 3 8 3a4.53 4.53 0 0 0-2.941 1.1z"/>' +
716
+ "</svg>",
717
+ );
718
+ }
719
+
545
720
  /**
546
721
  * @param {Object} options
547
722
  * @deprecated 2024-12-31