@schukai/monster 4.127.1 → 4.128.1

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.
@@ -24,9 +24,13 @@ import {
24
24
  ATTRIBUTE_UPDATER_INSERT,
25
25
  ATTRIBUTE_UPDATER_INSERT_REFERENCE,
26
26
  ATTRIBUTE_UPDATER_PROPERTIES,
27
+ ATTRIBUTE_UPDATER_PATCH,
28
+ ATTRIBUTE_UPDATER_PATCH_KEY,
29
+ ATTRIBUTE_UPDATER_PATCH_RENDER,
27
30
  ATTRIBUTE_UPDATER_REMOVE,
28
31
  ATTRIBUTE_UPDATER_REPLACE,
29
32
  ATTRIBUTE_UPDATER_SELECT_THIS,
33
+ customElementUpdaterLinkSymbol,
30
34
  } from "./constants.mjs";
31
35
 
32
36
  import { Base } from "../types/base.mjs";
@@ -42,7 +46,13 @@ import { ProxyObserver } from "../types/proxyobserver.mjs";
42
46
  import { validateArray, validateInstance } from "../types/validate.mjs";
43
47
  import { clone } from "../util/clone.mjs";
44
48
  import { trimSpaces } from "../util/trimspaces.mjs";
45
- import { addAttributeToken, addToObjectLink } from "./attributes.mjs";
49
+ import {
50
+ addAttributeToken,
51
+ addToObjectLink,
52
+ getLinkedObjects,
53
+ hasObjectLink,
54
+ removeObjectLink,
55
+ } from "./attributes.mjs";
46
56
  import {
47
57
  CustomElement,
48
58
  updaterTransformerMethodsSymbol,
@@ -75,12 +85,16 @@ const processingSymbol = Symbol("processing");
75
85
  const processQueueSymbol = Symbol("processQueue");
76
86
  const applyChangeSymbol = Symbol("applyChange");
77
87
  const updaterRootSymbol = Symbol.for("@schukai/monster/dom/@@updater-root");
88
+ const disposedSymbol = Symbol("disposed");
89
+ const subjectObserverSymbol = Symbol("subjectObserver");
90
+ const patchNodeKeySymbol = Symbol("patchNodeKey");
78
91
 
79
92
  /**
80
93
  * The updater class connects an object with the DOM. In this way, structures and contents in the DOM can be
81
94
  * programmatically adapted via attributes.
82
95
  *
83
- * For example, to include a string from an object, the attribute `data-monster-replace` can be used.
96
+ * For example, to include a string from an object, the attribute `data-monster-replace`
97
+ * or the lifecycle-safe `data-monster-patch` can be used.
84
98
  * a further explanation can be found under [monsterjs.org](https://monsterjs.org/)
85
99
  *
86
100
  * Changes to attributes are made only when the direct values are changed. If you want to assign changes
@@ -140,19 +154,23 @@ class Updater extends Base {
140
154
 
141
155
  this[pendingDiffsSymbol] = [];
142
156
  this[processingSymbol] = false;
157
+ this[disposedSymbol] = false;
143
158
 
144
- this[internalSymbol].subject.attachObserver(
145
- new Observer(() => {
146
- const real = this[internalSymbol].subject.getRealSubject();
147
- const diffResult = diff(this[internalSymbol].last, real);
148
- this[internalSymbol].last = clone(real);
149
- if (diffResult.length === 0) {
150
- return Promise.resolve();
151
- }
152
- this[pendingDiffsSymbol].push(diffResult);
153
- return this[processQueueSymbol]();
154
- }),
155
- );
159
+ this[subjectObserverSymbol] = new Observer(() => {
160
+ if (this[disposedSymbol] === true) {
161
+ return Promise.resolve();
162
+ }
163
+
164
+ const real = this[internalSymbol].subject.getRealSubject();
165
+ const diffResult = diff(this[internalSymbol].last, real);
166
+ this[internalSymbol].last = clone(real);
167
+ if (diffResult.length === 0) {
168
+ return Promise.resolve();
169
+ }
170
+ this[pendingDiffsSymbol].push(diffResult);
171
+ return this[processQueueSymbol]();
172
+ });
173
+ this[internalSymbol].subject.attachObserver(this[subjectObserverSymbol]);
156
174
  }
157
175
 
158
176
  /**
@@ -160,6 +178,10 @@ class Updater extends Base {
160
178
  * @return {Promise}
161
179
  */
162
180
  async [processQueueSymbol]() {
181
+ if (this[disposedSymbol] === true) {
182
+ return Promise.resolve();
183
+ }
184
+
163
185
  if (this[processingSymbol]) {
164
186
  return Promise.resolve();
165
187
  }
@@ -167,6 +189,11 @@ class Updater extends Base {
167
189
 
168
190
  try {
169
191
  while (this[pendingDiffsSymbol].length) {
192
+ if (this[disposedSymbol] === true) {
193
+ this[pendingDiffsSymbol].length = 0;
194
+ return Promise.resolve();
195
+ }
196
+
170
197
  const diffResult = this[pendingDiffsSymbol].shift();
171
198
  if (this[internalSymbol].features.batchUpdates === true) {
172
199
  const updatePaths = new Map();
@@ -208,6 +235,10 @@ class Updater extends Base {
208
235
 
209
236
  /** @private **/
210
237
  async [applyChangeSymbol](change) {
238
+ if (this[disposedSymbol] === true) {
239
+ return Promise.resolve();
240
+ }
241
+
211
242
  removeElement.call(this, change);
212
243
  insertElement.call(this, change);
213
244
  updateContent.call(this, change);
@@ -255,6 +286,10 @@ class Updater extends Base {
255
286
  * @throws {Error} the bind argument must start as a value with a path
256
287
  */
257
288
  enableEventProcessing() {
289
+ if (this[disposedSymbol] === true) {
290
+ return this;
291
+ }
292
+
258
293
  this.disableEventProcessing();
259
294
 
260
295
  this[internalSymbol].element[updaterRootSymbol] = true;
@@ -293,6 +328,28 @@ class Updater extends Base {
293
328
  return this;
294
329
  }
295
330
 
331
+ dispose() {
332
+ if (this[disposedSymbol] === true) {
333
+ return this;
334
+ }
335
+
336
+ this[disposedSymbol] = true;
337
+ this.disableEventProcessing();
338
+ this[pendingDiffsSymbol].length = 0;
339
+
340
+ if (this[subjectObserverSymbol] instanceof Observer) {
341
+ this[internalSymbol].subject.detachObserver(this[subjectObserverSymbol]);
342
+ }
343
+
344
+ delete this[timerElementEventHandlerSymbol];
345
+
346
+ return this;
347
+ }
348
+
349
+ isDisposed() {
350
+ return this[disposedSymbol] === true;
351
+ }
352
+
296
353
  /**
297
354
  * The run method must be called for the update to start working.
298
355
  * The method ensures that changes are detected.
@@ -307,6 +364,10 @@ class Updater extends Base {
307
364
  * @return {Promise}
308
365
  */
309
366
  run() {
367
+ if (this[disposedSymbol] === true) {
368
+ return Promise.resolve();
369
+ }
370
+
310
371
  // the key __init__has no further meaning and is only
311
372
  // used to create the diff for empty objects.
312
373
  this[internalSymbol].last = { __init__: true };
@@ -320,6 +381,10 @@ class Updater extends Base {
320
381
  * @return {Monster.DOM.Updater}
321
382
  */
322
383
  retrieve() {
384
+ if (this[disposedSymbol] === true) {
385
+ return this;
386
+ }
387
+
323
388
  retrieveFromBindings.call(this);
324
389
  return this;
325
390
  }
@@ -399,6 +464,13 @@ function getControlEventHandler() {
399
464
  * @param {Event} event
400
465
  */
401
466
  this[symbol] = (event) => {
467
+ if (
468
+ this[disposedSymbol] === true ||
469
+ !this[internalSymbol].element?.isConnected
470
+ ) {
471
+ return;
472
+ }
473
+
402
474
  const root = findClosestUpdaterRootFromEvent(event);
403
475
  if (root !== this[internalSymbol].element) {
404
476
  return;
@@ -419,6 +491,14 @@ function getControlEventHandler() {
419
491
  }
420
492
 
421
493
  this[timerElementEventHandlerSymbol] = new DeadMansSwitch(50, () => {
494
+ if (
495
+ this[disposedSymbol] === true ||
496
+ !this[internalSymbol].element?.isConnected ||
497
+ !element?.isConnected
498
+ ) {
499
+ return;
500
+ }
501
+
422
502
  try {
423
503
  retrieveAndSetValue.call(this, element);
424
504
  } catch (e) {
@@ -662,6 +742,7 @@ function removeElement(change) {
662
742
  for (const [, element] of this[internalSymbol].element
663
743
  .querySelectorAll(`:scope [${ATTRIBUTE_UPDATER_REMOVE}]`)
664
744
  .entries()) {
745
+ teardownManagedSubtree(element);
665
746
  element.parentNode.removeChild(element);
666
747
  }
667
748
  }
@@ -785,6 +866,7 @@ function insertElement(change) {
785
866
  )
786
867
  ) {
787
868
  try {
869
+ teardownManagedSubtree(node);
788
870
  containerElement.removeChild(node);
789
871
  } catch (e) {
790
872
  addErrorAttribute(containerElement, e);
@@ -850,6 +932,30 @@ function applyRecursive(node, key, path) {
850
932
  );
851
933
  }
852
934
 
935
+ if (node.hasAttribute(ATTRIBUTE_UPDATER_PATCH)) {
936
+ const value = node.getAttribute(ATTRIBUTE_UPDATER_PATCH);
937
+ node.setAttribute(
938
+ ATTRIBUTE_UPDATER_PATCH,
939
+ value.replaceAll(`path:${key}`, `path:${path}`),
940
+ );
941
+ }
942
+
943
+ if (node.hasAttribute(ATTRIBUTE_UPDATER_PATCH_KEY)) {
944
+ const value = node.getAttribute(ATTRIBUTE_UPDATER_PATCH_KEY);
945
+ node.setAttribute(
946
+ ATTRIBUTE_UPDATER_PATCH_KEY,
947
+ value.replaceAll(`path:${key}`, `path:${path}`),
948
+ );
949
+ }
950
+
951
+ if (node.hasAttribute(ATTRIBUTE_UPDATER_PATCH_RENDER)) {
952
+ const value = node.getAttribute(ATTRIBUTE_UPDATER_PATCH_RENDER);
953
+ node.setAttribute(
954
+ ATTRIBUTE_UPDATER_PATCH_RENDER,
955
+ value.replaceAll(`path:${key}`, `path:${path}`),
956
+ );
957
+ }
958
+
853
959
  if (node.hasAttribute(ATTRIBUTE_UPDATER_ATTRIBUTES)) {
854
960
  const value = node.getAttribute(ATTRIBUTE_UPDATER_ATTRIBUTES);
855
961
  node.setAttribute(
@@ -902,12 +1008,14 @@ function updateContent(change) {
902
1008
 
903
1009
  const p = clone(change?.["path"]);
904
1010
  runUpdateContent.call(this, this[internalSymbol].element, p, subject);
1011
+ runUpdatePatch.call(this, this[internalSymbol].element, p, subject);
905
1012
 
906
1013
  const slots = this[internalSymbol].element.querySelectorAll("slot");
907
1014
  if (slots.length > 0) {
908
1015
  for (const [, slot] of Object.entries(slots)) {
909
1016
  for (const [, element] of Object.entries(slot.assignedNodes())) {
910
1017
  runUpdateContent.call(this, element, p, subject);
1018
+ runUpdatePatch.call(this, element, p, subject);
911
1019
  }
912
1020
  }
913
1021
  }
@@ -967,6 +1075,7 @@ function runUpdateContent(container, parts, subject) {
967
1075
  }
968
1076
 
969
1077
  if (value instanceof HTMLElement) {
1078
+ teardownChildNodes(element);
970
1079
  while (element.firstChild) {
971
1080
  element.removeChild(element.firstChild);
972
1081
  }
@@ -977,12 +1086,364 @@ function runUpdateContent(container, parts, subject) {
977
1086
  addErrorAttribute(element, e);
978
1087
  }
979
1088
  } else {
1089
+ teardownChildNodes(element);
980
1090
  element.innerHTML = value;
981
1091
  }
982
1092
  }
983
1093
  }
984
1094
  }
985
1095
 
1096
+ function runUpdatePatch(container, parts, subject) {
1097
+ if (!isArray(parts)) return;
1098
+ if (!(container instanceof HTMLElement)) return;
1099
+ parts = clone(parts);
1100
+
1101
+ const mem = new WeakSet();
1102
+
1103
+ while (parts.length > 0) {
1104
+ const current = parts.join(".");
1105
+ parts.pop();
1106
+
1107
+ const query = `[${ATTRIBUTE_UPDATER_PATCH}^="path:${current}"], [${ATTRIBUTE_UPDATER_PATCH}^="static:"], [${ATTRIBUTE_UPDATER_PATCH}^="i18n:"]`;
1108
+ const e = container.querySelectorAll(`${query}`);
1109
+
1110
+ const iterator = new Set([...e]);
1111
+ if (container.matches(query)) {
1112
+ iterator.add(container);
1113
+ }
1114
+
1115
+ for (const [element] of iterator.entries()) {
1116
+ if (mem.has(element)) return;
1117
+ mem.add(element);
1118
+
1119
+ const attributes = element.getAttribute(ATTRIBUTE_UPDATER_PATCH);
1120
+ const cmd = trimSpaces(attributes);
1121
+
1122
+ const pipe = new Pipe(cmd);
1123
+ this[internalSymbol].callbacks.forEach((f, n) => {
1124
+ pipe.setCallback(n, f);
1125
+ });
1126
+
1127
+ let value;
1128
+ try {
1129
+ element.removeAttribute(ATTRIBUTE_ERRORMESSAGE);
1130
+ value = pipe.run(subject);
1131
+ } catch (e) {
1132
+ element.setAttribute(ATTRIBUTE_ERRORMESSAGE, e.message);
1133
+ continue;
1134
+ }
1135
+
1136
+ applyPatchValue.call(this, element, value);
1137
+ }
1138
+ }
1139
+ }
1140
+
1141
+ function applyPatchValue(element, value) {
1142
+ if (!(element instanceof HTMLElement)) {
1143
+ return;
1144
+ }
1145
+
1146
+ if (isArray(value)) {
1147
+ const keyDefinition = element.getAttribute(ATTRIBUTE_UPDATER_PATCH_KEY);
1148
+ if (isString(keyDefinition) && trimSpaces(keyDefinition) !== "") {
1149
+ replaceKeyedPatchedChildren.call(
1150
+ this,
1151
+ element,
1152
+ value,
1153
+ keyDefinition,
1154
+ element.getAttribute(ATTRIBUTE_UPDATER_PATCH_RENDER),
1155
+ );
1156
+ return;
1157
+ }
1158
+
1159
+ replacePatchedChildren(element, value);
1160
+ return;
1161
+ }
1162
+
1163
+ if (isInstance(value, DocumentFragment)) {
1164
+ replacePatchedChildren(element, value);
1165
+ return;
1166
+ }
1167
+
1168
+ if (value instanceof HTMLElement) {
1169
+ const existingChildren = Array.from(element.children);
1170
+ if (existingChildren.length === 1 && existingChildren[0] === value) {
1171
+ return;
1172
+ }
1173
+
1174
+ teardownChildNodes(element);
1175
+ while (element.firstChild) {
1176
+ element.removeChild(element.firstChild);
1177
+ }
1178
+
1179
+ try {
1180
+ element.appendChild(value);
1181
+ } catch (e) {
1182
+ addErrorAttribute(element, e);
1183
+ }
1184
+ return;
1185
+ }
1186
+
1187
+ const nextValue = value === null || value === undefined ? "" : String(value);
1188
+
1189
+ if (element.children.length > 0) {
1190
+ teardownChildNodes(element);
1191
+ }
1192
+
1193
+ element.textContent = nextValue;
1194
+ }
1195
+
1196
+ function replaceKeyedPatchedChildren(
1197
+ element,
1198
+ values,
1199
+ keyDefinition,
1200
+ renderDefinition,
1201
+ ) {
1202
+ if (!(element instanceof HTMLElement) || !isArray(values)) {
1203
+ return;
1204
+ }
1205
+
1206
+ const keyPipe = new Pipe(trimSpaces(keyDefinition));
1207
+ this[internalSymbol].callbacks.forEach((f, n) => {
1208
+ keyPipe.setCallback(n, f);
1209
+ });
1210
+ const renderPipe = createPatchRenderPipe.call(this, renderDefinition);
1211
+
1212
+ const existing = new Map();
1213
+ for (const node of Array.from(element.childNodes)) {
1214
+ const key = node?.[patchNodeKeySymbol];
1215
+ if (key !== undefined && !existing.has(key)) {
1216
+ existing.set(key, node);
1217
+ }
1218
+ }
1219
+
1220
+ const nextNodes = [];
1221
+ for (const value of values) {
1222
+ let key;
1223
+ try {
1224
+ key = keyPipe.run(value);
1225
+ } catch (e) {
1226
+ addErrorAttribute(element, e);
1227
+ return;
1228
+ }
1229
+ let renderedValue;
1230
+ try {
1231
+ renderedValue = resolvePatchRenderValue(value, renderPipe);
1232
+ } catch (e) {
1233
+ addErrorAttribute(element, e);
1234
+ return;
1235
+ }
1236
+
1237
+ const normalizedKey = key === null || key === undefined ? "" : String(key);
1238
+ if (existing.has(normalizedKey)) {
1239
+ const reused = existing.get(normalizedKey);
1240
+ existing.delete(normalizedKey);
1241
+ const nextNode = preparePatchedNode(reused, renderedValue);
1242
+ nextNode[patchNodeKeySymbol] = normalizedKey;
1243
+ if (nextNode !== reused && reused.parentNode === element) {
1244
+ element.replaceChild(nextNode, reused);
1245
+ }
1246
+ nextNodes.push(nextNode);
1247
+ continue;
1248
+ }
1249
+
1250
+ let created;
1251
+ try {
1252
+ created = createSinglePatchedNode(renderedValue);
1253
+ } catch (e) {
1254
+ addErrorAttribute(element, e);
1255
+ return;
1256
+ }
1257
+ created[patchNodeKeySymbol] = normalizedKey;
1258
+ nextNodes.push(created);
1259
+ }
1260
+
1261
+ for (const node of existing.values()) {
1262
+ if (node instanceof HTMLElement) {
1263
+ teardownManagedSubtree(node);
1264
+ }
1265
+ if (node.parentNode === element) {
1266
+ element.removeChild(node);
1267
+ }
1268
+ }
1269
+
1270
+ for (const node of nextNodes) {
1271
+ if (node.parentNode !== element) {
1272
+ element.appendChild(node);
1273
+ continue;
1274
+ }
1275
+
1276
+ if (element.lastChild !== node) {
1277
+ element.appendChild(node);
1278
+ }
1279
+ }
1280
+ }
1281
+
1282
+ function preparePatchedNode(node, value) {
1283
+ if (node?.nodeType === Node.TEXT_NODE) {
1284
+ if (
1285
+ value === null ||
1286
+ value === undefined ||
1287
+ isString(value) ||
1288
+ typeof value === "number" ||
1289
+ typeof value === "boolean"
1290
+ ) {
1291
+ node.textContent =
1292
+ value === null || value === undefined ? "" : String(value);
1293
+ return node;
1294
+ }
1295
+
1296
+ return createSinglePatchedNode(value);
1297
+ }
1298
+
1299
+ if (node instanceof HTMLElement) {
1300
+ if (value === node) {
1301
+ return node;
1302
+ }
1303
+
1304
+ return createSinglePatchedNode(value);
1305
+ }
1306
+
1307
+ return node;
1308
+ }
1309
+
1310
+ function createPatchRenderPipe(renderDefinition) {
1311
+ if (!isString(renderDefinition) || trimSpaces(renderDefinition) === "") {
1312
+ return null;
1313
+ }
1314
+
1315
+ const renderPipe = new Pipe(trimSpaces(renderDefinition));
1316
+ this[internalSymbol].callbacks.forEach((f, n) => {
1317
+ renderPipe.setCallback(n, f);
1318
+ });
1319
+
1320
+ return renderPipe;
1321
+ }
1322
+
1323
+ function resolvePatchRenderValue(value, renderPipe) {
1324
+ if (!(renderPipe instanceof Pipe)) {
1325
+ return value;
1326
+ }
1327
+
1328
+ return renderPipe.run(value);
1329
+ }
1330
+
1331
+ function replacePatchedChildren(element, value) {
1332
+ if (!(element instanceof HTMLElement)) {
1333
+ return;
1334
+ }
1335
+
1336
+ const fragment = document.createDocumentFragment();
1337
+
1338
+ if (isArray(value)) {
1339
+ for (const entry of value) {
1340
+ appendPatchChild(fragment, entry);
1341
+ }
1342
+ } else if (isInstance(value, DocumentFragment)) {
1343
+ fragment.appendChild(value);
1344
+ }
1345
+
1346
+ teardownChildNodes(element);
1347
+ while (element.firstChild) {
1348
+ element.removeChild(element.firstChild);
1349
+ }
1350
+
1351
+ element.appendChild(fragment);
1352
+ }
1353
+
1354
+ function appendPatchChild(fragment, value) {
1355
+ if (!(fragment instanceof DocumentFragment)) {
1356
+ return;
1357
+ }
1358
+
1359
+ if (value === null || value === undefined) {
1360
+ return;
1361
+ }
1362
+
1363
+ if (isArray(value)) {
1364
+ for (const entry of value) {
1365
+ appendPatchChild(fragment, entry);
1366
+ }
1367
+ return;
1368
+ }
1369
+
1370
+ if (isInstance(value, DocumentFragment) || value instanceof HTMLElement) {
1371
+ fragment.appendChild(value);
1372
+ return;
1373
+ }
1374
+
1375
+ fragment.appendChild(document.createTextNode(String(value)));
1376
+ }
1377
+
1378
+ function createPatchedNodes(value) {
1379
+ const fragment = document.createDocumentFragment();
1380
+ appendPatchChild(fragment, value);
1381
+ return Array.from(fragment.childNodes);
1382
+ }
1383
+
1384
+ function createSinglePatchedNode(value) {
1385
+ const nodes = createPatchedNodes(value);
1386
+ if (nodes.length === 0) {
1387
+ return document.createTextNode("");
1388
+ }
1389
+
1390
+ if (nodes.length !== 1) {
1391
+ throw new Error("keyed patch values must resolve to a single node");
1392
+ }
1393
+
1394
+ return nodes[0];
1395
+ }
1396
+
1397
+ function teardownChildNodes(root) {
1398
+ if (!(root instanceof HTMLElement)) {
1399
+ return;
1400
+ }
1401
+
1402
+ for (const child of Array.from(root.children)) {
1403
+ teardownManagedSubtree(child);
1404
+ }
1405
+ }
1406
+
1407
+ function teardownManagedSubtree(root) {
1408
+ if (!(root instanceof HTMLElement)) {
1409
+ return;
1410
+ }
1411
+
1412
+ const elements = [root, ...root.querySelectorAll("*")];
1413
+ for (const element of elements) {
1414
+ if (!hasObjectLinkSafe(element, customElementUpdaterLinkSymbol)) {
1415
+ continue;
1416
+ }
1417
+
1418
+ try {
1419
+ const linked = getLinkedObjects(element, customElementUpdaterLinkSymbol);
1420
+ for (const updaterSet of linked) {
1421
+ if (!(updaterSet instanceof Set)) {
1422
+ continue;
1423
+ }
1424
+
1425
+ for (const updater of updaterSet) {
1426
+ if (typeof updater?.dispose === "function") {
1427
+ updater.dispose();
1428
+ }
1429
+ }
1430
+ }
1431
+
1432
+ removeObjectLink(element, customElementUpdaterLinkSymbol);
1433
+ } catch (e) {
1434
+ addErrorAttribute(element, e);
1435
+ }
1436
+ }
1437
+ }
1438
+
1439
+ function hasObjectLinkSafe(element, symbol) {
1440
+ try {
1441
+ return hasObjectLink(element, symbol);
1442
+ } catch (e) {
1443
+ return false;
1444
+ }
1445
+ }
1446
+
986
1447
  /**
987
1448
  * @private
988
1449
  * @since 1.8.0