@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.
- package/CONTRIBUTING.md +1 -1
- package/README.md +467 -44
- package/package.json +9 -8
- package/src/crudHandler.js +150 -46
- package/src/dndHandler.js +11 -8
- package/src/formHandler.js +225 -170
- package/src/getDataHandler.js +111 -53
- package/src/index.js +4 -3
- package/src/initHandler.js +96 -46
- package/src/mapRegistry.js +10 -1
- package/src/setDataHandler.js +163 -84
- package/src/socketHandler.js +328 -0
- package/src/srcHandler.js +68 -32
- package/src/subscribeHandler.js +64 -64
- package/test/deferred-src.test.cjs +11 -0
- package/test/form-enter.test.cjs +48 -0
- package/test/get-data.test.cjs +100 -0
- package/test/init-read.test.cjs +145 -0
- package/test/load-more.test.cjs +19 -0
- package/test/save-array.test.cjs +31 -0
- package/test/save-completion.test.cjs +36 -0
- package/test/set-data-type.test.cjs +119 -0
- package/test/ssr-response.test.cjs +108 -0
- package/webpack.config.js +1 -0
package/src/formHandler.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import Observer from "@cocreate/observer";
|
|
2
|
-
import { getAttributeNames, ObjectId } from "@cocreate/utils";
|
|
2
|
+
import { getAttributeNames, ObjectId, queryElements } from "@cocreate/utils";
|
|
3
3
|
import Action from "@cocreate/actions";
|
|
4
4
|
import elementPrototype from "@cocreate/element-prototype";
|
|
5
5
|
|
|
@@ -13,27 +13,64 @@ import elementPrototype from "@cocreate/element-prototype";
|
|
|
13
13
|
* - If an array of form elements is provided, each element in the array is initialized.
|
|
14
14
|
*/
|
|
15
15
|
function init(elements) {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
16
|
+
if (!elements) elements = document.querySelectorAll("form, fieldset");
|
|
17
|
+
else if (!Array.isArray(elements)) elements = [elements];
|
|
18
|
+
|
|
19
|
+
for (let element of elements) {
|
|
20
|
+
runObjectId(element);
|
|
21
|
+
setAttribute(element);
|
|
22
|
+
if (element.tagName === "FIELDSET") continue;
|
|
23
|
+
disableAutoFill(element);
|
|
24
|
+
setValue(element);
|
|
25
|
+
element.addEventListener("keydown", handleEnter);
|
|
26
|
+
|
|
27
|
+
element.addEventListener("submit", function (event) {
|
|
28
|
+
if (!element.hasAttribute("action")) {
|
|
29
|
+
event.preventDefault();
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// Handle form reset event
|
|
34
|
+
element.addEventListener("reset", function (event) {
|
|
35
|
+
event.preventDefault(); // Prevent default reset
|
|
36
|
+
reset({ form: event.target }); // Call custom reset logic
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Enter shortcuts are explicit and do not replace native widget keyboard behavior.
|
|
42
|
+
function handleEnter(event) {
|
|
43
|
+
if (event.key !== "Enter" || event.defaultPrevented || event.isComposing ||
|
|
44
|
+
event.keyCode === 229 || event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) return;
|
|
45
|
+
|
|
46
|
+
const input = event.target;
|
|
47
|
+
const form = event.currentTarget;
|
|
48
|
+
if (input.tagName !== "INPUT" || input.form !== form ||
|
|
49
|
+
!["text", "search", "email", "url", "tel", "password", "number"].includes(input.type) ||
|
|
50
|
+
input.isContentEditable || input.closest(
|
|
51
|
+
'[role="combobox"], [role="listbox"], [role="tree"], [role="grid"], ' +
|
|
52
|
+
'[role="tablist"], [role="menu"], [aria-activedescendant], [aria-multiline="true"], input[list]'
|
|
53
|
+
)) return;
|
|
54
|
+
|
|
55
|
+
const source = input.closest("[submit-query]");
|
|
56
|
+
if (!source || (source !== form && !form.contains(source))) return;
|
|
57
|
+
|
|
58
|
+
// An opted-in shortcut must not fall through to implicit submission, even
|
|
59
|
+
// when its target is disabled, missing, or the key is being held down.
|
|
60
|
+
event.preventDefault();
|
|
61
|
+
if (event.repeat) return;
|
|
62
|
+
const selector = source.getAttribute("submit-query");
|
|
63
|
+
if (!selector?.trim()) return;
|
|
64
|
+
const targets = queryElements({ element: source, selector });
|
|
65
|
+
if (targets.length !== 1) return;
|
|
66
|
+
const target = targets[0];
|
|
67
|
+
if (!target.hasAttribute("actions") || typeof target.click !== "function" ||
|
|
68
|
+
(target.form || target.closest("form")) !== form ||
|
|
69
|
+
target.matches(":disabled, [disabled], [aria-disabled='true']") ||
|
|
70
|
+
target.closest("[hidden], [inert], [aria-hidden='true']") ||
|
|
71
|
+
!target.getClientRects().length || getComputedStyle(target).visibility !== "visible") return;
|
|
72
|
+
|
|
73
|
+
target.click();
|
|
37
74
|
}
|
|
38
75
|
|
|
39
76
|
/**
|
|
@@ -42,187 +79,205 @@ function init(elements) {
|
|
|
42
79
|
// TODO: runObjectId could potentially be removed and handled by element-prototype getAttribute $object_id
|
|
43
80
|
// Somthing to consider is that $object_id is not replaced by an _id causing getAttribute to return a new _id on each get
|
|
44
81
|
function runObjectId(form) {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
82
|
+
let elements = Array.from(form.querySelectorAll("[object='ObjectId()']"));
|
|
83
|
+
|
|
84
|
+
if (form.getAttribute("object") === "ObjectId()") {
|
|
85
|
+
elements.push(form);
|
|
86
|
+
}
|
|
87
|
+
for (let i = 0; i < elements.length; i++) {
|
|
88
|
+
let array = elements[i].getAttribute("array");
|
|
89
|
+
if (!array) continue;
|
|
90
|
+
elements[i].setAttribute("object", ObjectId().toString());
|
|
91
|
+
}
|
|
55
92
|
}
|
|
56
93
|
|
|
57
94
|
const formAttributes = [
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
95
|
+
"organization_id",
|
|
96
|
+
"host",
|
|
97
|
+
"storage",
|
|
98
|
+
"database",
|
|
99
|
+
"array",
|
|
100
|
+
"index",
|
|
101
|
+
"object",
|
|
102
|
+
"key"
|
|
66
103
|
];
|
|
67
104
|
const formAttributesSelector = formAttributes.map((attr) => `[${attr}]`).join(", ");
|
|
68
105
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
106
|
+
function setAttribute(scope, elements, attributes) {
|
|
107
|
+
const updateAttributes = attributes || formAttributes;
|
|
108
|
+
if (!elements) {
|
|
109
|
+
const selector = attributes
|
|
110
|
+
? attributes.map((attribute) => `[${attribute}]`).join(", ")
|
|
111
|
+
: formAttributesSelector;
|
|
112
|
+
if (!selector) return;
|
|
113
|
+
elements = scope.querySelectorAll(selector);
|
|
114
|
+
}
|
|
115
|
+
for (let el of elements) {
|
|
116
|
+
// Fieldsets define scopes; copying onto them would erase their overrides.
|
|
117
|
+
if (el.tagName === "FIELDSET" || el.getAttribute("form-attributes") === "false") continue;
|
|
118
|
+
|
|
119
|
+
for (let name of updateAttributes) {
|
|
120
|
+
let value = null;
|
|
121
|
+
|
|
122
|
+
let parent = el.parentElement;
|
|
123
|
+
while (parent) {
|
|
124
|
+
if ((parent.tagName === "FORM" ||
|
|
125
|
+
(parent.tagName === "FIELDSET" && name !== "key")) &&
|
|
126
|
+
parent.hasAttribute(name)) {
|
|
127
|
+
value = parent.getAttribute(name);
|
|
128
|
+
break;
|
|
129
|
+
}
|
|
130
|
+
if (attributes && parent === scope) break;
|
|
131
|
+
parent = parent.parentElement;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (value !== null) {
|
|
135
|
+
if (el.getAttribute(name) !== value) el.setAttribute(name, value);
|
|
136
|
+
} else if (attributes && parent === scope) {
|
|
137
|
+
el.removeAttribute(name);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
85
141
|
}
|
|
86
142
|
|
|
87
143
|
/**
|
|
88
144
|
* @param element
|
|
89
145
|
*/
|
|
90
146
|
function disableAutoFill(element) {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
147
|
+
// Clear the text area and set the autocomplete attribute to off.
|
|
148
|
+
if (element.tagName == "TEXTAREA") {
|
|
149
|
+
element.value = "";
|
|
150
|
+
element.setAttribute("autocomplete", "off");
|
|
151
|
+
}
|
|
152
|
+
// Set the autocomplete attribute to off.
|
|
153
|
+
if (!element.hasAttribute("autocomplete")) {
|
|
154
|
+
element.setAttribute("autocomplete", "off");
|
|
155
|
+
}
|
|
100
156
|
}
|
|
101
157
|
|
|
102
158
|
/**
|
|
103
159
|
* @param form
|
|
104
160
|
*/
|
|
105
161
|
function reset(action) {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
162
|
+
let form = action.form;
|
|
163
|
+
|
|
164
|
+
// Convert the form elements collection to an array
|
|
165
|
+
const formElementsArray = Array.from(form.elements);
|
|
166
|
+
|
|
167
|
+
// Query for additional elements with [object] or [key] attributes that are not already part of form controls
|
|
168
|
+
const customElements = Array.from(
|
|
169
|
+
form.querySelectorAll(
|
|
170
|
+
"[object], [key]:not(input):not(select):not(textarea):not(button)"
|
|
171
|
+
)
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
// Merge form elements and custom elements using the spread operator
|
|
175
|
+
const allElements = [...formElementsArray, ...customElements];
|
|
176
|
+
|
|
177
|
+
// Store the elements and their values in a map for restoration
|
|
178
|
+
const elementStates = new Map();
|
|
179
|
+
|
|
180
|
+
// Iterate over all elements and store their current state based on the 'reset' attribute
|
|
181
|
+
for (const element of allElements) {
|
|
182
|
+
if (["BUTTON", "FIELDSET"].includes(element.tagName)) continue;
|
|
183
|
+
// Get the reset attribute value, if any
|
|
184
|
+
const resetType = element.getAttribute("reset");
|
|
185
|
+
if (
|
|
186
|
+
element.hasAttribute("object") &&
|
|
187
|
+
(!resetType || resetType === "object")
|
|
188
|
+
)
|
|
189
|
+
element.setAttribute("object", "");
|
|
190
|
+
if (resetType === "false" || resetType === "object")
|
|
191
|
+
elementStates.set(
|
|
192
|
+
element,
|
|
193
|
+
element.getValue() ||
|
|
194
|
+
element.value ||
|
|
195
|
+
element.getAttribute("value")
|
|
196
|
+
);
|
|
197
|
+
if (!resetType || resetType === "value") {
|
|
198
|
+
if (element.contentEditable === "true") {
|
|
199
|
+
element.innerHTML = "";
|
|
200
|
+
} else {
|
|
201
|
+
element.setValue("");
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (form.hasAttribute("object")) form.setAttribute("object", "");
|
|
207
|
+
|
|
208
|
+
// Perform the default form reset
|
|
209
|
+
form.reset();
|
|
210
|
+
|
|
211
|
+
// Restore values based on the 'reset' attribute
|
|
212
|
+
elementStates.forEach((value, element) => {
|
|
213
|
+
element.setValue(value);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
// Dispatch a custom reset event
|
|
217
|
+
if (action.element) {
|
|
218
|
+
action.element.dispatchEvent(
|
|
219
|
+
new CustomEvent("reset", {
|
|
220
|
+
detail: {}
|
|
221
|
+
})
|
|
222
|
+
);
|
|
223
|
+
}
|
|
168
224
|
}
|
|
169
225
|
|
|
170
226
|
function setValue(form) {
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
227
|
+
form.setValue = (value) => {
|
|
228
|
+
if (typeof value !== "object") elementPrototype.setValue(form, value);
|
|
229
|
+
else {
|
|
230
|
+
const inputs = form.querySelectorAll("[name], [key]");
|
|
231
|
+
inputs.forEach((element) => {
|
|
232
|
+
const key =
|
|
233
|
+
element.getAttribute("key") || element.getAttribute("name");
|
|
234
|
+
if (value[key]) {
|
|
235
|
+
element.setValue(value[key]);
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
};
|
|
184
240
|
}
|
|
185
241
|
|
|
186
242
|
Observer.init({
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
243
|
+
name: "CoCreateForm",
|
|
244
|
+
types: ["addedNodes"],
|
|
245
|
+
selector: "form, fieldset",
|
|
246
|
+
callback: (mutation) => init(mutation.target)
|
|
191
247
|
});
|
|
192
248
|
|
|
193
249
|
Observer.init({
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
mutation.target.tagName === "FORM" && setAttribute(mutation.target)
|
|
250
|
+
name: "CoCreateForm",
|
|
251
|
+
types: ["attributes"],
|
|
252
|
+
attributeFilter: getAttributeNames([
|
|
253
|
+
"organization_id",
|
|
254
|
+
"host",
|
|
255
|
+
"storage",
|
|
256
|
+
"database",
|
|
257
|
+
"array",
|
|
258
|
+
"object",
|
|
259
|
+
"index"
|
|
260
|
+
]),
|
|
261
|
+
selector: "form, fieldset",
|
|
262
|
+
callback: (mutation) => setAttribute(mutation.target, null, [mutation.attributeName])
|
|
208
263
|
});
|
|
209
264
|
|
|
210
265
|
Observer.init({
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
266
|
+
name: "CoCreateFormElements",
|
|
267
|
+
types: ["addedNodes"],
|
|
268
|
+
selector: formAttributesSelector,
|
|
269
|
+
callback: function (mutation) {
|
|
270
|
+
let form = mutation.target.closest("form, fieldset");
|
|
271
|
+
if (form) setAttribute(form, [mutation.target]);
|
|
272
|
+
}
|
|
218
273
|
});
|
|
219
274
|
|
|
220
275
|
Action.init({
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
276
|
+
name: "reset",
|
|
277
|
+
endEvent: "reset",
|
|
278
|
+
callback: (action) => {
|
|
279
|
+
if (action.form) reset(action);
|
|
280
|
+
}
|
|
226
281
|
});
|
|
227
282
|
|
|
228
283
|
init();
|