@schukai/monster 4.13.1 → 4.15.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,22 @@
2
2
 
3
3
 
4
4
 
5
+ ## [4.15.0] - 2025-06-05
6
+
7
+ ### Add Features
8
+
9
+ - Update node and pnpx binaries; enhance datatable column bar and select components
10
+
11
+
12
+
13
+ ## [4.14.0] - 2025-06-04
14
+
15
+ ### Add Features
16
+
17
+ - Add i18n support in CustomElement template formatting
18
+
19
+
20
+
5
21
  ## [4.13.1] - 2025-06-03
6
22
 
7
23
  ### Bug Fixes
package/package.json CHANGED
@@ -1 +1 @@
1
- {"author":"schukai GmbH","dependencies":{"@floating-ui/dom":"^1.7.1","@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.13.1"}
1
+ {"author":"schukai GmbH","dependencies":{"@floating-ui/dom":"^1.7.1","@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.15.0"}
@@ -23,6 +23,9 @@ import { clone } from "../../util/clone.mjs";
23
23
  import { ColumnBarStyleSheet } from "./stylesheet/column-bar.mjs";
24
24
  import { createPopper } from "@popperjs/core";
25
25
  import { getLocaleOfDocument } from "../../dom/locale.mjs";
26
+ import {hasObjectLink} from "../../dom/attributes.mjs";
27
+ import {customElementUpdaterLinkSymbol} from "../../dom/constants.mjs";
28
+ import {getGlobalObject} from "../../types/global.mjs";
26
29
 
27
30
  export { ColumnBar };
28
31
 
@@ -56,6 +59,12 @@ const dotsContainerElementSymbol = Symbol("dotsContainerElement");
56
59
  */
57
60
  const popperInstanceSymbol = Symbol("popperInstance");
58
61
 
62
+ /**
63
+ * @private
64
+ * @type {symbol}
65
+ */
66
+ const closeEventHandlerSymbol = Symbol("closeEventHandler");
67
+
59
68
  /**
60
69
  * A column bar for a datatable
61
70
  *
@@ -72,7 +81,18 @@ class ColumnBar extends CustomElement {
72
81
  * @return {symbol}
73
82
  */
74
83
  static get [instanceSymbol]() {
75
- return Symbol.for("@schukai/monster/components/column-bar");
84
+ return Symbol.for("@schukai/monster/components/column-bar@@instance");
85
+ }
86
+
87
+ /**
88
+ * This method is called to customize the component.
89
+ * @returns {Map<unknown, unknown>}
90
+ */
91
+ get customization() {
92
+ return new Map([
93
+ ...super.customization,
94
+ ["templateFormatter.i18n", true],
95
+ ]);
76
96
  }
77
97
 
78
98
  /**
@@ -83,24 +103,61 @@ class ColumnBar extends CustomElement {
83
103
  *
84
104
  * @property {Object} templates Template definitions
85
105
  * @property {string} templates.main Main template
86
- * @property {object} datasource The datasource
87
- * @property {boolean} autoLoad If true, the datasource is called immediately after the control is created.
106
+ * @property {object} labels Locale definitions
107
+ * @property {string} locale.settings The text for the settings button
88
108
  */
89
109
  get defaults() {
90
- const obj = Object.assign({}, super.defaults, {
110
+ return Object.assign({}, super.defaults, {
91
111
  templates: {
92
112
  main: getTemplate(),
93
113
  },
94
- locale: getTranslations(),
114
+ labels: getTranslations(),
95
115
 
96
116
  columns: [],
97
117
  });
118
+ }
119
+
120
+ /**
121
+ * Called every time the element is added to the DOM. Useful for running initialization code.
122
+ * @return {void}
123
+ * @since 4.14.0
124
+ */
125
+ connectedCallback() {
126
+ super.connectedCallback();
127
+
128
+ this[closeEventHandlerSymbol] = (event) => {
129
+ const path = event.composedPath();
130
+ const isOutsideElement = !path.includes(this);
131
+ const isOutsideShadow = !path.includes(this.shadowRoot);
132
+
133
+ if (isOutsideElement && isOutsideShadow && this[settingsLayerElementSymbol]) {
134
+ this[settingsLayerElementSymbol].classList.remove("visible");
135
+ }
136
+ }
137
+
138
+ getGlobalObject("document").addEventListener("click" , this[closeEventHandlerSymbol]);
139
+ getGlobalObject("document").addEventListener("touch" , this[closeEventHandlerSymbol]);
98
140
 
99
- return obj;
100
141
  }
101
142
 
102
143
  /**
144
+ * Called every time the element is removed from the DOM. Useful for running clean up code.
103
145
  *
146
+ * @return {void}
147
+ * @since 4.14.0
148
+ */
149
+ disconnectedCallback() {
150
+ super.disconnectedCallback();
151
+
152
+ if(this[closeEventHandlerSymbol]) {
153
+ getGlobalObject("document").removeEventListener("click", this[closeEventHandlerSymbol]);
154
+ getGlobalObject("document").removeEventListener("touch", this[closeEventHandlerSymbol]);
155
+ this[closeEventHandlerSymbol] = null;
156
+ }
157
+
158
+ }
159
+
160
+ /**
104
161
  * @return {string}
105
162
  */
106
163
  static getTag() {
@@ -133,43 +190,47 @@ class ColumnBar extends CustomElement {
133
190
  function getTranslations() {
134
191
  const locale = getLocaleOfDocument();
135
192
  switch (locale.language) {
136
- case "de":
137
- return {
138
- settings: "Einstellungen",
139
- };
140
- case "fr":
141
- return {
142
- settings: "Paramètres",
143
- };
144
- case "sp":
145
- return {
146
- settings: "Configuración",
147
- };
148
- case "it":
149
- return {
150
- settings: "Impostazioni",
151
- };
152
- case "pl":
153
- return {
154
- settings: "Ustawienia",
155
- };
156
- case "no":
157
- return {
158
- settings: "Innstillinger",
159
- };
160
- case "dk":
161
- return {
162
- settings: "Indstillinger",
163
- };
164
- case "sw":
165
- return {
166
- settings: "Inställningar",
167
- };
168
- default:
193
+ case "de": // German
194
+ return { settings: "Einstellungen" };
195
+ case "fr": // French
196
+ return { settings: "Paramètres" };
197
+ case "es": // Spanish
198
+ return { settings: "Configuración" };
199
+ case "zh": // Mandarin (Chinese)
200
+ return { settings: "设置" };
201
+ case "hi": // Hindi
202
+ return { settings: "सेटिंग्स" };
203
+ case "bn": // Bengali
204
+ return { settings: "সেটিংস" };
205
+ case "pt": // Portuguese
206
+ return { settings: "Configurações" };
207
+ case "ru": // Russian
208
+ return { settings: "Настройки" };
209
+ case "ja": // Japanese
210
+ return { settings: "設定" };
211
+ case "pa": // Western Punjabi
212
+ return { settings: "ਸੈਟਿੰਗਾਂ" };
213
+ case "mr": // Marathi
214
+ return { settings: "सेटिंग्ज" };
215
+ case "it": // Italian
216
+ return { settings: "Impostazioni" };
217
+ case "nl": // Dutch
218
+ return { settings: "Instellingen" };
219
+ case "sv": // Swedish
220
+ return { settings: "Inställningar" };
221
+ case "pl": // Polish
222
+ return { settings: "Ustawienia" };
223
+ case "da": // Danish
224
+ return { settings: "Indstillinger" };
225
+ case "fi": // Finnish
226
+ return { settings: "Asetukset" };
227
+ case "no": // Norwegian
228
+ return { settings: "Innstillinger" };
229
+ case "cs": // Czech
230
+ return { settings: "Nastavení" };
231
+ default: // English fallback
169
232
  case "en":
170
- return {
171
- settings: "Settings",
172
- };
233
+ return { settings: "Settings" };
173
234
  }
174
235
  }
175
236
 
@@ -325,7 +386,7 @@ function getTemplate() {
325
386
  <div data-monster-role="control" part="control" data-monster-select-this="true" data-monster-attributes="class path:columns | has-entries | ?::hidden">
326
387
  <ul data-monster-insert="dots path:columns"
327
388
  data-monster-role="dots"></ul>
328
- <a href="#" data-monster-role="settings-button" data-monster-replace="path:locale.settings">Settings</a>
389
+ <a href="#" data-monster-role="settings-button">i18n{settings}</a>
329
390
  <div data-monster-role="settings-layer">
330
391
  <div data-monster-insert="column path:columns" data-monster-role="settings-popup-list">
331
392
  </div>
@@ -390,6 +390,7 @@ class Select extends CustomControl {
390
390
  * @property {"radio"|"checkbox"} type Selection mode: "radio" for single, "checkbox" for multiple.
391
391
  * @property {string} name Name of the hidden form field for form submission.
392
392
  * @property {string|null} url URL to dynamically fetch options via HTTP when opening or filtering.
393
+ * @property {Object} lookup Configuration for lookup requests.
393
394
  * @property {string} lookup.url URL template with ${filter} placeholder to fetch only selected entries on init when `url` is set and either `features.lazyLoad` or `filter.mode==="remote"`.
394
395
  * @property {boolean} lookup.grouping Group lookup requests: true to fetch all selected values in one request, false to fetch each individually.
395
396
  * @property {string} fetch.redirect Fetch redirect mode (e.g. "error").
@@ -1354,51 +1355,64 @@ function getTranslations() {
1354
1355
  function lookupSelection() {
1355
1356
  const self = this;
1356
1357
 
1357
- setTimeout(() => {
1358
- const selection = self.getOption("selection");
1359
- if (selection.length === 0) {
1360
- return;
1361
- }
1358
+ const observer = new IntersectionObserver((entries, obs) => {
1359
+ for (const entry of entries) {
1360
+ if (entry.isIntersecting) {
1361
+ console.log("IntersectionObserver: entry.target is visible");
1362
+ obs.disconnect(); // Only observe once
1362
1363
 
1363
- if (self[isLoadingSymbol] === true) {
1364
- return;
1365
- }
1364
+ setTimeout(() => {
1365
+ const selection = self.getOption("selection");
1366
+ if (selection.length === 0) {
1367
+ return;
1368
+ }
1366
1369
 
1367
- if (self[lazyLoadDoneSymbol] === true) {
1368
- return;
1369
- }
1370
+ if (self[isLoadingSymbol] === true) {
1371
+ return;
1372
+ }
1370
1373
 
1371
- let url = self.getOption("url");
1372
- const lookupUrl = self.getOption("lookup.url");
1373
- if (lookupUrl !== null) {
1374
- url = lookupUrl;
1375
- }
1374
+ if (self[lazyLoadDoneSymbol] === true) {
1375
+ return;
1376
+ }
1376
1377
 
1377
- this[cleanupOptionsListSymbol] = false;
1378
+ let url = self.getOption("url");
1379
+ const lookupUrl = self.getOption("lookup.url");
1380
+ if (lookupUrl !== null) {
1381
+ url = lookupUrl;
1382
+ }
1378
1383
 
1379
- if (this.getOption("lookup.grouping") === true) {
1380
- filterFromRemoteByValue
1381
- .call(
1382
- self,
1383
- url,
1384
- selection.map((s) => s?.["value"]),
1385
- )
1386
- .catch((e) => {
1387
- addErrorAttribute(self, e);
1388
- });
1389
- return;
1390
- }
1384
+ self[cleanupOptionsListSymbol] = false;
1385
+
1386
+ if (self.getOption("lookup.grouping") === true) {
1387
+ filterFromRemoteByValue
1388
+ .call(
1389
+ self,
1390
+ url,
1391
+ selection.map((s) => s?.["value"]),
1392
+ )
1393
+ .catch((e) => {
1394
+ addErrorAttribute(self, e);
1395
+ });
1396
+ return;
1397
+ }
1391
1398
 
1392
- for (const s of selection) {
1393
- if (s?.["value"]) {
1394
- filterFromRemoteByValue.call(self, url, s?.["value"]).catch((e) => {
1395
- addErrorAttribute(self, e);
1396
- });
1399
+ for (const s of selection) {
1400
+ if (s?.["value"]) {
1401
+ filterFromRemoteByValue.call(self, url, s["value"]).catch((e) => {
1402
+ addErrorAttribute(self, e);
1403
+ });
1404
+ }
1405
+ }
1406
+ }, 100);
1397
1407
  }
1398
1408
  }
1399
- }, 100);
1409
+ }, { threshold: 0.1 });
1410
+
1411
+ // Beobachte das Element selbst (dieses Element muss im DOM sein)
1412
+ observer.observe(self);
1400
1413
  }
1401
1414
 
1415
+
1402
1416
  /**
1403
1417
  *
1404
1418
  * @param url
@@ -297,6 +297,7 @@ class CustomElement extends HTMLElement {
297
297
  * @property {Object} templateFormatter.marker Specifies the marker for the templates.
298
298
  * @property {Function} templateFormatter.marker.open=null Specifies the opening marker for the templates.
299
299
  * @property {Function} templateFormatter.marker.close=null Specifies the closing marker for the templates.
300
+ * @property {Boolean} templateFormatter.i18n=false Specifies whether the templates should be formatted with i18n.
300
301
  * @property {Boolean} eventProcessing=false Specifies whether the control processes events.
301
302
  * @since 1.8.0
302
303
  */
@@ -314,6 +315,7 @@ class CustomElement extends HTMLElement {
314
315
  open: null,
315
316
  close: null,
316
317
  },
318
+ i18n : false,
317
319
  },
318
320
 
319
321
  eventProcessing: false,
@@ -1075,6 +1077,27 @@ function parseOptionsJSON(data) {
1075
1077
  return validateObject(obj);
1076
1078
  }
1077
1079
 
1080
+ /**
1081
+ * @private
1082
+ * @param html
1083
+ * @returns {*|string}
1084
+ */
1085
+ function substituteI18n(html) {
1086
+
1087
+ if(!this.getOption("templateFormatter.i18n", false)) {
1088
+ return html;
1089
+ }
1090
+
1091
+ const labels = this.getOption("labels", {});
1092
+ if (!(isObject(labels) || isIterable(labels))) {
1093
+ return html;
1094
+ }
1095
+
1096
+ const formatter = new Formatter(labels, {});
1097
+ formatter.setMarker("i18n{", '}')
1098
+ return formatter.format(html);
1099
+ }
1100
+
1078
1101
  /**
1079
1102
  * @private
1080
1103
  * @return {initHtmlContent}
@@ -1090,7 +1113,7 @@ function initHtmlContent() {
1090
1113
  if (isObject(mapping)) {
1091
1114
  html = new Formatter(mapping, {}).format(html);
1092
1115
  }
1093
- this.innerHTML = html;
1116
+ this.innerHTML = substituteI18n.call(this, html);
1094
1117
  }
1095
1118
  }
1096
1119
 
@@ -1194,7 +1217,7 @@ function initShadowRoot() {
1194
1217
  html = formatter.format(html);
1195
1218
  }
1196
1219
 
1197
- this.shadowRoot.innerHTML = html;
1220
+ this.shadowRoot.innerHTML = substituteI18n.call(this, html);
1198
1221
  return this;
1199
1222
  }
1200
1223
 
@@ -38,13 +38,12 @@ describe('Button', function () {
38
38
  before(function (done) {
39
39
  initJSDOM().then(() => {
40
40
 
41
- import("element-internals-polyfill").catch(e => done(e));
42
-
43
- import("../../../../source/components/form/button.mjs").then((m) => {
44
- Button = m['Button'];
45
- done()
46
- }).catch(e => done(e))
47
-
41
+ import("element-internals-polyfill").then(()=>{
42
+ import("../../../../source/components/form/button.mjs").then((m) => {
43
+ Button = m['Button'];
44
+ done()
45
+ }).catch(e => done(e))
46
+ }).catch(e => done(e));
48
47
 
49
48
  });
50
49
  })
@@ -67,7 +67,7 @@ function initJSDOM(options) {
67
67
  'InputEvent',
68
68
  'KeyboardEvent',
69
69
  'MutationObserver',
70
- 'navigator',
70
+ // 'navigator',
71
71
  'Node',
72
72
  'NodeFilter',
73
73
  'NodeList',
@@ -76,7 +76,9 @@ function initJSDOM(options) {
76
76
  'XMLSerializer',
77
77
  ].forEach(key => {
78
78
  try {
79
- g[key] = window[key]
79
+ console.log("setting key", key);
80
+
81
+ g[key] = window[key]
80
82
  } catch(e) {
81
83
  console.error("Error setting key", key, e);
82
84
  }