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