@schukai/monster 4.104.0 → 4.105.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 +13 -0
- package/package.json +1 -1
- package/source/components/datatable/dataset.mjs +63 -4
- package/source/components/form/form.mjs +2 -0
- package/source/components/layout/tabs.mjs +10 -0
- package/source/types/version.mjs +1 -1
- package/test/cases/components/datatable/writeback-sanitizer.mjs +104 -0
- package/test/cases/components/layout/tabs.mjs +24 -1
- package/test/cases/monster.mjs +1 -1
- package/test/web/test.html +2 -2
- package/test/web/tests.js +338 -55
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
|
|
4
4
|
|
|
5
|
+
## [4.105.0] - 2026-01-22
|
|
6
|
+
|
|
7
|
+
### Add Features
|
|
8
|
+
|
|
9
|
+
- Shorten tab labels for better readability
|
|
10
|
+
- Add write-back sanitizer for cleaner dataset diffs
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
- tabs labels
|
|
14
|
+
- update
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
|
|
5
18
|
## [4.104.0] - 2026-01-19
|
|
6
19
|
|
|
7
20
|
### 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.
|
|
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.105.0"}
|
|
@@ -26,7 +26,7 @@ import {
|
|
|
26
26
|
getDocument,
|
|
27
27
|
getWindow,
|
|
28
28
|
} from "../../dom/util.mjs";
|
|
29
|
-
import { isString } from "../../types/is.mjs";
|
|
29
|
+
import { isArray, isObject, isString } from "../../types/is.mjs";
|
|
30
30
|
import { Observer } from "../../types/observer.mjs";
|
|
31
31
|
import {
|
|
32
32
|
ATTRIBUTE_DATASOURCE_SELECTOR,
|
|
@@ -40,6 +40,8 @@ import {
|
|
|
40
40
|
} from "./util.mjs";
|
|
41
41
|
import { FormStyleSheet } from "../stylesheet/form.mjs";
|
|
42
42
|
import { addErrorAttribute } from "../../dom/error.mjs";
|
|
43
|
+
import { Pipe } from "../../data/pipe.mjs";
|
|
44
|
+
import { clone } from "../../util/clone.mjs";
|
|
43
45
|
|
|
44
46
|
export { DataSet };
|
|
45
47
|
|
|
@@ -135,6 +137,11 @@ class DataSet extends CustomElement {
|
|
|
135
137
|
"input, select, textarea, monster-select, monster-toggle-switch",
|
|
136
138
|
},
|
|
137
139
|
|
|
140
|
+
writeBack: {
|
|
141
|
+
transformer: null,
|
|
142
|
+
callbacks: null,
|
|
143
|
+
},
|
|
144
|
+
|
|
138
145
|
data: {},
|
|
139
146
|
});
|
|
140
147
|
|
|
@@ -204,6 +211,17 @@ class DataSet extends CustomElement {
|
|
|
204
211
|
pathWithIndex = String(index);
|
|
205
212
|
}
|
|
206
213
|
|
|
214
|
+
let sanitizedData;
|
|
215
|
+
try {
|
|
216
|
+
sanitizedData = applyWriteBackTransform.call(
|
|
217
|
+
this,
|
|
218
|
+
clone(internalData),
|
|
219
|
+
);
|
|
220
|
+
} catch (e) {
|
|
221
|
+
reject(e);
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
|
|
207
225
|
if (this.getOption("features.skipNoopWrite") === true) {
|
|
208
226
|
const currentData = this[datasourceLinkedElementSymbol]?.data;
|
|
209
227
|
let currentValue;
|
|
@@ -214,7 +232,7 @@ class DataSet extends CustomElement {
|
|
|
214
232
|
currentValue = undefined;
|
|
215
233
|
}
|
|
216
234
|
}
|
|
217
|
-
if (diff(currentValue,
|
|
235
|
+
if (diff(currentValue, sanitizedData).length === 0) {
|
|
218
236
|
resolve();
|
|
219
237
|
return;
|
|
220
238
|
}
|
|
@@ -223,7 +241,7 @@ class DataSet extends CustomElement {
|
|
|
223
241
|
if (this.getOption("logLevel") === "debug") {
|
|
224
242
|
console.log("monster-dataset: write", {
|
|
225
243
|
path: pathWithIndex,
|
|
226
|
-
data:
|
|
244
|
+
data: sanitizedData,
|
|
227
245
|
});
|
|
228
246
|
}
|
|
229
247
|
|
|
@@ -236,7 +254,7 @@ class DataSet extends CustomElement {
|
|
|
236
254
|
const unref = JSON.stringify(data);
|
|
237
255
|
const ref = JSON.parse(unref);
|
|
238
256
|
|
|
239
|
-
new Pathfinder(ref).setVia(pathWithIndex,
|
|
257
|
+
new Pathfinder(ref).setVia(pathWithIndex, sanitizedData);
|
|
240
258
|
|
|
241
259
|
this[datasourceLinkedElementSymbol].data = ref;
|
|
242
260
|
|
|
@@ -447,4 +465,45 @@ function getTemplate() {
|
|
|
447
465
|
`;
|
|
448
466
|
}
|
|
449
467
|
|
|
468
|
+
/**
|
|
469
|
+
* @private
|
|
470
|
+
* @param {Object|Array} data
|
|
471
|
+
* @return {Object|Array}
|
|
472
|
+
*/
|
|
473
|
+
function applyWriteBackTransform(data) {
|
|
474
|
+
const transformer = this.getOption("writeBack.transformer");
|
|
475
|
+
if (transformer === undefined || transformer === null || transformer === "") {
|
|
476
|
+
return clone(data);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
const pipe = new Pipe(transformer);
|
|
480
|
+
const callbacks = this.getOption("writeBack.callbacks");
|
|
481
|
+
|
|
482
|
+
if (isArray(callbacks)) {
|
|
483
|
+
for (const callback of callbacks) {
|
|
484
|
+
if (typeof callback === "function") {
|
|
485
|
+
pipe.setCallback(callback);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
if (isObject(callbacks)) {
|
|
491
|
+
for (const key in callbacks) {
|
|
492
|
+
if (
|
|
493
|
+
Object.prototype.hasOwnProperty.call(callbacks, key) &&
|
|
494
|
+
typeof callbacks[key] === "function"
|
|
495
|
+
) {
|
|
496
|
+
pipe.setCallback(key, callbacks[key]);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
const result = pipe.run(data);
|
|
502
|
+
if (typeof result === "undefined") {
|
|
503
|
+
return clone(data);
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
return clone(result);
|
|
507
|
+
}
|
|
508
|
+
|
|
450
509
|
registerCustomElement(DataSet);
|
|
@@ -1326,6 +1326,16 @@ function getButtonLabel(node) {
|
|
|
1326
1326
|
label = "";
|
|
1327
1327
|
}
|
|
1328
1328
|
|
|
1329
|
+
if (setLabel === true) {
|
|
1330
|
+
if (label.trim() === "") {
|
|
1331
|
+
label = node.textContent || "";
|
|
1332
|
+
}
|
|
1333
|
+
label = label.replace(/\s+/g, " ").trim();
|
|
1334
|
+
const words = label.split(" ").filter((word) => word !== "");
|
|
1335
|
+
if (words.length > 3) {
|
|
1336
|
+
label = `${words.slice(0, 3).join(" ")}…`;
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1329
1339
|
label = label.trim();
|
|
1330
1340
|
|
|
1331
1341
|
if (label === "") {
|
package/source/types/version.mjs
CHANGED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import * as chai from 'chai';
|
|
2
|
+
import {chaiDom} from "../../../util/chai-dom.mjs";
|
|
3
|
+
import {initJSDOM} from "../../../util/jsdom.mjs";
|
|
4
|
+
|
|
5
|
+
let expect = chai.expect;
|
|
6
|
+
chai.use(chaiDom);
|
|
7
|
+
|
|
8
|
+
let Dataset;
|
|
9
|
+
|
|
10
|
+
function buildDatasource(id, payload) {
|
|
11
|
+
const ds = document.createElement('monster-datasource-dom');
|
|
12
|
+
ds.id = id;
|
|
13
|
+
ds.innerHTML = `<script type="application/json">${JSON.stringify(payload)}</script>`;
|
|
14
|
+
return ds;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
describe('Dataset writeBack sanitizer', function () {
|
|
18
|
+
|
|
19
|
+
before(async function () {
|
|
20
|
+
await initJSDOM();
|
|
21
|
+
await import("element-internals-polyfill").catch(() => {});
|
|
22
|
+
await import("../../../../source/components/datatable/datasource/dom.mjs");
|
|
23
|
+
const mod = await import("../../../../source/components/datatable/dataset.mjs");
|
|
24
|
+
Dataset = mod['DataSet'];
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
beforeEach(() => {
|
|
28
|
+
const mocks = document.getElementById('mocks');
|
|
29
|
+
mocks.innerHTML = '';
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
afterEach(() => {
|
|
33
|
+
const mocks = document.getElementById('mocks');
|
|
34
|
+
mocks.innerHTML = '';
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('sanitizes data before write and avoids side effects', async function () {
|
|
38
|
+
const mocks = document.getElementById('mocks');
|
|
39
|
+
const ds = buildDatasource('ds-sanitize', [{}]);
|
|
40
|
+
mocks.appendChild(ds);
|
|
41
|
+
|
|
42
|
+
const dataset = document.createElement('monster-dataset');
|
|
43
|
+
expect(dataset).is.instanceof(Dataset);
|
|
44
|
+
|
|
45
|
+
dataset.setOption('datasource.selector', '#ds-sanitize');
|
|
46
|
+
dataset.setOption('mapping.data', '');
|
|
47
|
+
|
|
48
|
+
const initial = {name: '', email: 'a@b', tags: []};
|
|
49
|
+
dataset.setOption('data', initial);
|
|
50
|
+
|
|
51
|
+
dataset.setOption('writeBack.transformer', 'call:sanitize');
|
|
52
|
+
dataset.setOption('writeBack.callbacks', {
|
|
53
|
+
sanitize: (obj) => {
|
|
54
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
55
|
+
if (value === '' || value === null || value === undefined) {
|
|
56
|
+
delete obj[key];
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
if (Array.isArray(value) && value.length === 0) {
|
|
60
|
+
delete obj[key];
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return obj;
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
mocks.appendChild(dataset);
|
|
68
|
+
|
|
69
|
+
await new Promise((resolve) => setTimeout(resolve, 30));
|
|
70
|
+
await dataset.write();
|
|
71
|
+
|
|
72
|
+
expect(ds.data[0]).deep.equal({email: 'a@b'});
|
|
73
|
+
|
|
74
|
+
const internal = dataset.getInternalUpdateCloneData();
|
|
75
|
+
expect(internal.data).deep.equal({name: '', email: 'a@b', tags: []});
|
|
76
|
+
expect(initial).deep.equal({name: '', email: 'a@b', tags: []});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('uses original data when sanitizer returns undefined', async function () {
|
|
80
|
+
const mocks = document.getElementById('mocks');
|
|
81
|
+
const ds = buildDatasource('ds-undefined', [{}]);
|
|
82
|
+
mocks.appendChild(ds);
|
|
83
|
+
|
|
84
|
+
const dataset = document.createElement('monster-dataset');
|
|
85
|
+
dataset.setOption('datasource.selector', '#ds-undefined');
|
|
86
|
+
dataset.setOption('mapping.data', '');
|
|
87
|
+
|
|
88
|
+
const initial = {name: '', email: 'a@b'};
|
|
89
|
+
dataset.setOption('data', initial);
|
|
90
|
+
|
|
91
|
+
dataset.setOption('writeBack.transformer', 'call:noop');
|
|
92
|
+
dataset.setOption('writeBack.callbacks', {
|
|
93
|
+
noop: () => undefined,
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
mocks.appendChild(dataset);
|
|
97
|
+
|
|
98
|
+
await new Promise((resolve) => setTimeout(resolve, 30));
|
|
99
|
+
await dataset.write();
|
|
100
|
+
|
|
101
|
+
expect(ds.data[0]).deep.equal({name: '', email: 'a@b'});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
});
|
|
@@ -92,7 +92,30 @@ describe('Tabs', function () {
|
|
|
92
92
|
|
|
93
93
|
});
|
|
94
94
|
|
|
95
|
+
it('should shorten label from content when no explicit label is provided', function (done) {
|
|
96
|
+
|
|
97
|
+
let mocks = document.getElementById('mocks');
|
|
98
|
+
mocks.innerHTML = html1;
|
|
99
|
+
|
|
100
|
+
setTimeout(() => {
|
|
101
|
+
try {
|
|
102
|
+
const tabs = document.getElementById('mytabs');
|
|
103
|
+
expect(tabs).is.instanceof(Tabs);
|
|
104
|
+
|
|
105
|
+
setTimeout(() => {
|
|
106
|
+
const buttons = tabs.shadowRoot.querySelectorAll('button[part=button]');
|
|
107
|
+
const labelSpan = buttons[2].querySelector('span[data-monster-replace]');
|
|
108
|
+
expect(labelSpan).to.not.equal(null);
|
|
109
|
+
expect(labelSpan.textContent.trim()).to.equal('Das ist tab…');
|
|
110
|
+
done();
|
|
111
|
+
}, 100);
|
|
112
|
+
} catch (e) {
|
|
113
|
+
return done(e);
|
|
114
|
+
}
|
|
115
|
+
}, 0);
|
|
116
|
+
});
|
|
117
|
+
|
|
95
118
|
});
|
|
96
119
|
|
|
97
120
|
|
|
98
|
-
});
|
|
121
|
+
});
|
package/test/cases/monster.mjs
CHANGED
package/test/web/test.html
CHANGED
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
</head>
|
|
10
10
|
<body>
|
|
11
11
|
<div id="headline" style="display: flex;align-items: center;justify-content: center;flex-direction: column;">
|
|
12
|
-
<h1 style='margin-bottom: 0.1em;'>Monster 4.
|
|
13
|
-
<div id="lastupdate" style='font-size:0.7em'>last update
|
|
12
|
+
<h1 style='margin-bottom: 0.1em;'>Monster 4.104.0</h1>
|
|
13
|
+
<div id="lastupdate" style='font-size:0.7em'>last update Mi 21. Jan 21:19:58 CET 2026</div>
|
|
14
14
|
</div>
|
|
15
15
|
<div id="mocha-errors"
|
|
16
16
|
style="color: red;font-weight: bold;display: flex;align-items: center;justify-content: center;flex-direction: column;margin:20px;"></div>
|