@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.
Files changed (139) hide show
  1. package/dist/jsenv_navi.js +22959 -0
  2. package/index.js +66 -16
  3. package/package.json +23 -11
  4. package/src/actions.js +50 -26
  5. package/src/browser_integration/browser_integration.js +31 -6
  6. package/src/browser_integration/via_history.js +42 -9
  7. package/src/components/action_execution/render_actionable_component.jsx +6 -4
  8. package/src/components/action_execution/use_action.js +51 -282
  9. package/src/components/action_execution/use_execute_action.js +106 -92
  10. package/src/components/action_execution/use_run_on_mount.js +9 -0
  11. package/src/components/action_renderer.jsx +21 -32
  12. package/src/components/demos/0_button_demo.html +574 -103
  13. package/src/components/demos/10_column_reordering_debug.html +277 -0
  14. package/src/components/demos/11_table_selection_debug.html +432 -0
  15. package/src/components/demos/1_checkbox_demo.html +579 -202
  16. package/src/components/demos/2_input_textual_demo.html +81 -138
  17. package/src/components/demos/3_radio_demo.html +0 -2
  18. package/src/components/demos/4_select_demo.html +19 -23
  19. package/src/components/demos/6_tablist_demo.html +77 -0
  20. package/src/components/demos/7_table_selection_demo.html +176 -0
  21. package/src/components/demos/8_table_fixed_headers_demo.html +584 -0
  22. package/src/components/demos/9_table_column_drag_demo.html +325 -0
  23. package/src/components/demos/action/0_button_demo.html +2 -4
  24. package/src/components/demos/action/1_input_text_demo.html +643 -222
  25. package/src/components/demos/action/3_details_demo.html +146 -115
  26. package/src/components/demos/action/4_input_checkbox_demo.html +442 -322
  27. package/src/components/demos/action/5_input_checkbox_state_demo.html +270 -0
  28. package/src/components/demos/action/6_checkbox_list_demo.html +304 -72
  29. package/src/components/demos/action/7_radio_list_demo.html +310 -170
  30. package/src/components/demos/action/{8_editable_text_demo.html → 8_editable_demo.html} +65 -76
  31. package/src/components/demos/action/9_link_demo.html +84 -62
  32. package/src/components/demos/ui_transition/0_action_renderer_ui_transition_demo.html +695 -0
  33. package/src/components/demos/ui_transition/1_nested_ui_transition_demo.html +429 -0
  34. package/src/components/demos/ui_transition/2_height_transition_test.html +295 -0
  35. package/src/components/details/details.jsx +62 -64
  36. package/src/components/edition/editable.jsx +186 -0
  37. package/src/components/field/README.md +247 -0
  38. package/src/components/{input → field}/button.jsx +151 -130
  39. package/src/components/field/checkbox_list.jsx +184 -0
  40. package/src/components/{collect_form_element_values.js → field/collect_form_element_values.js} +7 -4
  41. package/src/components/{input → field}/field_css.js +4 -1
  42. package/src/components/field/form.jsx +211 -0
  43. package/src/components/{input → field}/input.jsx +1 -0
  44. package/src/components/{input → field}/input_checkbox.jsx +132 -155
  45. package/src/components/{input → field}/input_radio.jsx +135 -46
  46. package/src/components/field/input_textual.jsx +418 -0
  47. package/src/components/field/label.jsx +32 -0
  48. package/src/components/field/radio_list.jsx +182 -0
  49. package/src/components/{input → field}/select.jsx +17 -32
  50. package/src/components/field/use_action_events.js +132 -0
  51. package/src/components/field/use_form_events.js +55 -0
  52. package/src/components/field/use_ui_state_controller.js +506 -0
  53. package/src/components/item_tracker/README.md +461 -0
  54. package/src/components/item_tracker/use_isolated_item_tracker.jsx +209 -0
  55. package/src/components/item_tracker/use_isolated_item_tracker_demo.html +148 -0
  56. package/src/components/item_tracker/use_isolated_item_tracker_demo.jsx +460 -0
  57. package/src/components/item_tracker/use_item_tracker.jsx +143 -0
  58. package/src/components/item_tracker/use_item_tracker_demo.html +207 -0
  59. package/src/components/item_tracker/use_item_tracker_demo.jsx +216 -0
  60. package/src/components/keyboard_shortcuts/active_keyboard_shortcuts.jsx +87 -0
  61. package/src/components/keyboard_shortcuts/aria_key_shortcuts.js +61 -0
  62. package/src/components/keyboard_shortcuts/keyboard_key_meta.js +17 -0
  63. package/src/components/keyboard_shortcuts/keyboard_shortcuts.js +371 -0
  64. package/src/components/link/link.jsx +65 -102
  65. package/src/components/link/link_with_icon.jsx +52 -0
  66. package/src/components/loader/loader_background.jsx +85 -64
  67. package/src/components/loader/rectangle_loading.jsx +38 -19
  68. package/src/components/route.jsx +8 -4
  69. package/src/components/selection/selection.jsx +1583 -0
  70. package/src/components/svg/font_sized_svg.jsx +45 -0
  71. package/src/components/svg/icon_and_text.jsx +21 -0
  72. package/src/components/svg/svg_mask_overlay.jsx +105 -0
  73. package/src/components/table/drag/table_drag.jsx +506 -0
  74. package/src/components/table/resize/table_resize.jsx +650 -0
  75. package/src/components/table/resize/table_size.js +43 -0
  76. package/src/components/table/selection/table_selection.js +106 -0
  77. package/src/components/table/selection/table_selection.jsx +203 -0
  78. package/src/components/table/sticky/sticky_group.js +354 -0
  79. package/src/components/table/sticky/table_sticky.js +25 -0
  80. package/src/components/table/sticky/table_sticky.jsx +501 -0
  81. package/src/components/table/table.jsx +721 -0
  82. package/src/components/table/table_css.js +211 -0
  83. package/src/components/table/table_ui.jsx +49 -0
  84. package/src/components/table/use_cells_and_columns.js +90 -0
  85. package/src/components/table/use_object_array_to_cells.js +46 -0
  86. package/src/components/table/z_indexes.js +23 -0
  87. package/src/components/tablist/tablist.jsx +99 -0
  88. package/src/components/text/overflow.jsx +15 -0
  89. package/src/components/text/text_and_count.jsx +28 -0
  90. package/src/components/ui_transition.jsx +128 -0
  91. package/src/components/use_auto_focus.js +58 -7
  92. package/src/components/use_batch_during_render.js +33 -0
  93. package/src/components/use_debounce_true.js +7 -7
  94. package/src/components/use_dependencies_diff.js +35 -0
  95. package/src/components/use_focus_group.js +4 -3
  96. package/src/components/use_initial_value.js +8 -34
  97. package/src/components/use_signal_sync.js +1 -1
  98. package/src/components/use_stable_callback.js +68 -0
  99. package/src/components/use_state_array.js +16 -9
  100. package/src/docs/actions.md +22 -0
  101. package/src/notes.md +33 -12
  102. package/src/route/route.js +97 -47
  103. package/src/store/resource_graph.js +2 -1
  104. package/src/store/tests/{resource_graph_dependencies.test.js → resource_graph_dependencies.test_manual.js} +13 -13
  105. package/src/utils/is_signal.js +20 -0
  106. package/src/utils/stringify_for_display.js +4 -23
  107. package/src/validation/constraints/confirm_constraint.js +14 -0
  108. package/src/validation/constraints/create_unique_value_constraint.js +27 -0
  109. package/src/validation/constraints/native_constraints.js +313 -0
  110. package/src/validation/constraints/readonly_constraint.js +36 -0
  111. package/src/validation/constraints/single_space_constraint.js +13 -0
  112. package/src/validation/custom_constraint_validation.js +599 -0
  113. package/src/validation/custom_message.js +18 -0
  114. package/src/validation/demos/browser_style.png +0 -0
  115. package/src/validation/demos/form_validation_demo.html +142 -0
  116. package/src/validation/demos/form_validation_demo_preact.html +87 -0
  117. package/src/validation/demos/form_validation_native_popover_demo.html +168 -0
  118. package/src/validation/demos/form_validation_vs_native_demo.html +172 -0
  119. package/src/validation/demos/validation_message_demo.html +203 -0
  120. package/src/validation/hooks/use_constraints.js +23 -0
  121. package/src/validation/hooks/use_custom_validation_ref.js +73 -0
  122. package/src/validation/hooks/use_validation_message.js +19 -0
  123. package/src/validation/validation_message.js +741 -0
  124. package/src/components/editable_text/editable_text.jsx +0 -96
  125. package/src/components/form.jsx +0 -144
  126. package/src/components/input/checkbox_list.jsx +0 -294
  127. package/src/components/input/field.jsx +0 -61
  128. package/src/components/input/input_textual.jsx +0 -338
  129. package/src/components/input/radio_list.jsx +0 -283
  130. package/src/components/input/use_form_event.js +0 -20
  131. package/src/components/input/use_on_change.js +0 -12
  132. package/src/components/selection/selection.js +0 -5
  133. package/src/components/selection/selection_context.jsx +0 -262
  134. package/src/components/shortcut/shortcut_context.jsx +0 -390
  135. package/src/components/use_action_events.js +0 -37
  136. package/src/utils/iterable_weak_set.js +0 -62
  137. /package/src/components/demos/action/{11_nested_shortcuts_demo.html → 11_nested_shortcuts_demo.xhtml} +0 -0
  138. /package/src/components/{shortcut → keyboard_shortcuts}/os.js +0 -0
  139. /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
+ };