@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.
@@ -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
- if (!elements) elements = document.querySelectorAll("form");
17
- else if (!Array.isArray(elements)) elements = [elements];
18
-
19
- for (let element of elements) {
20
- runObjectId(element);
21
- setAttribute(element);
22
- disableAutoFill(element);
23
- setValue(element);
24
-
25
- element.addEventListener("submit", function (event) {
26
- if (!element.hasAttribute("action")) {
27
- event.preventDefault();
28
- }
29
- });
30
-
31
- // Handle form reset event
32
- element.addEventListener("reset", function (event) {
33
- event.preventDefault(); // Prevent default reset
34
- reset({ form: event.target }); // Call custom reset logic
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
- let elements = Array.from(form.querySelectorAll("[object='ObjectId()']"));
46
-
47
- if (form.getAttribute("object") === "ObjectId()") {
48
- elements.push(form);
49
- }
50
- for (let i = 0; i < elements.length; i++) {
51
- let array = elements[i].getAttribute("array");
52
- if (!array) continue;
53
- elements[i].setAttribute("object", ObjectId().toString());
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
- "organization_id",
59
- "host",
60
- "storage",
61
- "database",
62
- "array",
63
- "index",
64
- "object",
65
- "key"
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
- * @param form
71
- */
72
- function setAttribute(form, elements) {
73
- if (!elements) elements = form.querySelectorAll(formAttributesSelector);
74
- for (let el of elements) {
75
- if (el.getAttribute("form-attributes") === "false") continue;
76
- for (let attribute of form.attributes) {
77
- if (!formAttributes.includes(attribute.name)) continue;
78
- // let elArray = el.getAttribute("array");
79
- // if (elArray && attribute.name === "array" && elArray !== attribute.value) break;
80
- // Set the value of the attribute.
81
- // TODO: form-attributes="false" naming convention, perhaps skip by defualt if storage, database, array not the same and use attribute to apply for cases where one _id will be used across 2 arrays
82
- el.setAttribute(attribute.name, attribute.value);
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
- // Clear the text area and set the autocomplete attribute to off.
92
- if (element.tagName == "TEXTAREA") {
93
- element.value = "";
94
- element.setAttribute("autocomplete", "off");
95
- }
96
- // Set the autocomplete attribute to off.
97
- if (!element.hasAttribute("autocomplete")) {
98
- element.setAttribute("autocomplete", "off");
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
- let form = action.form;
107
-
108
- // Convert the form elements collection to an array
109
- const formElementsArray = Array.from(form.elements);
110
-
111
- // Query for additional elements with [object] or [key] attributes that are not already part of form controls
112
- const customElements = Array.from(
113
- form.querySelectorAll(
114
- "[object], [key]:not(input):not(select):not(textarea):not(button)"
115
- )
116
- );
117
-
118
- // Merge form elements and custom elements using the spread operator
119
- const allElements = [...formElementsArray, ...customElements];
120
-
121
- // Store the elements and their values in a map for restoration
122
- const elementStates = new Map();
123
-
124
- // Iterate over all elements and store their current state based on the 'reset' attribute
125
- for (const element of allElements) {
126
- if (["BUTTON", "FIELDSET"].includes(element.tagName)) continue;
127
- // Get the reset attribute value, if any
128
- const resetType = element.getAttribute("reset");
129
- if (
130
- element.hasAttribute("object") &&
131
- (!resetType || resetType === "object")
132
- )
133
- element.setAttribute("object", "");
134
- if (resetType === "false" || resetType === "object")
135
- elementStates.set(
136
- element,
137
- element.getValue() ||
138
- element.value ||
139
- element.getAttribute("value")
140
- );
141
- if (!resetType || resetType === "value") {
142
- if (element.contentEditable === "true") {
143
- element.innerHTML = "";
144
- } else {
145
- element.setValue("");
146
- }
147
- }
148
- }
149
-
150
- if (form.hasAttribute("object")) form.setAttribute("object", "");
151
-
152
- // Perform the default form reset
153
- form.reset();
154
-
155
- // Restore values based on the 'reset' attribute
156
- elementStates.forEach((value, element) => {
157
- element.setValue(value);
158
- });
159
-
160
- // Dispatch a custom reset event
161
- if (action.element) {
162
- action.element.dispatchEvent(
163
- new CustomEvent("reset", {
164
- detail: {}
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
- form.setValue = (value) => {
172
- if (typeof value !== "object") elementPrototype.setValue(form, value);
173
- else {
174
- const inputs = form.querySelectorAll("[name], [key]");
175
- inputs.forEach((element) => {
176
- const key =
177
- element.getAttribute("key") || element.getAttribute("name");
178
- if (value[key]) {
179
- element.setValue(value[key]);
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
- name: "CoCreateForm",
188
- types: ["addedNodes"],
189
- selector: "form",
190
- callback: (mutation) => init(mutation.target)
244
+ name: "CoCreateForm",
245
+ types: ["addedNodes"],
246
+ selector: formSelector,
247
+ callback: (mutation) => init(mutation.target)
191
248
  });
192
249
 
193
250
  Observer.init({
194
- name: "CoCreateForm",
195
- types: ["attributes"],
196
- attributeFilter: getAttributeNames([
197
- "organization_id",
198
- "host",
199
- "storage",
200
- "database",
201
- "array",
202
- "object",
203
- "index"
204
- ]),
205
- selector: "form",
206
- callback: (mutation) =>
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
- name: "CoCreateFormElements",
212
- types: ["addedNodes"],
213
- selector: formAttributesSelector,
214
- callback: function (mutation) {
215
- let form = mutation.target.form || mutation.target.closest("form");
216
- if (form) setAttribute(form, [mutation.target]);
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
- name: "reset",
222
- endEvent: "reset",
223
- callback: (action) => {
224
- if (action.form) reset(action);
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";