@cocreate/elements 1.43.1 → 1.44.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.
@@ -0,0 +1,417 @@
1
+ import {
2
+ getAttributes,
3
+ checkValue
4
+ } from "@cocreate/utils";
5
+
6
+ import { elements, forms, keys } from "./mapRegistry.js";
7
+ import { subscribe } from "./subscribeHandler.js";
8
+
9
+ /**
10
+ * Parses and normalizes HTML attributes from an element into a canonical CRUD data object.
11
+ * Infers execution type (object, index, array, database, storage, data) dynamically.
12
+ *
13
+ * @param {Element} element
14
+ * @returns {Object|undefined} Normalized data payload object
15
+ */
16
+ export function getDataObject(element) {
17
+ const data = getAttributes(element);
18
+ const crudType = ["storage", "database", "array", "index", "object"];
19
+
20
+ for (let i = 0; i < crudType.length; i++) {
21
+ if (!checkValue(data[crudType[i]])) return;
22
+
23
+ if (data[crudType[i]] && data[crudType[i]].includes(",")) {
24
+ const array = data[crudType[i]].split(",");
25
+ data[crudType[i]] = [];
26
+
27
+ for (let j = 0; j < array.length; j++) {
28
+ const item = array[j].trim();
29
+ if (crudType[i] === "object") {
30
+ data[crudType[i]].push({ _id: item });
31
+ } else {
32
+ data[crudType[i]].push(item);
33
+ }
34
+ }
35
+ }
36
+ }
37
+
38
+ if (data.object || data.object === "" || data.key || data.key === "") {
39
+ if (!data.array || (data.array && !data.array.length)) return;
40
+ data.type = "object";
41
+ } else if (data.index || data.index === "") {
42
+ if (!data.array || (data.array && !data.array.length)) return;
43
+ data.type = "index";
44
+ } else if (data.array || data.array === "") data.type = "array";
45
+ else if (data.database || data.database === "") data.type = "database";
46
+ else if (data.storage || data.storage === "") data.type = "storage";
47
+ else if (data.data) data.type = "data";
48
+
49
+ delete data.isRealtime;
50
+ return data;
51
+ }
52
+
53
+ /**
54
+ * Registers an element and its generated dataKey into local client maps.
55
+ * Binds element save/read helper methods and associates form context.
56
+ *
57
+ * @param {Element} element
58
+ * @param {Object} data
59
+ * @returns {{ string: string, object: Object }}
60
+ */
61
+ export function initDataKey(element, data) {
62
+ const dataKey = getDataKey(data);
63
+
64
+ if (keys.has(dataKey.string)) {
65
+ keys.get(dataKey.string).elements.set(element, "");
66
+ } else {
67
+ keys.set(dataKey.string, {
68
+ elements: new Map([[element, ""]]),
69
+ data,
70
+ dataKey
71
+ });
72
+ subscribe(dataKey, data);
73
+ }
74
+
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
+ }
97
+ }
98
+ }
99
+
100
+ if (!element.read) element.read = () => read(element);
101
+ if (!element.save) element.save = () => save(element);
102
+
103
+ return dataKey;
104
+ }
105
+
106
+ /**
107
+ * Generates the canonical client-side dataKey object for a payload.
108
+ * Field-level "key" is explicitly excluded for object-level operations
109
+ * to ensure all fields targeting the same document share 1 dataKey.
110
+ *
111
+ * @param {Object} data
112
+ * @returns {{ string: string, object: Object }}
113
+ */
114
+ export function getDataKey(data) {
115
+ const dataKey = {};
116
+ const attributes = [
117
+ "host",
118
+ "organization_id",
119
+ "apikey",
120
+ "method",
121
+ "type",
122
+ "storage",
123
+ "database",
124
+ "array",
125
+ "index",
126
+ "object",
127
+ "key",
128
+ "updateKey",
129
+ "$filter",
130
+ "upsert",
131
+ "namespace",
132
+ "room",
133
+ "broadcast",
134
+ "broadcastSender",
135
+ "broadcastBrowser"
136
+ ];
137
+
138
+ for (const attribute of attributes) {
139
+ // Exclude field-level "key" from object-scope dataKeys so fields share document key
140
+ if (attribute === "key" && data.type === "object") continue;
141
+
142
+ const value = data[attribute];
143
+ if (value !== undefined && value !== null && value !== "") {
144
+ if (Array.isArray(value)) {
145
+ const sortedVal = [...value];
146
+ if (typeof sortedVal[0] === "string") sortedVal.sort();
147
+ dataKey[attribute] = sortedVal;
148
+ } else {
149
+ dataKey[attribute] = value;
150
+ }
151
+ }
152
+ }
153
+
154
+ const object = Object.fromEntries(
155
+ Object.entries(dataKey).sort(([a], [b]) => a.localeCompare(b))
156
+ );
157
+ const string = JSON.stringify(object);
158
+
159
+ return { string, object };
160
+ }
161
+
162
+ /**
163
+ * Finds all DOM elements watching keys matched by an incoming payload.
164
+ *
165
+ * @param {Object} data
166
+ * @returns {Element[]}
167
+ */
168
+ export function getDataElements(data) {
169
+ const matchingElements = [];
170
+ const matchingKeys = getDataKeys(data);
171
+
172
+ for (let i = 0; i < matchingKeys.length; i++) {
173
+ const keyEntry = keys.get(matchingKeys[i]);
174
+ if (keyEntry?.elements?.size) {
175
+ matchingElements.push(...keyEntry.elements.keys());
176
+ }
177
+ }
178
+ return matchingElements;
179
+ }
180
+
181
+ /**
182
+ * Evaluates registered client keys against a payload to identify subscriptions.
183
+ *
184
+ * @param {Object} data
185
+ * @returns {string[]} Matching key strings
186
+ */
187
+ export function getDataKeys(data) {
188
+ const matchingKeyStrings = [];
189
+ const targetKeys = [
190
+ "type",
191
+ "storage",
192
+ "database",
193
+ "array",
194
+ "index",
195
+ "object",
196
+ "$filter"
197
+ ];
198
+
199
+ for (const [keyString, sortedKey] of keys.entries()) {
200
+ let hasMatch = true;
201
+
202
+ for (const key of targetKeys) {
203
+ if (
204
+ !data.$filter &&
205
+ key === "$filter" &&
206
+ sortedKey.data.hasOwnProperty(key)
207
+ ) {
208
+ hasMatch = true;
209
+ } else if (data.hasOwnProperty(key)) {
210
+ if (!sortedKey.data.hasOwnProperty(key)) {
211
+ hasMatch = false;
212
+ break;
213
+ }
214
+ if (
215
+ Array.isArray(sortedKey.data[key]) &&
216
+ Array.isArray(data[key])
217
+ ) {
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;
232
+ }
233
+ }
234
+ } else if (sortedKey.data[key] !== data[key]) {
235
+ hasMatch = false;
236
+ break;
237
+ }
238
+ }
239
+ }
240
+
241
+ if (hasMatch) {
242
+ matchingKeyStrings.push(keyString);
243
+ }
244
+ }
245
+
246
+ return matchingKeyStrings;
247
+ }
248
+
249
+ /**
250
+ * Formats values into a standardized CRUD write payload.
251
+ */
252
+ function formatPayload(payload, value, element) {
253
+ if (!payload[payload.type] && payload.key) {
254
+ payload.method = payload.type + ".create";
255
+ if (payload.type === "object") {
256
+ if (!payload.object) payload.object = {};
257
+ else if (typeof payload.object === "string")
258
+ payload.object = { _id: payload.object };
259
+
260
+ if (payload.key) {
261
+ if (payload.key === "{}") {
262
+ if (Array.isArray(value)) {
263
+ payload.object = value;
264
+ } else payload.object = { ...payload.object, ...value };
265
+ } else payload.object[payload.key] = value;
266
+ }
267
+ if (payload.isUpsert && payload.$filter)
268
+ payload.method = "object.update";
269
+ } else payload[payload.type] = value;
270
+ } else if (payload[payload.type] && payload.key) {
271
+ const attributes = element.attributes;
272
+ if (payload.key.startsWith("$")) {
273
+ const operators = ["$rename", "$inc", "$push", "$each", "$splice", "$unset", "$delete", "$slice", "$pop", "$shift", "$addToSet", "$pull"];
274
+ const matchedAttr = Array.from(attributes).find(attr => operators.includes(attr.name));
275
+ if (matchedAttr) {
276
+ payload.key = matchedAttr.name + "." + payload.key;
277
+ }
278
+ }
279
+
280
+ if (payload.type === "object") {
281
+ if (typeof payload[payload.type] === "string") {
282
+ if (payload.key === "{}") {
283
+ payload[payload.type] = {
284
+ _id: payload[payload.type],
285
+ ...(Array.isArray(value) ? value[0] : value)
286
+ };
287
+ } else {
288
+ payload[payload.type] = {
289
+ _id: payload[payload.type],
290
+ [payload.key]: value
291
+ };
292
+ }
293
+ } else if (Array.isArray(payload[payload.type])) {
294
+ if (payload.key === "{}") {
295
+ payload[payload.type][0] = {
296
+ ...payload[payload.type][0],
297
+ ...value
298
+ };
299
+ } else {
300
+ if (payload[payload.type][0]) {
301
+ payload[payload.type][0][payload.key] = value;
302
+ }
303
+ if (!payload[payload.type][0]?._id) {
304
+ payload.method = payload.type + ".create";
305
+ }
306
+ }
307
+ } else if (typeof payload[payload.type] === "object") {
308
+ if (payload.key === "{}") {
309
+ payload[payload.type] = { ...payload[payload.type], ...value };
310
+ } else {
311
+ payload[payload.type][payload.key] = value;
312
+ if (!payload[payload.type]._id) {
313
+ payload.method = payload.type + ".create";
314
+ }
315
+ }
316
+ }
317
+ } else {
318
+ payload[payload.type] = { [payload[payload.type]]: value };
319
+ }
320
+
321
+ if (payload.isUpdate || payload.isUpdate === "") {
322
+ if (!payload.key) return false;
323
+ delete payload.isUpdate;
324
+ payload.updateKey = { [payload.key]: value };
325
+ payload.method = payload.type + ".update";
326
+ } else if (payload.isDelete || payload.isDelete === "") {
327
+ delete payload.isDelete;
328
+ if (payload.type === "object" && payload.key) {
329
+ payload.method = payload.type + ".update";
330
+ if (typeof payload[payload.type] === "string") {
331
+ payload[payload.type] = {
332
+ _id: payload[payload.type],
333
+ [payload.key]: undefined
334
+ };
335
+ } else if (typeof payload[payload.type] === "object") {
336
+ payload[payload.type][payload.key] = undefined;
337
+ }
338
+ } else {
339
+ payload.method = payload.type + ".delete";
340
+ }
341
+ }
342
+ }
343
+ return true;
344
+ }
345
+
346
+ /**
347
+ * Extracts and aggregates form field values into unified CRUD operational payloads.
348
+ * Groups inputs targeting the same document scope via dataKey into a single write operation.
349
+ *
350
+ * @param {Element} form
351
+ * @returns {Promise<Object[]>} Array of CRUD request payloads
352
+ */
353
+ export async function getData(form) {
354
+ 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);
362
+ }
363
+
364
+
365
+ for (const type of formObject.types.values()) {
366
+ for (const [element, data] of type.entries()) {
367
+ if (
368
+ !element.hasAttribute("key") ||
369
+ element.getAttribute("save") === "false"
370
+ ) {
371
+ continue;
372
+ }
373
+
374
+ if (element.hasAttribute("actions")) {
375
+ const actionAttr = element.getAttribute("actions");
376
+ if (actionAttr.includes("save") || actionAttr.includes("delete")) continue;
377
+ }
378
+
379
+ if (element.matches("[render-query]")) {
380
+ const attrType = element.getAttribute("type");
381
+ if (attrType !== "file") continue;
382
+ }
383
+
384
+ const value = await element.getValue();
385
+ if (value === undefined) continue;
386
+
387
+ const payload = { ...data };
388
+ const dataKey = elements.get(element);
389
+
390
+ const success = formatPayload(payload, value, element);
391
+ if (!success) continue;
392
+
393
+ // Group form fields sharing the exact same document dataKey into 1 payload write
394
+ const groupKey = dataKey;
395
+ if (dataKeys.has(groupKey)) {
396
+ const storedPayload = dataKeys.get(groupKey);
397
+ storedPayload[payload.type] = {
398
+ ...storedPayload[payload.type],
399
+ ...payload[payload.type]
400
+ };
401
+ } else {
402
+ dataKeys.set(groupKey, payload);
403
+ }
404
+ }
405
+ }
406
+
407
+ return Array.from(dataKeys.values());
408
+ }
409
+
410
+ export default {
411
+ initDataKey,
412
+ getDataKey,
413
+ getDataElements,
414
+ getDataKeys,
415
+ getData,
416
+ getDataObject
417
+ };