@cocreate/elements 1.46.0 → 1.48.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.
@@ -3,8 +3,39 @@ import {
3
3
  checkValue
4
4
  } from "@cocreate/utils";
5
5
 
6
- import { elements, forms, keys } from "./mapRegistry.js";
7
- import { subscribe } from "./subscribeHandler.js";
6
+ import {
7
+ elements,
8
+ forms,
9
+ keys,
10
+ elementSubscriptions,
11
+ subscriptions
12
+ } from "./mapRegistry.js";
13
+ import { getSubscription } from "./subscribeHandler.js";
14
+ import { read, save } from "./crudHandler.js";
15
+
16
+ const environment = localStorage.getItem("environment");
17
+
18
+ function registerFormElement(form, element, data) {
19
+ if (!form || !data?.type) return;
20
+
21
+ if (!form.save) form.save = () => save(form);
22
+ if (!form.getData) form.getData = () => getData(form);
23
+
24
+ let formObject = forms.get(form);
25
+ if (!formObject) {
26
+ formObject = {
27
+ elements: new Map(),
28
+ types: new Map()
29
+ };
30
+ forms.set(form, formObject);
31
+ }
32
+
33
+ formObject.elements.set(element, data);
34
+ if (!formObject.types.has(data.type)) {
35
+ formObject.types.set(data.type, new Map());
36
+ }
37
+ formObject.types.get(data.type).set(element, data);
38
+ }
8
39
 
9
40
  /**
10
41
  * Parses and normalizes HTML attributes from an element into a canonical CRUD data object.
@@ -46,6 +77,13 @@ export function getDataObject(element) {
46
77
  else if (data.storage || data.storage === "") data.type = "storage";
47
78
  else if (data.data) data.type = "data";
48
79
 
80
+ if (
81
+ environment &&
82
+ !data.database &&
83
+ ["array", "index", "object"].includes(data.type)
84
+ )
85
+ data.database = environment;
86
+
49
87
  delete data.isRealtime;
50
88
  return data;
51
89
  }
@@ -59,7 +97,10 @@ export function getDataObject(element) {
59
97
  * @returns {{ string: string, object: Object }}
60
98
  */
61
99
  export function initDataKey(element, data) {
100
+ if (!data.database && environment) data.database = environment;
101
+
62
102
  const dataKey = getDataKey(data);
103
+ const subscription = getSubscription(data);
63
104
 
64
105
  if (keys.has(dataKey.string)) {
65
106
  keys.get(dataKey.string).elements.set(element, "");
@@ -69,34 +110,27 @@ export function initDataKey(element, data) {
69
110
  data,
70
111
  dataKey
71
112
  });
72
- subscribe(dataKey, data);
73
113
  }
74
114
 
75
- elements.set(element, dataKey.string);
76
-
77
- if (element.parentElement) {
78
- const form = element.closest("form");
79
- if (form) {
80
- if (!form.save) form.save = () => save(form);
81
- if (!form.getData) form.getData = () => getData(form);
82
-
83
- let formObject = forms.get(form);
84
- if (formObject) {
85
- formObject.elements.set(element, data);
86
- if (formObject.types.has(data.type)) {
87
- formObject.types.get(data.type).set(element, data);
88
- } else {
89
- formObject.types.set(data.type, new Map([[element, data]]));
90
- }
91
- } else {
92
- forms.set(form, {
93
- elements: new Map([[element, data]]),
94
- types: new Map([[data.type, new Map([[element, data]])]])
95
- });
96
- }
115
+ if (subscription) {
116
+ let subscriptionEntry = subscriptions.get(subscription.string);
117
+ if (subscriptionEntry) {
118
+ subscriptionEntry.elements.add(element);
119
+ } else {
120
+ subscriptionEntry = {
121
+ elements: new Set([element]),
122
+ subscription
123
+ };
124
+ subscriptions.set(subscription.string, subscriptionEntry);
97
125
  }
126
+ elementSubscriptions.set(element, subscription.string);
98
127
  }
99
128
 
129
+ elements.set(element, dataKey.string);
130
+
131
+ const form = element.form || element.closest?.("form");
132
+ registerFormElement(form, element, data);
133
+
100
134
  if (!element.read) element.read = () => read(element);
101
135
  if (!element.save) element.save = () => save(element);
102
136
 
@@ -113,6 +147,8 @@ export function initDataKey(element, data) {
113
147
  */
114
148
  export function getDataKey(data) {
115
149
  const dataKey = {};
150
+ if (!data.database && environment) data.database = environment;
151
+
116
152
  const attributes = [
117
153
  "host",
118
154
  "organization_id",
@@ -126,13 +162,13 @@ export function getDataKey(data) {
126
162
  "object",
127
163
  "key",
128
164
  "updateKey",
129
- "$filter",
165
+ "filter",
130
166
  "upsert",
131
167
  "namespace",
132
168
  "room",
133
169
  "broadcast",
134
170
  "broadcastSender",
135
- "broadcastBrowser"
171
+ "broadcastClient"
136
172
  ];
137
173
 
138
174
  for (const attribute of attributes) {
@@ -193,7 +229,7 @@ export function getDataKeys(data) {
193
229
  "array",
194
230
  "index",
195
231
  "object",
196
- "$filter"
232
+ "filter"
197
233
  ];
198
234
 
199
235
  for (const [keyString, sortedKey] of keys.entries()) {
@@ -201,8 +237,8 @@ export function getDataKeys(data) {
201
237
 
202
238
  for (const key of targetKeys) {
203
239
  if (
204
- !data.$filter &&
205
- key === "$filter" &&
240
+ !data.filter &&
241
+ key === "filter" &&
206
242
  sortedKey.data.hasOwnProperty(key)
207
243
  ) {
208
244
  hasMatch = true;
@@ -211,25 +247,28 @@ export function getDataKeys(data) {
211
247
  hasMatch = false;
212
248
  break;
213
249
  }
250
+
251
+ // Filtered object elements watch a collection query, not one
252
+ // specific object. Creates and updates contain the affected
253
+ // object(s), so their object shape will not match the empty
254
+ // object scope used when the filtered element was registered.
255
+ if (key === "object" && sortedKey.data.filter) continue;
256
+
214
257
  if (
215
258
  Array.isArray(sortedKey.data[key]) &&
216
259
  Array.isArray(data[key])
217
260
  ) {
218
- if (key === "object" && sortedKey.data.$filter) {
219
- hasMatch = true;
220
- } else {
221
- const matches = sortedKey.data[key].some((value) => {
222
- if (key === "object") {
223
- return data[key].some((obj) => obj._id === value._id);
224
- } else {
225
- return data[key].includes(value);
226
- }
227
- });
228
-
229
- if (!matches) {
230
- hasMatch = false;
231
- break;
261
+ const matches = sortedKey.data[key].some((value) => {
262
+ if (key === "object") {
263
+ return data[key].some((obj) => obj._id === value._id);
264
+ } else {
265
+ return data[key].includes(value);
232
266
  }
267
+ });
268
+
269
+ if (!matches) {
270
+ hasMatch = false;
271
+ break;
233
272
  }
234
273
  } else if (sortedKey.data[key] !== data[key]) {
235
274
  hasMatch = false;
@@ -264,7 +303,7 @@ function formatPayload(payload, value, element) {
264
303
  } else payload.object = { ...payload.object, ...value };
265
304
  } else payload.object[payload.key] = value;
266
305
  }
267
- if (payload.isUpsert && payload.$filter)
306
+ if (payload.isUpsert && payload.filter)
268
307
  payload.method = "object.update";
269
308
  } else payload[payload.type] = value;
270
309
  } else if (payload[payload.type] && payload.key) {
@@ -352,19 +391,27 @@ function formatPayload(payload, value, element) {
352
391
  */
353
392
  export async function getData(form) {
354
393
  const dataKeys = new Map();
355
- const formObject = forms.get(form);
356
-
357
- if (!formObject) {
358
- const selector = "[storage], [database], [array], [object]";
359
- const elementsInForm = form.querySelectorAll(selector);
360
- await init(elementsInForm);
361
- return await getData(form);
394
+ const selector = "[storage], [database], [array], [object]";
395
+
396
+ // form.elements includes controls associated through form="id", even when
397
+ // the controller lives elsewhere in the document.
398
+ for (const element of Array.from(form.elements || [])) {
399
+ if (!element.matches(selector) || element.closest("[template]")) continue;
400
+
401
+ // A dataKey identifies the document scope and intentionally excludes
402
+ // the field-level `key`. Reusing the registry entry's data here would
403
+ // therefore give every control the key of the first registered field.
404
+ const data = getDataObject(element);
405
+ registerFormElement(form, element, data);
362
406
  }
363
407
 
408
+ const formObject = forms.get(form);
409
+ if (!formObject) return [];
364
410
 
365
411
  for (const type of formObject.types.values()) {
366
412
  for (const [element, data] of type.entries()) {
367
413
  if (
414
+ element.closest("[template]") ||
368
415
  !element.hasAttribute("key") ||
369
416
  element.getAttribute("save") === "false"
370
417
  ) {
@@ -384,6 +431,17 @@ export async function getData(form) {
384
431
  const value = await element.getValue();
385
432
  if (value === undefined) continue;
386
433
 
434
+ // A form can expose multiple file sources (for example, device and
435
+ // camera) while only one source has a pending selection. An unused
436
+ // file input must not produce an empty object.create operation.
437
+ if (
438
+ element.matches('input[type="file"]') &&
439
+ Array.isArray(value) &&
440
+ value.length === 0
441
+ ) {
442
+ continue;
443
+ }
444
+
387
445
  const payload = { ...data };
388
446
  const dataKey = elements.get(element);
389
447
 
@@ -414,4 +472,4 @@ export default {
414
472
  getDataKeys,
415
473
  getData,
416
474
  getDataObject
417
- };
475
+ };
package/src/index.js CHANGED
@@ -32,8 +32,9 @@ import { reset } from "./formHandler.js";
32
32
 
33
33
  import "@cocreate/element-prototype";
34
34
  import "./dndHandler.js";
35
- import "./srcHandler.js";
36
- import "./valueQuery.js";
35
+ import "./srcHandler.js";
36
+ import "./socketHandler.js";
37
+ import "./valueQuery.js";
37
38
 
38
39
  import { elements, forms, keys, debounce } from "./mapRegistry.js"
39
40
 
@@ -50,4 +51,4 @@ export default {
50
51
  debounce,
51
52
  getAttributes,
52
53
  setDataType
53
- };
54
+ };
@@ -6,8 +6,15 @@ import { getAttributes } from "@cocreate/utils";
6
6
  import { initDataKey, getData, getDataObject } from "./getDataHandler.js";
7
7
  import { read, save } from "./crudHandler.js";
8
8
  import { setData } from "./setDataHandler.js";
9
- import { elements, forms, keys, debounce } from "./mapRegistry.js";
10
- import { unsubscribe} from "./subscribeHandler.js";
9
+ import {
10
+ elements,
11
+ forms,
12
+ keys,
13
+ debounce,
14
+ elementSubscriptions,
15
+ subscriptions
16
+ } from "./mapRegistry.js";
17
+ import { unsubscribe } from "./subscribeHandler.js";
11
18
 
12
19
  const selector = "[storage], [database], [array], [render-json]";
13
20
 
@@ -36,36 +43,31 @@ export async function init(element) {
36
43
  elements.has(element[i]) ||
37
44
  element[i].tagName === "FORM" ||
38
45
  element[i].getAttribute("crud") === "false"
39
- )
46
+ ) {
40
47
  continue;
48
+ }
41
49
 
42
50
  promises.push(
43
51
  (async () => {
44
- let data = await initElement(element[i]);
45
- if (data) {
46
- let dataKey = initDataKey(element[i], data);
47
- return { string: dataKey.string, data };
52
+ const el = element[i];
53
+ try {
54
+ const data = await initElement(el);
55
+ if (data) {
56
+ const dataKey = initDataKey(el, data);
57
+ // read() already batches elements by dataKey. Schedule each
58
+ // ready element without waiting for unrelated async fields.
59
+ await read(el, data, dataKey);
60
+ }
61
+ } catch (error) {
62
+ // Allow initialization to be retried if filter setup failed.
63
+ if (elements.get(el) === "") elements.delete(el);
64
+ console.error("[CoCreateElements] Failed to initialize element:", el, error);
48
65
  }
49
66
  })()
50
67
  );
51
68
  }
52
69
 
53
- let results = await Promise.all(promises);
54
- let dataObjects = new Map();
55
-
56
- for (let res of results) {
57
- if (res) dataObjects.set(res.string, res.data);
58
- }
59
-
60
- if (dataObjects && dataObjects.size > 0) {
61
- for (let key of dataObjects.keys()) {
62
- let keyEntry = keys.get(key);
63
- if (keyEntry) {
64
- let { data, elements: mappedElements } = keyEntry;
65
- read(Array.from(mappedElements.keys()), data, { string: key });
66
- }
67
- }
68
- }
70
+ await Promise.all(promises);
69
71
  }
70
72
 
71
73
  /**
@@ -76,14 +78,17 @@ export async function init(element) {
76
78
  */
77
79
  export async function initElement(el) {
78
80
  let data = getDataObject(el);
79
- if (!data || !data.type) return;
81
+ if (!data || !data.type) {
82
+ return;
83
+ }
80
84
 
81
85
  if (
82
86
  data.object &&
83
87
  data.object !== "pending" &&
84
88
  !/^[0-9a-fA-F]{24}$/.test(data.object)
85
- )
89
+ ) {
86
90
  return;
91
+ }
87
92
 
88
93
  if (!elements.has(el)) {
89
94
  elements.set(el, "");
@@ -124,10 +129,10 @@ export async function initElement(el) {
124
129
  await filter.init(el);
125
130
 
126
131
  if (el.getFilter) {
127
- data.$filter = await el.getFilter();
132
+ data.filter = await el.getFilter();
128
133
 
129
134
  el.setFilter = (filter) => {
130
- data.$filter = filter;
135
+ data.filter = filter;
131
136
  el.hasRead = false;
132
137
  read(el, data);
133
138
  };
@@ -160,16 +165,13 @@ export async function remove(element) {
160
165
 
161
166
  form = form.elements;
162
167
  for (let formEl of form.keys()) {
168
+ removeSubscription(formEl);
163
169
  let key = elements.get(formEl);
164
170
  if (key) {
165
171
  let keyEntry = keys.get(key);
166
172
  if (keyEntry) {
167
173
  keyEntry.elements.delete(formEl);
168
- if (!keyEntry.elements.size) {
169
- // Last element watching this key was removed -> trigger socket unsubscribe
170
- unsubscribe(keyEntry.dataKey, keyEntry.data);
171
- keys.delete(key);
172
- }
174
+ if (!keyEntry.elements.size) keys.delete(key);
173
175
  }
174
176
  elements.delete(formEl);
175
177
  }
@@ -180,15 +182,13 @@ export async function remove(element) {
180
182
  let keyEntry = keys.get(key);
181
183
  if (keyEntry) {
182
184
  keyEntry.elements.delete(el);
183
- if (!keyEntry.elements.size) {
184
- unsubscribe(keyEntry.dataKey, keyEntry.data);
185
- keys.delete(key);
186
- }
185
+ if (!keyEntry.elements.size) keys.delete(key);
187
186
  }
188
187
  }
189
188
  elements.delete(el);
190
189
  forms.delete(el);
191
190
  } else {
191
+ removeSubscription(el);
192
192
  let key = elements.get(el);
193
193
  if (key) {
194
194
  if (el.getFilter) filter.filters.delete(el);
@@ -198,11 +198,7 @@ export async function remove(element) {
198
198
  keyEntry.elements.delete(el);
199
199
  elements.delete(el);
200
200
 
201
- if (!keyEntry.elements.size) {
202
- // Last element watching this key was removed -> trigger socket unsubscribe
203
- unsubscribe(keyEntry.dataKey, keyEntry.data);
204
- keys.delete(key);
205
- }
201
+ if (!keyEntry.elements.size) keys.delete(key);
206
202
  } else {
207
203
  elements.delete(el);
208
204
  }
@@ -220,11 +216,27 @@ export async function remove(element) {
220
216
  }
221
217
  }
222
218
 
219
+ function removeSubscription(element) {
220
+ const subscription = elementSubscriptions.get(element);
221
+ if (!subscription) return;
222
+
223
+ const subscriptionEntry = subscriptions.get(subscription);
224
+ if (subscriptionEntry) {
225
+ subscriptionEntry.elements.delete(element);
226
+ if (!subscriptionEntry.elements.size) {
227
+ unsubscribe(subscriptionEntry.subscription);
228
+ subscriptions.delete(subscription);
229
+ }
230
+ }
231
+
232
+ elementSubscriptions.delete(element);
233
+ }
234
+
223
235
  /**
224
236
  * Initializes global CRUD socket response listeners.
225
237
  */
226
238
  export function initSocket() {
227
- const type = ["storage", "database", "array", "index", "object", "filter"];
239
+ const type = ["storage", "database", "array", "index", "object"];
228
240
  const method = ["create", "update", "delete"];
229
241
 
230
242
  for (let i = 0; i < type.length; i++) {
@@ -233,9 +245,11 @@ export function initSocket() {
233
245
 
234
246
  CRUD.listen(action, function (data) {
235
247
  if (
236
- data.resolved &&
248
+ data.key &&
249
+ !data.updateDB &&
237
250
  data.status === "received" &&
238
- CRUD.socket.clientId === data.clientId
251
+ !data.method.includes(".delete") &&
252
+ CRUD.socket.frameId === data.frameId
239
253
  )
240
254
  return;
241
255
  if (data.rendered) return;
@@ -253,6 +267,7 @@ Observer.init({
253
267
  types: ["addedNodes"],
254
268
  selector: selector,
255
269
  callback: function (mutation) {
270
+ if (mutation.move) return;
256
271
  init([mutation.target]);
257
272
  }
258
273
  });
@@ -262,7 +277,7 @@ Observer.init({
262
277
  types: ["removedNodes"],
263
278
  selector: selector,
264
279
  callback: function (mutation) {
265
- if (mutation.target.hasAttribute("render-clone")) return;
280
+ if (mutation.move) return;
266
281
  remove(mutation.target);
267
282
  }
268
283
  });
@@ -273,6 +288,8 @@ Observer.init({
273
288
  attributeFilter: [
274
289
  "organization_id",
275
290
  "host",
291
+ "namespace",
292
+ "room",
276
293
  "storage",
277
294
  "database",
278
295
  "array",
@@ -290,6 +307,39 @@ Observer.init({
290
307
  }
291
308
  });
292
309
 
310
+ Observer.init({
311
+ name: "CoCreateElementsReadAttribute",
312
+ types: ["attributes"],
313
+ attributeFilter: ["read"],
314
+ selector,
315
+ callback: async function (mutation) {
316
+ const el = mutation.target;
317
+ const currentValue = el.getAttribute("read");
318
+ if (currentValue === mutation.oldValue) return;
319
+
320
+ if (currentValue === "false") {
321
+ el.hasRead = false;
322
+ for (const [key, pending] of debounce) {
323
+ pending.elements?.delete(el);
324
+ if (pending.elements && !pending.elements.size) {
325
+ clearTimeout(pending.timer);
326
+ debounce.delete(key);
327
+ }
328
+ }
329
+ return;
330
+ }
331
+
332
+ if (!elements.has(el)) {
333
+ await init([el]);
334
+ } else if (el.getFilter && el.setFilter) {
335
+ el.setFilter(await el.getFilter());
336
+ } else {
337
+ el.hasRead = false;
338
+ await read(el);
339
+ }
340
+ }
341
+ });
342
+
293
343
  init();
294
344
 
295
- export default { init, initElement, initSocket, remove };
345
+ export default { init, initElement, initSocket, remove };
@@ -2,5 +2,14 @@ export const elements = new WeakMap();
2
2
  export const forms = new WeakMap();
3
3
  export const keys = new Map();
4
4
  export const debounce = new Map();
5
+ export const elementSubscriptions = new WeakMap();
6
+ export const subscriptions = new Map();
5
7
 
6
- export default { elements, forms, keys, debounce}
8
+ export default {
9
+ elements,
10
+ forms,
11
+ keys,
12
+ debounce,
13
+ elementSubscriptions,
14
+ subscriptions
15
+ }