actual-css 0.1.0 → 0.3.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/CHANGELOG.md +90 -1
- package/README.md +25 -8
- package/dist/actual-themes.min.css +1 -0
- package/dist/actual.css +1492 -931
- package/dist/actual.js +1 -3
- package/dist/actual.min.css +1 -1
- package/dist/optional.css +1048 -0
- package/dist/optional.min.css +1 -0
- package/docs/design-notes/browser-support.md +95 -6
- package/docs/design-notes/cascade-layer.md +1 -1
- package/docs/design-notes/enhancement-contract.md +101 -0
- package/docs/design-notes/forced-colors.md +105 -0
- package/docs/design-notes/naming.md +3 -1
- package/docs/design-notes/platform-alignment.md +492 -0
- package/docs/design-notes/references.md +8 -0
- package/docs/design-notes/widget-primitives.md +119 -0
- package/docs/navigation.json +135 -0
- package/docs/pages/components/accordion.md +80 -0
- package/docs/pages/components/alert.md +203 -0
- package/docs/pages/components/aura.md +35 -0
- package/docs/pages/components/avatar.md +95 -0
- package/docs/pages/components/badge.md +102 -0
- package/docs/pages/components/breadcrumb.md +19 -0
- package/docs/pages/components/button.md +205 -0
- package/docs/pages/components/card.md +183 -0
- package/docs/pages/components/chat.md +50 -0
- package/docs/pages/components/dialog.md +367 -0
- package/docs/pages/components/drawer.md +113 -0
- package/docs/pages/components/fab.md +118 -0
- package/docs/pages/components/key.md +33 -0
- package/docs/pages/components/meter.md +23 -0
- package/docs/pages/components/navbar.md +92 -0
- package/docs/pages/components/pagination.md +55 -0
- package/docs/pages/components/progress.md +29 -0
- package/docs/pages/components/skeleton.md +38 -0
- package/docs/pages/components/spinner.md +93 -0
- package/docs/pages/components/table.md +102 -0
- package/docs/pages/enhancements/flyout.md +391 -0
- package/docs/pages/enhancements/overview.md +36 -0
- package/docs/pages/enhancements/scrollspy.md +71 -0
- package/docs/pages/enhancements/status-bar.md +72 -0
- package/docs/pages/enhancements/tabs.md +80 -0
- package/docs/pages/enhancements/tooltip.md +130 -0
- package/docs/pages/examples/overview.md +41 -0
- package/docs/pages/forms/choice-cards.md +102 -0
- package/docs/pages/forms/controls.md +32 -0
- package/docs/pages/forms/detached-actions.md +59 -0
- package/docs/pages/forms/floating-field.md +88 -0
- package/docs/pages/forms/form-actions.md +56 -0
- package/docs/pages/forms/input-policy.md +91 -0
- package/docs/pages/forms/joined-controls.md +37 -0
- package/docs/pages/forms/labels.md +42 -0
- package/docs/pages/forms/otp.md +50 -0
- package/docs/pages/forms/overview.md +188 -0
- package/docs/pages/forms/password.md +40 -0
- package/docs/pages/forms/range.md +17 -0
- package/docs/pages/forms/states-and-sizes.md +321 -0
- package/docs/pages/forms/sticky-actions.md +187 -0
- package/docs/pages/forms/switch.md +31 -0
- package/docs/pages/forms/validation.md +241 -0
- package/docs/{mental-model.md → pages/foundations/philosophy.md} +33 -18
- package/docs/pages/foundations/theming.md +92 -0
- package/docs/pages/foundations/tokens.md +421 -0
- package/docs/{typography.md → pages/foundations/typography.md} +27 -9
- package/docs/pages/guides/bootstrap.md +438 -0
- package/docs/pages/guides/introduction.md +157 -0
- package/docs/pages/guides/optional-components.md +33 -0
- package/docs/pages/guides/progressive-enhancement.md +686 -0
- package/docs/pages/guides/tailwind.md +455 -0
- package/docs/pages/layout/app-shell.md +25 -0
- package/docs/pages/layout/center.md +29 -0
- package/docs/pages/layout/cluster.md +57 -0
- package/docs/pages/layout/frame.md +30 -0
- package/docs/pages/layout/grid.md +142 -0
- package/docs/pages/layout/list.md +41 -0
- package/docs/pages/layout/media.md +21 -0
- package/docs/pages/layout/overview.md +37 -0
- package/docs/pages/layout/scroll-snap.md +54 -0
- package/docs/pages/layout/sidebar-layout.md +27 -0
- package/docs/pages/layout/stack.md +18 -0
- package/docs/pages/layout/switcher.md +36 -0
- package/docs/pages/layout/topbar.md +23 -0
- package/docs/pages/patterns/actions.md +52 -0
- package/docs/pages/patterns/code-block.md +36 -0
- package/docs/pages/patterns/exclusive-accordion.md +16 -0
- package/docs/pages/patterns/hierarchical-nav.md +26 -0
- package/docs/pages/patterns/icon-input.md +25 -0
- package/docs/pages/patterns/intent-nesting.md +3 -0
- package/docs/pages/patterns/navigation-list.md +235 -0
- package/docs/pages/patterns/overview.md +58 -0
- package/docs/pages/patterns/structured-lists.md +47 -0
- package/docs/pages/utilities/accessibility.md +34 -0
- package/docs/pages/utilities/flex.md +105 -0
- package/docs/pages/utilities/links.md +18 -0
- package/docs/pages/utilities/list.md +31 -0
- package/docs/pages/utilities/overflow.md +43 -0
- package/docs/pages/utilities/overview.md +18 -0
- package/docs/pages/utilities/shape.md +25 -0
- package/docs/pages/utilities/sizing-wrapping.md +115 -0
- package/docs/pages/utilities/spacing.md +79 -0
- package/docs/pages/utilities/text.md +43 -0
- package/llms.txt +10 -4
- package/package.json +30 -16
- package/src/css/actual.css +12 -1
- package/src/css/base.css +3 -9
- package/src/css/components/accordion.css +18 -30
- package/src/css/components/actions.css +4 -1
- package/src/css/components/alert.css +162 -49
- package/src/css/components/avatar.css +16 -10
- package/src/css/components/badge.css +82 -39
- package/src/css/components/breadcrumb.css +7 -15
- package/src/css/components/busy.css +7 -0
- package/src/css/components/button.css +82 -38
- package/src/css/components/card.css +17 -37
- package/src/css/components/drawer.css +30 -13
- package/src/css/components/flyout.css +39 -110
- package/src/css/components/index.css +1 -0
- package/src/css/components/join.css +14 -3
- package/src/css/components/key.css +2 -0
- package/src/css/components/menu.css +125 -0
- package/src/css/components/meter.css +0 -23
- package/src/css/components/modal.css +162 -7
- package/src/css/components/navbar.css +17 -18
- package/src/css/components/overline.css +0 -7
- package/src/css/components/pagination.css +1 -1
- package/src/css/components/progress.css +8 -26
- package/src/css/components/scrollspy.css +2 -2
- package/src/css/components/skeleton.css +7 -0
- package/src/css/components/spinner.css +10 -11
- package/src/css/components/status-bar.css +18 -28
- package/src/css/components/surface.css +2 -8
- package/src/css/components/tab.css +20 -48
- package/src/css/components/table.css +7 -5
- package/src/css/components/tooltip.css +14 -7
- package/src/css/focus.css +16 -3
- package/src/css/forms/choice-card.css +13 -15
- package/src/css/forms/choice.css +21 -28
- package/src/css/forms/control.css +16 -26
- package/src/css/forms/custom-select.css +9 -11
- package/src/css/forms/form-actions.css +14 -3
- package/src/css/forms/form.css +7 -4
- package/src/css/forms/index.css +5 -4
- package/src/css/forms/input-icon.css +30 -12
- package/src/css/forms/native.css +48 -33
- package/src/css/forms/range.css +103 -0
- package/src/css/forms/select.css +8 -8
- package/src/css/forms/switch.css +14 -13
- package/src/css/forms/validation.css +32 -5
- package/src/css/grid.css +19 -18
- package/src/css/intents.css +2 -2
- package/src/css/layout.css +29 -1
- package/src/css/optional/aura.css +93 -0
- package/src/css/optional/chat.css +105 -0
- package/src/css/optional/fab.css +104 -0
- package/src/css/optional/floating-field.css +87 -0
- package/src/css/optional/index.css +5 -0
- package/src/css/optional/layout-extra.css +7 -4
- package/src/css/optional/otp.css +115 -0
- package/src/css/optional/scroll-snap.css +6 -0
- package/src/css/optional/scroller.css +19 -7
- package/src/css/optional/typography-fluid.css +5 -0
- package/src/css/optional/utilities-extra.css +17 -2
- package/src/css/prose.css +18 -8
- package/src/css/reset.css +0 -10
- package/src/css/theme.css +29 -13
- package/src/css/themes/cyberpunk.css +1 -1
- package/src/css/themes/dim.css +3 -3
- package/src/css/themes/edge.css +11 -37
- package/src/css/themes/index.css +3 -3
- package/src/css/themes/{dark.css → indigo.css} +5 -4
- package/src/css/themes/neon.css +5 -5
- package/src/css/themes/square.css +0 -3
- package/src/css/tokens.css +81 -21
- package/src/css/utilities.css +8 -10
- package/src/css/variants.css +37 -65
- package/src/js/command.js +1 -3
- package/src/js/context-menu.js +72 -53
- package/src/js/dialog.js +68 -79
- package/src/js/dismiss.js +15 -0
- package/src/js/enhance.js +76 -19
- package/src/js/enhancement-loader.js +301 -0
- package/src/js/events.js +3 -0
- package/src/js/filter.js +3 -3
- package/src/js/floating.js +101 -131
- package/src/js/flyout.js +106 -96
- package/src/js/focus-group.js +116 -0
- package/src/js/focus.js +6 -1
- package/src/js/index.js +4 -2
- package/src/js/input.js +1 -1
- package/src/js/keys.js +27 -0
- package/src/js/mask.js +7 -6
- package/src/js/menu.js +89 -35
- package/src/js/parse-config.js +47 -0
- package/src/js/password.js +9 -1
- package/src/js/scrollspy.js +133 -62
- package/src/js/selectors.js +14 -31
- package/src/js/status.js +16 -14
- package/src/js/surface.js +219 -125
- package/src/js/tab.js +46 -66
- package/src/js/tooltip.js +53 -31
- package/src/js/validation.js +45 -51
- package/dist/actual.js.map +0 -31
- package/docs/README.md +0 -26
- package/docs/components.md +0 -860
- package/docs/design-notes/previous-work.md +0 -5
- package/docs/forms.md +0 -1265
- package/docs/javascript.md +0 -426
- package/docs/layout.md +0 -480
- package/docs/patterns.md +0 -415
- package/docs/reserved.md +0 -7
- package/docs/tokens.md +0 -455
- package/docs/ui.md +0 -937
- package/docs/utilities.md +0 -384
- package/src/css/dialog-fallback.css +0 -56
- package/src/js/dialog-fallback.js +0 -163
package/src/js/tooltip.js
CHANGED
|
@@ -28,8 +28,7 @@
|
|
|
28
28
|
*/
|
|
29
29
|
|
|
30
30
|
import enhance from "./enhance.js";
|
|
31
|
-
import {
|
|
32
|
-
import { reposition, track } from "./floating.js";
|
|
31
|
+
import { autoUpdate, reposition } from "./floating.js";
|
|
33
32
|
import { CLASSES } from "./selectors.js";
|
|
34
33
|
|
|
35
34
|
const SHOW_DELAY_MS = 150;
|
|
@@ -40,12 +39,13 @@ const HIDE_DELAY_MS = 100;
|
|
|
40
39
|
* @property {Set<Element>} refs Triggers that currently reference this tip.
|
|
41
40
|
* @property {Element | null} activeRef Trigger that last showed this tip.
|
|
42
41
|
* @property {AbortController} controller Tip-level event listener cleanup.
|
|
43
|
-
* @property {() => void}
|
|
42
|
+
* @property {() => void} stopTracking Floating-position cleanup.
|
|
44
43
|
* @property {boolean} generated True when the tip was created from data-tooltip text.
|
|
45
44
|
* @property {{ parent: Node, next: ChildNode | null } | null} mount Original DOM position for explicit tips moved into a dialog/body root.
|
|
46
45
|
* @property {ReturnType<typeof setTimeout> | null} timer Pending delayed show.
|
|
47
46
|
* @property {ReturnType<typeof setTimeout> | null} hideTimer Pending delayed hide.
|
|
48
|
-
* @property {boolean}
|
|
47
|
+
* @property {boolean} hovered Pointer is over the active trigger.
|
|
48
|
+
* @property {boolean} focused The active trigger has keyboard focus.
|
|
49
49
|
* @property {boolean} overTip Pointer is over the tooltip.
|
|
50
50
|
*/
|
|
51
51
|
|
|
@@ -132,7 +132,7 @@ function nextTooltipId(doc) {
|
|
|
132
132
|
}
|
|
133
133
|
|
|
134
134
|
function repositionTip(ref, tip) {
|
|
135
|
-
reposition(ref, tip, {
|
|
135
|
+
return reposition(ref, tip, {
|
|
136
136
|
placement: placementFor(ref),
|
|
137
137
|
distance: 6,
|
|
138
138
|
flip: true,
|
|
@@ -165,11 +165,11 @@ function scheduleHide(tip) {
|
|
|
165
165
|
const state = tipStates.get(tip);
|
|
166
166
|
if (!state) return;
|
|
167
167
|
if (isClickTrigger(state.activeRef) || isAlwaysVisible(state.activeRef)) return;
|
|
168
|
-
if (state.
|
|
168
|
+
if (state.hovered || state.focused || state.overTip) return;
|
|
169
169
|
if (state.hideTimer) clearTimeout(state.hideTimer);
|
|
170
170
|
state.hideTimer = setTimeout(() => {
|
|
171
171
|
state.hideTimer = null;
|
|
172
|
-
if (!state.
|
|
172
|
+
if (!state.hovered && !state.focused && !state.overTip) hideTip(tip);
|
|
173
173
|
}, HIDE_DELAY_MS);
|
|
174
174
|
}
|
|
175
175
|
|
|
@@ -185,25 +185,21 @@ function wireTip(tip, options = {}) {
|
|
|
185
185
|
refs: new Set(),
|
|
186
186
|
activeRef: null,
|
|
187
187
|
controller,
|
|
188
|
-
|
|
188
|
+
stopTracking: autoUpdate(tip, () => {
|
|
189
|
+
if (isAlwaysVisible(state.activeRef)) tip.hidden = false;
|
|
190
|
+
if (!tip.hidden && state.activeRef) {
|
|
191
|
+
if (!repositionTip(state.activeRef, tip)) hideTip(tip, true);
|
|
192
|
+
}
|
|
193
|
+
}),
|
|
189
194
|
generated: options.generated === true,
|
|
190
195
|
mount: options.mount || null,
|
|
191
196
|
timer: null,
|
|
192
197
|
hideTimer: null,
|
|
193
|
-
|
|
198
|
+
hovered: false,
|
|
199
|
+
focused: false,
|
|
194
200
|
overTip: false,
|
|
195
201
|
};
|
|
196
202
|
|
|
197
|
-
const onHide = () => hideTip(tip);
|
|
198
|
-
const onOutOfView = () => hideTip(tip, true);
|
|
199
|
-
const onReposition = () => {
|
|
200
|
-
if (isAlwaysVisible(state.activeRef)) tip.hidden = false;
|
|
201
|
-
if (!tip.hidden && state.activeRef) repositionTip(state.activeRef, tip);
|
|
202
|
-
};
|
|
203
|
-
|
|
204
|
-
tip.addEventListener(EVENTS.reposition, onReposition, { signal: controller.signal });
|
|
205
|
-
tip.addEventListener(EVENTS.hide, onHide, { signal: controller.signal });
|
|
206
|
-
tip.addEventListener(EVENTS.outOfView, onOutOfView, { signal: controller.signal });
|
|
207
203
|
tip.addEventListener(
|
|
208
204
|
"mouseenter",
|
|
209
205
|
() => {
|
|
@@ -221,6 +217,17 @@ function wireTip(tip, options = {}) {
|
|
|
221
217
|
{ signal: controller.signal },
|
|
222
218
|
);
|
|
223
219
|
|
|
220
|
+
tip.ownerDocument.addEventListener(
|
|
221
|
+
"keydown",
|
|
222
|
+
(event) => {
|
|
223
|
+
if (event.key !== "Escape" || event.ctrlKey || event.altKey || event.shiftKey) return;
|
|
224
|
+
if (tip.hidden) return;
|
|
225
|
+
event.preventDefault();
|
|
226
|
+
hideTip(tip);
|
|
227
|
+
},
|
|
228
|
+
{ signal: controller.signal },
|
|
229
|
+
);
|
|
230
|
+
|
|
224
231
|
tipStates.set(tip, state);
|
|
225
232
|
return state;
|
|
226
233
|
}
|
|
@@ -256,10 +263,11 @@ function ensureTip(trigger) {
|
|
|
256
263
|
if (!tip) {
|
|
257
264
|
tip = explicitTipFor(trigger);
|
|
258
265
|
if (!tip) return null;
|
|
259
|
-
tip.
|
|
260
|
-
|
|
261
|
-
|
|
266
|
+
// Wire and mount the shared tip only on first retain. A later trigger
|
|
267
|
+
// must not re-hide a tooltip that is currently visible for another.
|
|
262
268
|
if (!tipStates.has(tip)) {
|
|
269
|
+
tip.hidden = true;
|
|
270
|
+
tip.style.position = "fixed";
|
|
263
271
|
mount = mountTip(tip, trigger);
|
|
264
272
|
}
|
|
265
273
|
}
|
|
@@ -268,12 +276,22 @@ function ensureTip(trigger) {
|
|
|
268
276
|
state.refs.add(trigger);
|
|
269
277
|
|
|
270
278
|
const triggerController = new AbortController();
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
279
|
+
trigger.addEventListener(
|
|
280
|
+
"mouseleave",
|
|
281
|
+
() => {
|
|
282
|
+
state.hovered = false;
|
|
283
|
+
scheduleHide(tip);
|
|
284
|
+
},
|
|
285
|
+
{ signal: triggerController.signal },
|
|
286
|
+
);
|
|
287
|
+
trigger.addEventListener(
|
|
288
|
+
"blur",
|
|
289
|
+
() => {
|
|
290
|
+
state.focused = false;
|
|
291
|
+
scheduleHide(tip);
|
|
292
|
+
},
|
|
293
|
+
{ signal: triggerController.signal },
|
|
294
|
+
);
|
|
277
295
|
|
|
278
296
|
const cleanup = () => {
|
|
279
297
|
triggerController.abort();
|
|
@@ -290,7 +308,7 @@ function ensureTip(trigger) {
|
|
|
290
308
|
|
|
291
309
|
hideTip(tip, true);
|
|
292
310
|
state.controller.abort();
|
|
293
|
-
state.
|
|
311
|
+
state.stopTracking();
|
|
294
312
|
tipStates.delete(tip);
|
|
295
313
|
if (state.generated) tip.remove();
|
|
296
314
|
else restoreTip(tip, state);
|
|
@@ -310,7 +328,6 @@ function show(tip, ref, immediate = false) {
|
|
|
310
328
|
if (!state) return;
|
|
311
329
|
|
|
312
330
|
state.activeRef = ref;
|
|
313
|
-
state.overRef = true;
|
|
314
331
|
clearHide(tip);
|
|
315
332
|
if (state.timer) clearTimeout(state.timer);
|
|
316
333
|
const reveal = () => {
|
|
@@ -339,7 +356,12 @@ function handleTriggerIntent(e) {
|
|
|
339
356
|
return;
|
|
340
357
|
|
|
341
358
|
const tip = ensureTip(trigger);
|
|
342
|
-
if (tip)
|
|
359
|
+
if (!tip) return;
|
|
360
|
+
|
|
361
|
+
const state = tipStates.get(tip);
|
|
362
|
+
if (e.type === "focusin") state.focused = true;
|
|
363
|
+
else state.hovered = true;
|
|
364
|
+
show(tip, trigger);
|
|
343
365
|
}
|
|
344
366
|
|
|
345
367
|
function handleTriggerClick(e) {
|
package/src/js/validation.js
CHANGED
|
@@ -8,27 +8,22 @@
|
|
|
8
8
|
* framework: server / AJAX validation lives in app code and feeds results
|
|
9
9
|
* back through FormValidator.setErrors().
|
|
10
10
|
*
|
|
11
|
-
* Opt in with
|
|
12
|
-
*
|
|
11
|
+
* Opt in with data-enhance="validation". The .needs-validation class
|
|
12
|
+
* remains valid for presentation-only (CSS :user-invalid feedback without
|
|
13
|
+
* the JS behavior). Importing the module registers the behavior; there is
|
|
14
|
+
* no init call.
|
|
13
15
|
*
|
|
14
|
-
* The
|
|
15
|
-
*
|
|
16
|
-
* only takes effect on the next focusout/input event, not immediately. A
|
|
17
|
-
* submit that races that window can still bypass native validation (the
|
|
18
|
-
* managed novalidate attribute is still present). Treat the class as fixed
|
|
19
|
-
* for a form's lifetime rather than something toggled at runtime. Removing
|
|
20
|
-
* the form from the DOM, however, is fully supported: the enhance() sweep
|
|
21
|
-
* runs the cleanup below, which aborts all listeners, removes the managed
|
|
22
|
-
* novalidate attribute, and releases the form's internal state.
|
|
16
|
+
* The marker is a static, init-time contract. Removing the form from the
|
|
17
|
+
* DOM tears down everything, including the managed novalidate attribute.
|
|
23
18
|
*/
|
|
24
19
|
|
|
25
|
-
import enhance from "./enhance.js";
|
|
20
|
+
import enhance, { enhancementSelector } from "./enhance.js";
|
|
26
21
|
import { EVENTS } from "./events.js";
|
|
27
22
|
import { CLASSES } from "./selectors.js";
|
|
28
23
|
|
|
29
24
|
const NOVALIDATE = "novalidate";
|
|
30
25
|
const WAS_VALIDATED_CLASS = CLASSES.wasValidated;
|
|
31
|
-
const
|
|
26
|
+
const VALIDATION_SELECTOR = enhancementSelector("validation");
|
|
32
27
|
const FIELD_CLASS = CLASSES.field;
|
|
33
28
|
const DANGER_CLASS = CLASSES.danger;
|
|
34
29
|
const MANAGED_NOVALIDATE_ATTR = "validationManagedNovalidate";
|
|
@@ -79,8 +74,11 @@ const rules = {
|
|
|
79
74
|
return false;
|
|
80
75
|
}
|
|
81
76
|
},
|
|
77
|
+
// A user-entered decimal: optional sign, digits with optional fraction,
|
|
78
|
+
// optional exponent. Rejects JS-isms that Number() would accept (0x10,
|
|
79
|
+
// Infinity, scientific shorthand, whitespace).
|
|
82
80
|
number(v) {
|
|
83
|
-
return
|
|
81
|
+
return /^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?$/.test(v);
|
|
84
82
|
},
|
|
85
83
|
digits(v) {
|
|
86
84
|
return /^\d+$/.test(v);
|
|
@@ -111,18 +109,28 @@ function fieldContainer(el) {
|
|
|
111
109
|
return el.closest?.(`.${FIELD_CLASS}`) || null;
|
|
112
110
|
}
|
|
113
111
|
|
|
114
|
-
// .field.danger
|
|
115
|
-
//
|
|
116
|
-
//
|
|
117
|
-
//
|
|
112
|
+
// .field.danger added by validation is tracked in a WeakSet so that only
|
|
113
|
+
// validation-owned state is removed. A .danger applied manually by app code
|
|
114
|
+
// for an unrelated reason is left alone: validation claims ownership only
|
|
115
|
+
// when it introduces the class itself.
|
|
116
|
+
const dangerOwned = new WeakSet();
|
|
117
|
+
|
|
118
|
+
function markDangerOwned(field) {
|
|
119
|
+
if (!field.classList.contains(DANGER_CLASS)) {
|
|
120
|
+
dangerOwned.add(field);
|
|
121
|
+
}
|
|
122
|
+
field.classList.add(DANGER_CLASS);
|
|
123
|
+
}
|
|
124
|
+
|
|
118
125
|
function syncFieldDangerState(el) {
|
|
119
126
|
const field = fieldContainer(el);
|
|
120
127
|
if (!field) return;
|
|
121
128
|
|
|
122
129
|
if (field.querySelector('[aria-invalid="true"], [data-validation-errors]')) {
|
|
123
|
-
field
|
|
124
|
-
} else {
|
|
130
|
+
markDangerOwned(field);
|
|
131
|
+
} else if (dangerOwned.has(field)) {
|
|
125
132
|
field.classList.remove(DANGER_CLASS);
|
|
133
|
+
dangerOwned.delete(field);
|
|
126
134
|
}
|
|
127
135
|
}
|
|
128
136
|
|
|
@@ -177,7 +185,7 @@ function errorEl(el) {
|
|
|
177
185
|
if (!ids) return null;
|
|
178
186
|
for (const id of ids.split(/\s+/)) {
|
|
179
187
|
const node = el.ownerDocument.getElementById(id);
|
|
180
|
-
if (node?.
|
|
188
|
+
if (node?.matches(`.${CLASSES.fieldError}, [data-field-error]`)) {
|
|
181
189
|
return node;
|
|
182
190
|
}
|
|
183
191
|
}
|
|
@@ -186,7 +194,10 @@ function errorEl(el) {
|
|
|
186
194
|
|
|
187
195
|
function markInvalid(el) {
|
|
188
196
|
el.setAttribute("aria-invalid", "true");
|
|
189
|
-
fieldContainer(el)
|
|
197
|
+
const field = fieldContainer(el);
|
|
198
|
+
if (field) {
|
|
199
|
+
markDangerOwned(field);
|
|
200
|
+
}
|
|
190
201
|
}
|
|
191
202
|
|
|
192
203
|
function markValid(el) {
|
|
@@ -205,14 +216,6 @@ function hydrateServerErrors(form) {
|
|
|
205
216
|
}
|
|
206
217
|
}
|
|
207
218
|
|
|
208
|
-
function ensureManagedNoValidate(form) {
|
|
209
|
-
if (!form.classList.contains(NEEDS_VALIDATION_CLASS)) return;
|
|
210
|
-
if (form.hasAttribute(NOVALIDATE)) return;
|
|
211
|
-
|
|
212
|
-
form.setAttribute(NOVALIDATE, "");
|
|
213
|
-
form.dataset[MANAGED_NOVALIDATE_ATTR] = "";
|
|
214
|
-
}
|
|
215
|
-
|
|
216
219
|
function releaseManagedNoValidate(form) {
|
|
217
220
|
if (form.dataset[MANAGED_NOVALIDATE_ATTR] === undefined) return;
|
|
218
221
|
form.removeAttribute(NOVALIDATE);
|
|
@@ -220,8 +223,6 @@ function releaseManagedNoValidate(form) {
|
|
|
220
223
|
}
|
|
221
224
|
|
|
222
225
|
function validateField(el, trigger) {
|
|
223
|
-
const form = el.form;
|
|
224
|
-
if (!form?.classList.contains(NEEDS_VALIDATION_CLASS)) return;
|
|
225
226
|
if (ignoreField(el)) return;
|
|
226
227
|
|
|
227
228
|
if (trigger === "input" && el.dataset.validationErrors === "server") {
|
|
@@ -261,11 +262,6 @@ function connectForm(form) {
|
|
|
261
262
|
(event) => {
|
|
262
263
|
const el = event.target;
|
|
263
264
|
if (el instanceof Element && el.form === form) {
|
|
264
|
-
if (!form.classList.contains(NEEDS_VALIDATION_CLASS)) {
|
|
265
|
-
releaseManagedNoValidate(form);
|
|
266
|
-
return;
|
|
267
|
-
}
|
|
268
|
-
ensureManagedNoValidate(form);
|
|
269
265
|
validateField(el, "blur");
|
|
270
266
|
}
|
|
271
267
|
},
|
|
@@ -277,11 +273,6 @@ function connectForm(form) {
|
|
|
277
273
|
(event) => {
|
|
278
274
|
const el = event.target;
|
|
279
275
|
if (el instanceof Element && el.form === form) {
|
|
280
|
-
if (!form.classList.contains(NEEDS_VALIDATION_CLASS)) {
|
|
281
|
-
releaseManagedNoValidate(form);
|
|
282
|
-
return;
|
|
283
|
-
}
|
|
284
|
-
ensureManagedNoValidate(form);
|
|
285
276
|
validateField(el, "input");
|
|
286
277
|
}
|
|
287
278
|
},
|
|
@@ -306,13 +297,6 @@ function connectForm(form) {
|
|
|
306
297
|
(event) => {
|
|
307
298
|
if (event.submitter?.formNoValidate) return;
|
|
308
299
|
|
|
309
|
-
if (!form.classList.contains(NEEDS_VALIDATION_CLASS)) {
|
|
310
|
-
releaseManagedNoValidate(form);
|
|
311
|
-
return;
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
ensureManagedNoValidate(form);
|
|
315
|
-
|
|
316
300
|
let firstInvalid = null;
|
|
317
301
|
|
|
318
302
|
for (const el of form.elements) {
|
|
@@ -354,6 +338,10 @@ function connectForm(form) {
|
|
|
354
338
|
{ signal: controller.signal },
|
|
355
339
|
);
|
|
356
340
|
|
|
341
|
+
// Disconnect contract: release resources only. Validation-owned classes,
|
|
342
|
+
// aria-invalid, custom validity, and rendered errors are intentionally left
|
|
343
|
+
// in place, so a move/remove/reinsert of the form does not erase business
|
|
344
|
+
// state. A full reset goes through the reset event, not teardown.
|
|
357
345
|
const cleanup = () => {
|
|
358
346
|
controller.abort();
|
|
359
347
|
releaseManagedNoValidate(form);
|
|
@@ -370,10 +358,16 @@ export class FormValidator {
|
|
|
370
358
|
}
|
|
371
359
|
|
|
372
360
|
static registerRule(name, callback) {
|
|
361
|
+
if (typeof name !== "string" || name.trim() === "") {
|
|
362
|
+
throw new TypeError("registerRule() requires a non-empty rule name.");
|
|
363
|
+
}
|
|
364
|
+
if (typeof callback !== "function") {
|
|
365
|
+
throw new TypeError(`registerRule("${name}") requires a function callback.`);
|
|
366
|
+
}
|
|
373
367
|
rules[name] = callback;
|
|
374
368
|
}
|
|
375
369
|
|
|
376
|
-
static init(selector =
|
|
370
|
+
static init(selector = VALIDATION_SELECTOR) {
|
|
377
371
|
if (typeof document === "undefined") return;
|
|
378
372
|
for (const form of document.querySelectorAll(selector)) {
|
|
379
373
|
if (isFormElement(form)) connectForm(form);
|
|
@@ -412,7 +406,7 @@ export class FormValidator {
|
|
|
412
406
|
|
|
413
407
|
if (typeof document !== "undefined") {
|
|
414
408
|
enhance({
|
|
415
|
-
[`form
|
|
409
|
+
[`form${VALIDATION_SELECTOR}`]: (form) => connectForm(form),
|
|
416
410
|
});
|
|
417
411
|
}
|
|
418
412
|
|