@jsenv/navi 0.0.1 → 0.1.1
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/dist/jsenv_navi.js +22959 -0
- package/index.js +66 -16
- package/package.json +23 -11
- package/src/actions.js +50 -26
- package/src/browser_integration/browser_integration.js +31 -6
- package/src/browser_integration/via_history.js +42 -9
- package/src/components/action_execution/render_actionable_component.jsx +6 -4
- package/src/components/action_execution/use_action.js +51 -282
- package/src/components/action_execution/use_execute_action.js +106 -92
- package/src/components/action_execution/use_run_on_mount.js +9 -0
- package/src/components/action_renderer.jsx +21 -32
- package/src/components/demos/0_button_demo.html +574 -103
- package/src/components/demos/10_column_reordering_debug.html +277 -0
- package/src/components/demos/11_table_selection_debug.html +432 -0
- package/src/components/demos/1_checkbox_demo.html +579 -202
- package/src/components/demos/2_input_textual_demo.html +81 -138
- package/src/components/demos/3_radio_demo.html +0 -2
- package/src/components/demos/4_select_demo.html +19 -23
- package/src/components/demos/6_tablist_demo.html +77 -0
- package/src/components/demos/7_table_selection_demo.html +176 -0
- package/src/components/demos/8_table_fixed_headers_demo.html +584 -0
- package/src/components/demos/9_table_column_drag_demo.html +325 -0
- package/src/components/demos/action/0_button_demo.html +2 -4
- package/src/components/demos/action/1_input_text_demo.html +643 -222
- package/src/components/demos/action/3_details_demo.html +146 -115
- package/src/components/demos/action/4_input_checkbox_demo.html +442 -322
- package/src/components/demos/action/5_input_checkbox_state_demo.html +270 -0
- package/src/components/demos/action/6_checkbox_list_demo.html +304 -72
- package/src/components/demos/action/7_radio_list_demo.html +310 -170
- package/src/components/demos/action/{8_editable_text_demo.html → 8_editable_demo.html} +65 -76
- package/src/components/demos/action/9_link_demo.html +84 -62
- package/src/components/demos/ui_transition/0_action_renderer_ui_transition_demo.html +695 -0
- package/src/components/demos/ui_transition/1_nested_ui_transition_demo.html +429 -0
- package/src/components/demos/ui_transition/2_height_transition_test.html +295 -0
- package/src/components/details/details.jsx +62 -64
- package/src/components/edition/editable.jsx +186 -0
- package/src/components/field/README.md +247 -0
- package/src/components/{input → field}/button.jsx +151 -130
- package/src/components/field/checkbox_list.jsx +184 -0
- package/src/components/{collect_form_element_values.js → field/collect_form_element_values.js} +7 -4
- package/src/components/{input → field}/field_css.js +4 -1
- package/src/components/field/form.jsx +211 -0
- package/src/components/{input → field}/input.jsx +1 -0
- package/src/components/{input → field}/input_checkbox.jsx +132 -155
- package/src/components/{input → field}/input_radio.jsx +135 -46
- package/src/components/field/input_textual.jsx +418 -0
- package/src/components/field/label.jsx +32 -0
- package/src/components/field/radio_list.jsx +182 -0
- package/src/components/{input → field}/select.jsx +17 -32
- package/src/components/field/use_action_events.js +132 -0
- package/src/components/field/use_form_events.js +55 -0
- package/src/components/field/use_ui_state_controller.js +506 -0
- package/src/components/item_tracker/README.md +461 -0
- package/src/components/item_tracker/use_isolated_item_tracker.jsx +209 -0
- package/src/components/item_tracker/use_isolated_item_tracker_demo.html +148 -0
- package/src/components/item_tracker/use_isolated_item_tracker_demo.jsx +460 -0
- package/src/components/item_tracker/use_item_tracker.jsx +143 -0
- package/src/components/item_tracker/use_item_tracker_demo.html +207 -0
- package/src/components/item_tracker/use_item_tracker_demo.jsx +216 -0
- package/src/components/keyboard_shortcuts/active_keyboard_shortcuts.jsx +87 -0
- package/src/components/keyboard_shortcuts/aria_key_shortcuts.js +61 -0
- package/src/components/keyboard_shortcuts/keyboard_key_meta.js +17 -0
- package/src/components/keyboard_shortcuts/keyboard_shortcuts.js +371 -0
- package/src/components/link/link.jsx +65 -102
- package/src/components/link/link_with_icon.jsx +52 -0
- package/src/components/loader/loader_background.jsx +85 -64
- package/src/components/loader/rectangle_loading.jsx +38 -19
- package/src/components/route.jsx +8 -4
- package/src/components/selection/selection.jsx +1583 -0
- package/src/components/svg/font_sized_svg.jsx +45 -0
- package/src/components/svg/icon_and_text.jsx +21 -0
- package/src/components/svg/svg_mask_overlay.jsx +105 -0
- package/src/components/table/drag/table_drag.jsx +506 -0
- package/src/components/table/resize/table_resize.jsx +650 -0
- package/src/components/table/resize/table_size.js +43 -0
- package/src/components/table/selection/table_selection.js +106 -0
- package/src/components/table/selection/table_selection.jsx +203 -0
- package/src/components/table/sticky/sticky_group.js +354 -0
- package/src/components/table/sticky/table_sticky.js +25 -0
- package/src/components/table/sticky/table_sticky.jsx +501 -0
- package/src/components/table/table.jsx +721 -0
- package/src/components/table/table_css.js +211 -0
- package/src/components/table/table_ui.jsx +49 -0
- package/src/components/table/use_cells_and_columns.js +90 -0
- package/src/components/table/use_object_array_to_cells.js +46 -0
- package/src/components/table/z_indexes.js +23 -0
- package/src/components/tablist/tablist.jsx +99 -0
- package/src/components/text/overflow.jsx +15 -0
- package/src/components/text/text_and_count.jsx +28 -0
- package/src/components/ui_transition.jsx +128 -0
- package/src/components/use_auto_focus.js +58 -7
- package/src/components/use_batch_during_render.js +33 -0
- package/src/components/use_debounce_true.js +7 -7
- package/src/components/use_dependencies_diff.js +35 -0
- package/src/components/use_focus_group.js +4 -3
- package/src/components/use_initial_value.js +8 -34
- package/src/components/use_signal_sync.js +1 -1
- package/src/components/use_stable_callback.js +68 -0
- package/src/components/use_state_array.js +16 -9
- package/src/docs/actions.md +22 -0
- package/src/notes.md +33 -12
- package/src/route/route.js +97 -47
- package/src/store/resource_graph.js +2 -1
- package/src/store/tests/{resource_graph_dependencies.test.js → resource_graph_dependencies.test_manual.js} +13 -13
- package/src/utils/is_signal.js +20 -0
- package/src/utils/stringify_for_display.js +4 -23
- package/src/validation/constraints/confirm_constraint.js +14 -0
- package/src/validation/constraints/create_unique_value_constraint.js +27 -0
- package/src/validation/constraints/native_constraints.js +313 -0
- package/src/validation/constraints/readonly_constraint.js +36 -0
- package/src/validation/constraints/single_space_constraint.js +13 -0
- package/src/validation/custom_constraint_validation.js +599 -0
- package/src/validation/custom_message.js +18 -0
- package/src/validation/demos/browser_style.png +0 -0
- package/src/validation/demos/form_validation_demo.html +142 -0
- package/src/validation/demos/form_validation_demo_preact.html +87 -0
- package/src/validation/demos/form_validation_native_popover_demo.html +168 -0
- package/src/validation/demos/form_validation_vs_native_demo.html +172 -0
- package/src/validation/demos/validation_message_demo.html +203 -0
- package/src/validation/hooks/use_constraints.js +23 -0
- package/src/validation/hooks/use_custom_validation_ref.js +73 -0
- package/src/validation/hooks/use_validation_message.js +19 -0
- package/src/validation/validation_message.js +741 -0
- package/src/components/editable_text/editable_text.jsx +0 -96
- package/src/components/form.jsx +0 -144
- package/src/components/input/checkbox_list.jsx +0 -294
- package/src/components/input/field.jsx +0 -61
- package/src/components/input/input_textual.jsx +0 -338
- package/src/components/input/radio_list.jsx +0 -283
- package/src/components/input/use_form_event.js +0 -20
- package/src/components/input/use_on_change.js +0 -12
- package/src/components/selection/selection.js +0 -5
- package/src/components/selection/selection_context.jsx +0 -262
- package/src/components/shortcut/shortcut_context.jsx +0 -390
- package/src/components/use_action_events.js +0 -37
- package/src/utils/iterable_weak_set.js +0 -62
- /package/src/components/demos/action/{11_nested_shortcuts_demo.html → 11_nested_shortcuts_demo.xhtml} +0 -0
- /package/src/components/{shortcut → keyboard_shortcuts}/os.js +0 -0
- /package/src/route/{route.test.html → route.xtest.html} +0 -0
|
@@ -0,0 +1,599 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom form validation implementation
|
|
3
|
+
*
|
|
4
|
+
* This implementation addresses several limitations of the browser's native validation API:
|
|
5
|
+
*
|
|
6
|
+
* Limitations of native validation:
|
|
7
|
+
* - Cannot programmatically detect if validation message is currently displayed
|
|
8
|
+
* - No ability to dismiss messages with keyboard (e.g., Escape key)
|
|
9
|
+
* - Requires complex event handling to manage validation message display
|
|
10
|
+
* - Limited support for storing/managing multiple validation messages
|
|
11
|
+
* - No customization of validation message appearance
|
|
12
|
+
*
|
|
13
|
+
* Design approach:
|
|
14
|
+
* - Works alongside native validation (which acts as a fallback)
|
|
15
|
+
* - Proactively detects validation issues before native validation triggers
|
|
16
|
+
* - Provides complete control over validation message UX
|
|
17
|
+
* - Supports keyboard navigation and dismissal
|
|
18
|
+
* - Allows custom styling and positioning of validation messages
|
|
19
|
+
*
|
|
20
|
+
* Features:
|
|
21
|
+
* - Constraint-based validation system with built-in and custom constraints
|
|
22
|
+
* - Custom validation messages with different severity levels
|
|
23
|
+
* - Form submission prevention on validation failure
|
|
24
|
+
* - Validation on Enter key in forms or standalone inputs
|
|
25
|
+
* - Escape key to dismiss validation messages
|
|
26
|
+
* - Support for standard HTML validation attributes (required, pattern, type="email")
|
|
27
|
+
* - Validation messages that follow the input element and adapt to viewport
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import {
|
|
31
|
+
DISABLED_CONSTRAINT,
|
|
32
|
+
MAX_CONSTRAINT,
|
|
33
|
+
MAX_LENGTH_CONSTRAINT,
|
|
34
|
+
MIN_CONSTRAINT,
|
|
35
|
+
MIN_LENGTH_CONSTRAINT,
|
|
36
|
+
PATTERN_CONSTRAINT,
|
|
37
|
+
REQUIRED_CONSTRAINT,
|
|
38
|
+
TYPE_EMAIL_CONSTRAINT,
|
|
39
|
+
TYPE_NUMBER_CONSTRAINT,
|
|
40
|
+
} from "./constraints/native_constraints.js";
|
|
41
|
+
import { READONLY_CONSTRAINT } from "./constraints/readonly_constraint.js";
|
|
42
|
+
import { openValidationMessage } from "./validation_message.js";
|
|
43
|
+
|
|
44
|
+
let debug = false;
|
|
45
|
+
|
|
46
|
+
const validationInProgressWeakSet = new WeakSet();
|
|
47
|
+
|
|
48
|
+
export const requestAction = (
|
|
49
|
+
target,
|
|
50
|
+
action,
|
|
51
|
+
{
|
|
52
|
+
event,
|
|
53
|
+
requester = target,
|
|
54
|
+
actionOrigin,
|
|
55
|
+
method = "rerun",
|
|
56
|
+
meta = {},
|
|
57
|
+
confirmMessage,
|
|
58
|
+
} = {},
|
|
59
|
+
) => {
|
|
60
|
+
if (!actionOrigin) {
|
|
61
|
+
console.warn("requestAction: actionOrigin is required");
|
|
62
|
+
}
|
|
63
|
+
let elementToValidate = requester;
|
|
64
|
+
|
|
65
|
+
let validationInterface = elementToValidate.__validationInterface__;
|
|
66
|
+
if (!validationInterface) {
|
|
67
|
+
validationInterface = installCustomConstraintValidation(elementToValidate);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const customEventDetail = {
|
|
71
|
+
action,
|
|
72
|
+
actionOrigin,
|
|
73
|
+
method,
|
|
74
|
+
event,
|
|
75
|
+
requester,
|
|
76
|
+
meta,
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
if (debug) {
|
|
80
|
+
console.debug(
|
|
81
|
+
`action requested by`,
|
|
82
|
+
requester,
|
|
83
|
+
`(event: "${event?.type}")`,
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Determine what needs to be validated and how to handle the result
|
|
88
|
+
const isForm = elementToValidate.tagName === "FORM";
|
|
89
|
+
const formToValidate = isForm ? elementToValidate : elementToValidate.form;
|
|
90
|
+
|
|
91
|
+
let isValid = false;
|
|
92
|
+
let elementForConfirmation = elementToValidate;
|
|
93
|
+
let elementForDispatch = elementToValidate;
|
|
94
|
+
|
|
95
|
+
if (formToValidate) {
|
|
96
|
+
// Form validation case
|
|
97
|
+
if (validationInProgressWeakSet.has(formToValidate)) {
|
|
98
|
+
if (debug) {
|
|
99
|
+
console.debug(`validation already in progress for`, formToValidate);
|
|
100
|
+
}
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
validationInProgressWeakSet.add(formToValidate);
|
|
104
|
+
setTimeout(() => {
|
|
105
|
+
validationInProgressWeakSet.delete(formToValidate);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// Validate all form elements
|
|
109
|
+
const formElements = formToValidate.elements;
|
|
110
|
+
isValid = true; // Assume valid until proven otherwise
|
|
111
|
+
for (const formElement of formElements) {
|
|
112
|
+
const elementValidationInterface = formElement.__validationInterface__;
|
|
113
|
+
if (!elementValidationInterface) {
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const elementIsValid = elementValidationInterface.checkValidity({
|
|
118
|
+
fromRequestAction: true,
|
|
119
|
+
skipReadonly:
|
|
120
|
+
formElement.tagName === "BUTTON" && formElement !== requester,
|
|
121
|
+
});
|
|
122
|
+
if (!elementIsValid) {
|
|
123
|
+
elementValidationInterface.reportValidity();
|
|
124
|
+
isValid = false;
|
|
125
|
+
break;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
elementForConfirmation = formToValidate;
|
|
130
|
+
elementForDispatch = target;
|
|
131
|
+
} else {
|
|
132
|
+
// Single element validation case
|
|
133
|
+
isValid = validationInterface.checkValidity({ fromRequestAction: true });
|
|
134
|
+
if (!isValid) {
|
|
135
|
+
if (event) {
|
|
136
|
+
event.preventDefault();
|
|
137
|
+
}
|
|
138
|
+
validationInterface.reportValidity();
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
elementForConfirmation = target;
|
|
142
|
+
elementForDispatch = target;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// If validation failed, dispatch actionprevented and return
|
|
146
|
+
if (!isValid) {
|
|
147
|
+
const actionPreventedCustomEvent = new CustomEvent("actionprevented", {
|
|
148
|
+
detail: customEventDetail,
|
|
149
|
+
});
|
|
150
|
+
elementForDispatch.dispatchEvent(actionPreventedCustomEvent);
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Validation passed, check for confirmation
|
|
155
|
+
confirmMessage =
|
|
156
|
+
confirmMessage ||
|
|
157
|
+
elementForConfirmation.getAttribute("data-confirm-message");
|
|
158
|
+
if (confirmMessage) {
|
|
159
|
+
// eslint-disable-next-line no-alert
|
|
160
|
+
if (!window.confirm(confirmMessage)) {
|
|
161
|
+
const actionPreventedCustomEvent = new CustomEvent("actionprevented", {
|
|
162
|
+
detail: customEventDetail,
|
|
163
|
+
});
|
|
164
|
+
elementForDispatch.dispatchEvent(actionPreventedCustomEvent);
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// All good, dispatch the action
|
|
170
|
+
const actionCustomEvent = new CustomEvent("action", {
|
|
171
|
+
detail: customEventDetail,
|
|
172
|
+
});
|
|
173
|
+
if (debug) {
|
|
174
|
+
console.debug(
|
|
175
|
+
`element is valid -> dispatch "action" on`,
|
|
176
|
+
elementForDispatch,
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
elementForDispatch.dispatchEvent(actionCustomEvent);
|
|
180
|
+
return true;
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
export const closeValidationMessage = (element, reason) => {
|
|
184
|
+
const validationInterface = element.__validationInterface__;
|
|
185
|
+
if (!validationInterface) {
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
const { validationMessage } = validationInterface;
|
|
189
|
+
if (!validationMessage) {
|
|
190
|
+
return false;
|
|
191
|
+
}
|
|
192
|
+
return validationMessage.close(reason);
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
export const checkValidity = (element) => {
|
|
196
|
+
const validationInterface = element.__validationInterface__;
|
|
197
|
+
if (!validationInterface) {
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
return validationInterface.checkValidity();
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
export const installCustomConstraintValidation = (
|
|
204
|
+
element,
|
|
205
|
+
elementReceivingValidationMessage = element,
|
|
206
|
+
) => {
|
|
207
|
+
if (element.tagName === "INPUT" && element.type === "hidden") {
|
|
208
|
+
elementReceivingValidationMessage = element.form || document.body;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const validationInterface = {
|
|
212
|
+
uninstall: undefined,
|
|
213
|
+
registerConstraint: undefined,
|
|
214
|
+
addCustomMessage: undefined,
|
|
215
|
+
removeCustomMessage: undefined,
|
|
216
|
+
checkValidity: undefined,
|
|
217
|
+
reportValidity: undefined,
|
|
218
|
+
validationMessage: null,
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
const cleanupCallbackSet = new Set();
|
|
222
|
+
cleanup: {
|
|
223
|
+
const uninstall = () => {
|
|
224
|
+
for (const cleanupCallback of cleanupCallbackSet) {
|
|
225
|
+
cleanupCallback();
|
|
226
|
+
}
|
|
227
|
+
cleanupCallbackSet.clear();
|
|
228
|
+
};
|
|
229
|
+
validationInterface.uninstall = uninstall;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
expose_as_node_property: {
|
|
233
|
+
element.__validationInterface__ = validationInterface;
|
|
234
|
+
cleanupCallbackSet.add(() => {
|
|
235
|
+
delete element.__validationInterface__;
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const dispatchCancelCustomEvent = (options) => {
|
|
240
|
+
const cancelEvent = new CustomEvent("cancel", options);
|
|
241
|
+
element.dispatchEvent(cancelEvent);
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
const closeElementValidationMessage = (reason) => {
|
|
245
|
+
if (validationInterface.validationMessage) {
|
|
246
|
+
validationInterface.validationMessage.close(reason);
|
|
247
|
+
return true;
|
|
248
|
+
}
|
|
249
|
+
return false;
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
const constraintSet = new Set();
|
|
253
|
+
constraintSet.add(DISABLED_CONSTRAINT);
|
|
254
|
+
constraintSet.add(REQUIRED_CONSTRAINT);
|
|
255
|
+
constraintSet.add(PATTERN_CONSTRAINT);
|
|
256
|
+
constraintSet.add(TYPE_EMAIL_CONSTRAINT);
|
|
257
|
+
constraintSet.add(TYPE_NUMBER_CONSTRAINT);
|
|
258
|
+
constraintSet.add(MIN_LENGTH_CONSTRAINT);
|
|
259
|
+
constraintSet.add(MAX_LENGTH_CONSTRAINT);
|
|
260
|
+
constraintSet.add(MIN_CONSTRAINT);
|
|
261
|
+
constraintSet.add(MAX_CONSTRAINT);
|
|
262
|
+
constraintSet.add(READONLY_CONSTRAINT);
|
|
263
|
+
register_constraint: {
|
|
264
|
+
validationInterface.registerConstraint = (constraint) => {
|
|
265
|
+
if (typeof constraint === "function") {
|
|
266
|
+
constraint = {
|
|
267
|
+
name: constraint.name || "custom_function",
|
|
268
|
+
check: constraint,
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
constraintSet.add(constraint);
|
|
272
|
+
return () => {
|
|
273
|
+
constraintSet.delete(constraint);
|
|
274
|
+
};
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
let failedConstraintInfo = null;
|
|
279
|
+
const validityInfoMap = new Map();
|
|
280
|
+
|
|
281
|
+
const resetValidity = ({ fromRequestAction } = {}) => {
|
|
282
|
+
if (fromRequestAction && failedConstraintInfo) {
|
|
283
|
+
for (const [key, customMessage] of customMessageMap) {
|
|
284
|
+
if (customMessage.removeOnRequestAction) {
|
|
285
|
+
customMessageMap.delete(key);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
for (const [, validityInfo] of validityInfoMap) {
|
|
291
|
+
if (validityInfo.cleanup) {
|
|
292
|
+
validityInfo.cleanup();
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
validityInfoMap.clear();
|
|
296
|
+
failedConstraintInfo = null;
|
|
297
|
+
};
|
|
298
|
+
cleanupCallbackSet.add(resetValidity);
|
|
299
|
+
|
|
300
|
+
const checkValidity = ({ fromRequestAction, skipReadonly } = {}) => {
|
|
301
|
+
resetValidity({ fromRequestAction });
|
|
302
|
+
for (const constraint of constraintSet) {
|
|
303
|
+
const constraintCleanupSet = new Set();
|
|
304
|
+
const registerChange = (register) => {
|
|
305
|
+
const registerResult = register(() => {
|
|
306
|
+
checkValidity();
|
|
307
|
+
});
|
|
308
|
+
if (typeof registerResult === "function") {
|
|
309
|
+
constraintCleanupSet.add(registerResult);
|
|
310
|
+
}
|
|
311
|
+
};
|
|
312
|
+
const cleanup = () => {
|
|
313
|
+
for (const cleanupCallback of constraintCleanupSet) {
|
|
314
|
+
cleanupCallback();
|
|
315
|
+
}
|
|
316
|
+
constraintCleanupSet.clear();
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
const checkResult = constraint.check(element, {
|
|
320
|
+
fromRequestAction,
|
|
321
|
+
skipReadonly,
|
|
322
|
+
registerChange,
|
|
323
|
+
});
|
|
324
|
+
if (!checkResult) {
|
|
325
|
+
cleanup();
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
const constraintValidityInfo =
|
|
329
|
+
typeof checkResult === "string"
|
|
330
|
+
? { message: checkResult }
|
|
331
|
+
: checkResult;
|
|
332
|
+
|
|
333
|
+
failedConstraintInfo = {
|
|
334
|
+
name: constraint.name,
|
|
335
|
+
constraint,
|
|
336
|
+
...constraintValidityInfo,
|
|
337
|
+
cleanup,
|
|
338
|
+
reportStatus: "not_reported",
|
|
339
|
+
};
|
|
340
|
+
validityInfoMap.set(constraint, failedConstraintInfo);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (!failedConstraintInfo) {
|
|
344
|
+
closeElementValidationMessage("becomes_valid");
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
return !failedConstraintInfo;
|
|
348
|
+
};
|
|
349
|
+
const reportValidity = ({ skipFocus } = {}) => {
|
|
350
|
+
if (!failedConstraintInfo) {
|
|
351
|
+
closeElementValidationMessage("becomes_valid");
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
if (failedConstraintInfo.silent) {
|
|
355
|
+
closeElementValidationMessage("invalid_silent");
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
if (validationInterface.validationMessage) {
|
|
359
|
+
const { message, level, closeOnClickOutside } = failedConstraintInfo;
|
|
360
|
+
validationInterface.validationMessage.update(message, {
|
|
361
|
+
level,
|
|
362
|
+
closeOnClickOutside,
|
|
363
|
+
});
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
if (!skipFocus) {
|
|
367
|
+
element.focus();
|
|
368
|
+
}
|
|
369
|
+
const closeOnCleanup = () => {
|
|
370
|
+
closeElementValidationMessage("cleanup");
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
const elementTarget =
|
|
374
|
+
failedConstraintInfo.target || elementReceivingValidationMessage;
|
|
375
|
+
|
|
376
|
+
validationInterface.validationMessage = openValidationMessage(
|
|
377
|
+
elementTarget,
|
|
378
|
+
failedConstraintInfo.message,
|
|
379
|
+
{
|
|
380
|
+
level: failedConstraintInfo.level,
|
|
381
|
+
closeOnClickOutside: failedConstraintInfo.closeOnClickOutside,
|
|
382
|
+
onClose: () => {
|
|
383
|
+
cleanupCallbackSet.delete(closeOnCleanup);
|
|
384
|
+
validationInterface.validationMessage = null;
|
|
385
|
+
if (failedConstraintInfo) {
|
|
386
|
+
failedConstraintInfo.reportStatus = "closed";
|
|
387
|
+
}
|
|
388
|
+
if (!skipFocus) {
|
|
389
|
+
element.focus();
|
|
390
|
+
}
|
|
391
|
+
},
|
|
392
|
+
},
|
|
393
|
+
);
|
|
394
|
+
failedConstraintInfo.reportStatus = "reported";
|
|
395
|
+
cleanupCallbackSet.add(closeOnCleanup);
|
|
396
|
+
};
|
|
397
|
+
validationInterface.checkValidity = checkValidity;
|
|
398
|
+
validationInterface.reportValidity = reportValidity;
|
|
399
|
+
|
|
400
|
+
const customMessageMap = new Map();
|
|
401
|
+
custom_message: {
|
|
402
|
+
constraintSet.add({
|
|
403
|
+
name: "custom_message",
|
|
404
|
+
check: () => {
|
|
405
|
+
for (const [, { message, level }] of customMessageMap) {
|
|
406
|
+
return { message, level };
|
|
407
|
+
}
|
|
408
|
+
return null;
|
|
409
|
+
},
|
|
410
|
+
});
|
|
411
|
+
const addCustomMessage = (
|
|
412
|
+
key,
|
|
413
|
+
message,
|
|
414
|
+
{ level = "error", removeOnRequestAction = false } = {},
|
|
415
|
+
) => {
|
|
416
|
+
customMessageMap.set(key, { message, level, removeOnRequestAction });
|
|
417
|
+
checkValidity();
|
|
418
|
+
reportValidity();
|
|
419
|
+
return () => {
|
|
420
|
+
removeCustomMessage(key);
|
|
421
|
+
};
|
|
422
|
+
};
|
|
423
|
+
const removeCustomMessage = (key) => {
|
|
424
|
+
if (customMessageMap.has(key)) {
|
|
425
|
+
customMessageMap.delete(key);
|
|
426
|
+
checkValidity();
|
|
427
|
+
reportValidity();
|
|
428
|
+
}
|
|
429
|
+
};
|
|
430
|
+
cleanupCallbackSet.add(() => {
|
|
431
|
+
customMessageMap.clear();
|
|
432
|
+
});
|
|
433
|
+
Object.assign(validationInterface, {
|
|
434
|
+
addCustomMessage,
|
|
435
|
+
removeCustomMessage,
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
close_and_check_on_input: {
|
|
440
|
+
const oninput = () => {
|
|
441
|
+
customMessageMap.clear();
|
|
442
|
+
closeElementValidationMessage("input_event");
|
|
443
|
+
checkValidity();
|
|
444
|
+
};
|
|
445
|
+
element.addEventListener("input", oninput);
|
|
446
|
+
cleanupCallbackSet.add(() => {
|
|
447
|
+
element.removeEventListener("input", oninput);
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
check_on_actionend: {
|
|
452
|
+
// this ensure we re-check validity (and remove message no longer relevant)
|
|
453
|
+
// once the action ends (used to remove the NOT_BUSY_CONSTRAINT message)
|
|
454
|
+
const onactionend = () => {
|
|
455
|
+
checkValidity();
|
|
456
|
+
};
|
|
457
|
+
element.addEventListener("actionend", onactionend);
|
|
458
|
+
if (element.form) {
|
|
459
|
+
element.form.addEventListener("actionend", onactionend);
|
|
460
|
+
cleanupCallbackSet.add(() => {
|
|
461
|
+
element.form.removeEventListener("actionend", onactionend);
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
cleanupCallbackSet.add(() => {
|
|
465
|
+
element.removeEventListener("actionend", onactionend);
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
report_on_report_validity_call: {
|
|
470
|
+
const nativeReportValidity = element.reportValidity;
|
|
471
|
+
element.reportValidity = () => {
|
|
472
|
+
reportValidity();
|
|
473
|
+
};
|
|
474
|
+
cleanupCallbackSet.add(() => {
|
|
475
|
+
element.reportValidity = nativeReportValidity;
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
dispatch_request_submit_call_on_form: {
|
|
480
|
+
const onRequestSubmit = (form, e) => {
|
|
481
|
+
if (form !== element.form && form !== element) {
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
const requestSubmitCustomEvent = new CustomEvent("requestsubmit", {
|
|
486
|
+
cancelable: true,
|
|
487
|
+
detail: { cause: e },
|
|
488
|
+
});
|
|
489
|
+
form.dispatchEvent(requestSubmitCustomEvent);
|
|
490
|
+
if (requestSubmitCustomEvent.defaultPrevented) {
|
|
491
|
+
e.preventDefault();
|
|
492
|
+
}
|
|
493
|
+
};
|
|
494
|
+
requestSubmitCallbackSet.add(onRequestSubmit);
|
|
495
|
+
cleanupCallbackSet.add(() => {
|
|
496
|
+
requestSubmitCallbackSet.delete(onRequestSubmit);
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
execute_on_form_submit: {
|
|
501
|
+
const form = element.form || element.tagName === "FORM" ? element : null;
|
|
502
|
+
if (!form) {
|
|
503
|
+
break execute_on_form_submit;
|
|
504
|
+
}
|
|
505
|
+
const removeListener = addEventListener(form, "submit", (e) => {
|
|
506
|
+
e.preventDefault();
|
|
507
|
+
if (debug) {
|
|
508
|
+
console.debug(`"submit" called -> dispatch "action" on`, form);
|
|
509
|
+
}
|
|
510
|
+
const actionCustomEvent = new CustomEvent("action", {
|
|
511
|
+
detail: {
|
|
512
|
+
action: null,
|
|
513
|
+
event: e,
|
|
514
|
+
method: "rerun",
|
|
515
|
+
requester: form,
|
|
516
|
+
meta: {},
|
|
517
|
+
},
|
|
518
|
+
});
|
|
519
|
+
form.dispatchEvent(actionCustomEvent);
|
|
520
|
+
});
|
|
521
|
+
cleanupCallbackSet.add(() => {
|
|
522
|
+
removeListener();
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
close_on_escape: {
|
|
527
|
+
const onkeydown = (e) => {
|
|
528
|
+
if (e.key === "Escape") {
|
|
529
|
+
if (!closeElementValidationMessage("escape_key")) {
|
|
530
|
+
dispatchCancelCustomEvent({ detail: { reason: "escape_key" } });
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
};
|
|
534
|
+
element.addEventListener("keydown", onkeydown);
|
|
535
|
+
cleanupCallbackSet.add(() => {
|
|
536
|
+
element.removeEventListener("keydown", onkeydown);
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
cancel_on_blur: {
|
|
541
|
+
const onblur = () => {
|
|
542
|
+
if (element.value === "") {
|
|
543
|
+
dispatchCancelCustomEvent({ detail: { reason: "blur_empty" } });
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
// if we have failed constraint, we cancel too
|
|
547
|
+
if (failedConstraintInfo) {
|
|
548
|
+
dispatchCancelCustomEvent({
|
|
549
|
+
detail: {
|
|
550
|
+
reason: "blur_invalid",
|
|
551
|
+
failedConstraintInfo,
|
|
552
|
+
},
|
|
553
|
+
});
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
};
|
|
557
|
+
element.addEventListener("blur", onblur);
|
|
558
|
+
cleanupCallbackSet.add(() => {
|
|
559
|
+
element.removeEventListener("blur", onblur);
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
return validationInterface;
|
|
564
|
+
};
|
|
565
|
+
|
|
566
|
+
// https://developer.mozilla.org/en-US/docs/Web/HTML/Guides/Constraint_validation
|
|
567
|
+
|
|
568
|
+
const requestSubmitCallbackSet = new Set();
|
|
569
|
+
const requestSubmit = HTMLFormElement.prototype.requestSubmit;
|
|
570
|
+
HTMLFormElement.prototype.requestSubmit = function (submitter) {
|
|
571
|
+
let prevented = false;
|
|
572
|
+
const preventDefault = () => {
|
|
573
|
+
prevented = true;
|
|
574
|
+
};
|
|
575
|
+
for (const requestSubmitCallback of requestSubmitCallbackSet) {
|
|
576
|
+
requestSubmitCallback(this, { submitter, preventDefault });
|
|
577
|
+
}
|
|
578
|
+
if (prevented) {
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
requestSubmit.call(this, submitter);
|
|
582
|
+
};
|
|
583
|
+
|
|
584
|
+
// const submit = HTMLFormElement.prototype.submit;
|
|
585
|
+
// HTMLFormElement.prototype.submit = function (...args) {
|
|
586
|
+
// const form = this;
|
|
587
|
+
// if (form.hasAttribute("data-method")) {
|
|
588
|
+
// console.warn("You must use form.requestSubmit() instead of form.submit()");
|
|
589
|
+
// return form.requestSubmit();
|
|
590
|
+
// }
|
|
591
|
+
// return submit.apply(this, args);
|
|
592
|
+
// };
|
|
593
|
+
|
|
594
|
+
const addEventListener = (element, event, callback) => {
|
|
595
|
+
element.addEventListener(event, callback);
|
|
596
|
+
return () => {
|
|
597
|
+
element.removeEventListener(event, callback);
|
|
598
|
+
};
|
|
599
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { installCustomConstraintValidation } from "./custom_constraint_validation.js";
|
|
2
|
+
|
|
3
|
+
export const addCustomMessage = (element, key, message, options) => {
|
|
4
|
+
const customConstraintValidation =
|
|
5
|
+
element.__validationInterface__ ||
|
|
6
|
+
(element.__validationInterface__ =
|
|
7
|
+
installCustomConstraintValidation(element));
|
|
8
|
+
|
|
9
|
+
return customConstraintValidation.addCustomMessage(key, message, options);
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export const removeCustomMessage = (element, key) => {
|
|
13
|
+
const customConstraintValidation = element.__validationInterface__;
|
|
14
|
+
if (!customConstraintValidation) {
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
customConstraintValidation.removeCustomMessage(key);
|
|
18
|
+
};
|
|
Binary file
|