@schukai/monster 4.137.1 → 4.137.3

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/README.md CHANGED
@@ -91,7 +91,7 @@ be closed immediately.
91
91
 
92
92
  ## License
93
93
 
94
- Copyright © 2024 Volker Schukai
94
+ Copyright © 2026 Volker Schukai
95
95
 
96
96
  Licensed under [AGPL](https://www.gnu.org/licenses/agpl-3.0.de.html). Commercial licenses are also available.
97
97
 
package/package.json CHANGED
@@ -1 +1 @@
1
- {"author":"Volker Schukai","dependencies":{"@floating-ui/dom":"^1.7.6"},"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.137.1"}
1
+ {"author":"Volker Schukai","dependencies":{"@floating-ui/dom":"^1.7.6"},"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.137.3"}
@@ -580,6 +580,7 @@ function initPopperSwitch() {
580
580
  }
581
581
 
582
582
  this[switchElementSymbol] = switchButton;
583
+ updatePopperSwitchVisibility.call(this);
583
584
  }
584
585
 
585
586
  /**
@@ -1180,25 +1181,36 @@ function adjustButtonVisibility() {
1180
1181
  const self = this;
1181
1182
 
1182
1183
  return new Promise((resolve) => {
1183
- const observer = new MutationObserver(function (mutations) {
1184
+ let resolved = false;
1185
+ let observer;
1186
+ const runIfRendered = () => {
1187
+ if (resolved === true) return;
1188
+
1184
1189
  const defCount = self.getOption("buttons.standard").length;
1185
1190
  const domCount = self[navElementSymbol].querySelectorAll(
1186
- 'button[data-monster-role="button"]',
1191
+ 'button[data-monster-role="button"][data-monster-tab-reference]',
1187
1192
  ).length;
1188
1193
 
1189
- // in drawing
1190
1194
  if (defCount !== domCount) return;
1191
1195
 
1192
- observer.disconnect();
1193
-
1196
+ resolved = true;
1197
+ observer?.disconnect();
1194
1198
  checkAndRearrangeButtons.call(self);
1195
-
1196
1199
  resolve();
1200
+ };
1201
+
1202
+ observer = new MutationObserver(function () {
1203
+ runIfRendered();
1197
1204
  });
1198
1205
 
1199
1206
  observer.observe(self[navElementSymbol], {
1200
1207
  attributes: true,
1208
+ childList: true,
1209
+ subtree: true,
1201
1210
  });
1211
+
1212
+ runIfRendered();
1213
+ getWindow().requestAnimationFrame(runIfRendered);
1202
1214
  });
1203
1215
  }
1204
1216
 
@@ -1273,16 +1285,25 @@ function rearrangeButtons() {
1273
1285
 
1274
1286
  setButtonCollections.call(this, standardButtons, popperButtons);
1275
1287
 
1276
- if (this[switchElementSymbol]) {
1277
- if (popperButtons.length > 0) {
1278
- this[switchElementSymbol].classList.remove("hidden");
1279
- } else {
1280
- this[switchElementSymbol].classList.add("hidden");
1281
- }
1282
- }
1288
+ updatePopperSwitchVisibility.call(this);
1283
1289
  });
1284
1290
  }
1285
1291
 
1292
+ /**
1293
+ * @private
1294
+ */
1295
+ function updatePopperSwitchVisibility() {
1296
+ if (!this[switchElementSymbol]) {
1297
+ return;
1298
+ }
1299
+
1300
+ if (this.getOption("buttons.popper").length > 0) {
1301
+ this[switchElementSymbol].classList.remove("hidden");
1302
+ } else {
1303
+ this[switchElementSymbol].classList.add("hidden");
1304
+ }
1305
+ }
1306
+
1286
1307
  /**
1287
1308
  * @private
1288
1309
  * @param {string} ref
@@ -15,10 +15,8 @@
15
15
  import { internalStateSymbol } from "../../constants.mjs";
16
16
  import { extend } from "../../data/extend.mjs";
17
17
  import { getGlobalFunction } from "../../types/global.mjs";
18
- import { addAttributeToken } from "../attributes.mjs";
19
18
  import {
20
19
  ATTRIBUTE_CLASS,
21
- ATTRIBUTE_ERRORMESSAGE,
22
20
  ATTRIBUTE_ID,
23
21
  ATTRIBUTE_SRC,
24
22
  ATTRIBUTE_TITLE,
@@ -35,6 +33,9 @@ import { instanceSymbol } from "../../constants.mjs";
35
33
 
36
34
  export { Data };
37
35
 
36
+ const KEY_ALLOWED_ORIGINS = "allowedOrigins";
37
+ const JSON_SCRIPT_TYPE_PATTERN = /^(application\/json|text\/json|application\/[a-z0-9.+-]+\+json)$/i;
38
+
38
39
  /**
39
40
  * This class is used by the resource manager to embed data.
40
41
  *
@@ -54,6 +55,7 @@ class Data extends Resource {
54
55
  mode: "cors",
55
56
  credentials: "same-origin",
56
57
  type: "application/json",
58
+ [KEY_ALLOWED_ORIGINS]: ["self"],
57
59
  });
58
60
  }
59
61
 
@@ -73,7 +75,6 @@ class Data extends Resource {
73
75
  * @return {Monster.DOM.Resource}
74
76
  */
75
77
  connect() {
76
- const self = this;
77
78
  if (!(this[referenceSymbol] instanceof HTMLElement)) {
78
79
  this.create();
79
80
  }
@@ -105,6 +106,12 @@ class Data extends Resource {
105
106
  */
106
107
  function createElement() {
107
108
  const document = this.getOption(KEY_DOCUMENT);
109
+ const type = this.getOption(ATTRIBUTE_TYPE);
110
+
111
+ if (!isInertJSONType(type)) {
112
+ throw new Error("unsupported data resource type");
113
+ }
114
+
108
115
  this[referenceSymbol] = document.createElement(TAG_SCRIPT);
109
116
 
110
117
  for (const key of [
@@ -127,6 +134,8 @@ function createElement() {
127
134
  * throws {Error} target not found
128
135
  */
129
136
  function appendToDocument() {
137
+ const document = this.getOption(KEY_DOCUMENT);
138
+ const url = getValidatedURL.call(this, document);
130
139
  const targetNode = document.querySelector(this.getOption(KEY_QUERY, "head"));
131
140
  if (!(targetNode instanceof HTMLElement)) {
132
141
  throw new Error("target not found");
@@ -134,7 +143,7 @@ function appendToDocument() {
134
143
 
135
144
  targetNode.appendChild(this[referenceSymbol]);
136
145
 
137
- getGlobalFunction("fetch")(this.getOption(ATTRIBUTE_SRC), {
146
+ getGlobalFunction("fetch")(url.toString(), {
138
147
  method: "GET", // *GET, POST, PUT, DELETE, etc.
139
148
  mode: this.getOption("mode", "cors"), // no-cors, *cors, same-origin
140
149
  cache: "no-cache", // *default, no-cache, reload, force-cache, only-if-cached
@@ -159,9 +168,48 @@ function appendToDocument() {
159
168
  loaded: true,
160
169
  error: e.toString(),
161
170
  });
162
-
163
- targetNode.setAttribute(ATTRIBUTE_ERRORMESSAGE, e.toString());
164
171
  });
165
172
 
166
173
  return this;
167
174
  }
175
+
176
+ /**
177
+ * @private
178
+ * @param {string} type
179
+ * @return {boolean}
180
+ */
181
+ function isInertJSONType(type) {
182
+ if (type === undefined) {
183
+ return false;
184
+ }
185
+
186
+ return JSON_SCRIPT_TYPE_PATTERN.test(String(type).split(";")[0].trim());
187
+ }
188
+
189
+ /**
190
+ * @private
191
+ * @param {Document} document
192
+ * @return {URL}
193
+ */
194
+ function getValidatedURL(document) {
195
+ const url = new URL(this.getOption(ATTRIBUTE_SRC), document.baseURI);
196
+
197
+ if (!["http:", "https:"].includes(url.protocol)) {
198
+ throw new Error("unsupported data resource protocol");
199
+ }
200
+
201
+ const allowedOriginsOption = this.getOption(KEY_ALLOWED_ORIGINS, ["self"]);
202
+ const allowedOrigins = Array.isArray(allowedOriginsOption)
203
+ ? allowedOriginsOption
204
+ : [allowedOriginsOption];
205
+ const documentOrigin = new URL(document.baseURI).origin;
206
+ const allowed = allowedOrigins.map((origin) =>
207
+ origin === "self" ? documentOrigin : new URL(origin, document.baseURI).origin,
208
+ );
209
+
210
+ if (!allowed.includes(url.origin)) {
211
+ throw new Error("data resource origin not allowed");
212
+ }
213
+
214
+ return url;
215
+ }
@@ -63,7 +63,7 @@ describe('Data', function () {
63
63
  it('setEventTypes()', function (done) {
64
64
 
65
65
  const data = new Data({
66
- src: new DataUrl('', 'text/javascript').toString()
66
+ src: '/data.json'
67
67
  });
68
68
 
69
69
  data.connect().available().then(() => {
@@ -71,12 +71,77 @@ describe('Data', function () {
71
71
  }).catch(e => done(e));
72
72
 
73
73
  })
74
+
75
+ it('rejects executable script types', function () {
76
+
77
+ const data = new Data({
78
+ src: new DataUrl('console.log(1);', 'text/javascript').toString(),
79
+ type: 'text/javascript'
80
+ });
81
+
82
+ expect(() => data.connect()).to.throw('unsupported data resource type');
83
+ expect(document.querySelector('script')).not.to.exist;
84
+
85
+ })
86
+
87
+ it('rejects remote origins by default', function () {
88
+
89
+ const data = new Data({
90
+ src: 'https://cdn.example/data.json'
91
+ });
92
+
93
+ expect(() => data.connect()).to.throw('data resource origin not allowed');
94
+ expect(document.querySelector('script')).not.to.exist;
95
+
96
+ })
97
+
98
+ it('allows configured remote origins', function (done) {
99
+
100
+ const data = new Data({
101
+ src: 'https://cdn.example/data.json',
102
+ allowedOrigins: ['https://cdn.example']
103
+ });
104
+
105
+ data.connect().available().then(() => {
106
+ expect(data.isConnected()).to.be.true;
107
+
108
+ done();
109
+ }).catch(e => done(e));
110
+
111
+ })
112
+
113
+ it('does not expose fetch errors in DOM attributes', function (done) {
114
+
115
+ globalThis['fetch'] = function () {
116
+ return Promise.reject(new Error('token=secret'));
117
+ };
118
+
119
+ const data = new Data({
120
+ src: '/data.json',
121
+ id: 'data-error'
122
+ });
123
+
124
+ data.connect();
125
+
126
+ setTimeout(() => {
127
+ try {
128
+ const script = document.getElementById('data-error');
129
+
130
+ expect(script.hasAttribute('data-monster-error')).to.be.false;
131
+
132
+ done();
133
+ } catch (e) {
134
+ done(e);
135
+ }
136
+ }, 0);
137
+
138
+ })
74
139
  });
75
140
 
76
141
  describe('External Data', () => {
77
142
 
78
143
  let id = new ID('data').toString();
79
- let server, data, url = 'https://cdnjs.cloudflare.com/ajax/libs/layzr.js/2.2.2/layzr.min.js';
144
+ let server, data, url = '/layzr.json';
80
145
 
81
146
  beforeEach(() => {
82
147
 
@@ -126,4 +191,4 @@ describe('Data', function () {
126
191
  });
127
192
 
128
193
 
129
- });
194
+ });
@@ -104,7 +104,7 @@ describe('ResourceManager', function () {
104
104
 
105
105
  describe('check availability example.json', function () {
106
106
  it('add data and check content', function (done) {
107
- manager.addData('https://example.com/example.json').connect().available().then(r => {
107
+ manager.addData('/example.json').connect().available().then(r => {
108
108
  expect(document.querySelector('html').outerHTML).contains('>{"a":"test"}</script></head>');
109
109
  done();
110
110
  }).catch(e => done(e));
@@ -115,4 +115,4 @@ describe('ResourceManager', function () {
115
115
  });
116
116
 
117
117
 
118
- });
118
+ });