@cocreate/elements 1.43.2 → 1.44.2
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/.github/workflows/automated.yml +22 -20
- package/package.json +9 -10
- package/src/crudHandler.js +282 -0
- package/src/dndHandler.js +155 -0
- package/src/getDataHandler.js +417 -0
- package/src/index.js +11 -1455
- package/src/initHandler.js +295 -0
- package/src/mapRegistry.js +6 -0
- package/src/setDataHandler.js +441 -0
- package/src/subscribeHandler.js +114 -0
- /package/src/{form.js → formHandler.js} +0 -0
- /package/src/{fetchSrc.js → srcHandler.js} +0 -0
- /package/src/{value.js → valueQuery.js} +0 -0
|
@@ -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
|
+
};
|