@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.
@@ -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
- 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
- }
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
- 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
- }
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
- "organization_id",
59
- "host",
60
- "storage",
61
- "database",
62
- "array",
63
- "index",
64
- "object",
65
- "key"
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
- * @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
- }
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
- // 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
- }
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
- 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
- }
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
- 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
- };
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
- name: "CoCreateForm",
188
- types: ["addedNodes"],
189
- selector: "form",
190
- callback: (mutation) => init(mutation.target)
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
- 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)
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
- 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
- }
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
- name: "reset",
222
- endEvent: "reset",
223
- callback: (action) => {
224
- if (action.form) reset(action);
225
- }
276
+ name: "reset",
277
+ endEvent: "reset",
278
+ callback: (action) => {
279
+ if (action.form) reset(action);
280
+ }
226
281
  });
227
282
 
228
283
  init();