@faqir-ui/core 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +97 -0
- package/cdn.json +28 -0
- package/dist/faqir-core.js +11792 -0
- package/dist/faqir-core.min.js +5 -0
- package/dist/faqir-core.min.js.map +11 -0
- package/dist/faqir-core.mjs +9735 -0
- package/dist/faqir.aurora.css +13527 -0
- package/dist/faqir.brutalist.css +13536 -0
- package/dist/faqir.contrast.css +13569 -0
- package/dist/faqir.default.css +13468 -0
- package/dist/faqir.document-serif.css +13457 -0
- package/dist/faqir.document.css +13466 -0
- package/dist/faqir.glass.css +13576 -0
- package/dist/faqir.midnight.css +13518 -0
- package/dist/faqir.paper.css +13539 -0
- package/dist/faqir.slate.css +13518 -0
- package/dist/faqir.soft.css +13540 -0
- package/dist/faqir.terminal.css +13546 -0
- package/dist/plugins/faqir-collapse.js +110 -0
- package/dist/plugins/faqir-intersect.js +64 -0
- package/dist/plugins/faqir-mask.js +339 -0
- package/dist/plugins/faqir-persist.js +129 -0
- package/dist/plugins/faqir-validate.js +221 -0
- package/dist/sri.json +23 -0
- package/examples/cdn-two-tag.html +49 -0
- package/faqir-core.d.ts +856 -0
- package/package.json +77 -0
- package/src/cdn-entry.js +12 -0
- package/src/esm-entry.js +58 -0
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
// @ui:plugin faqir-validate
|
|
2
|
+
// @ui:provides l-validate
|
|
3
|
+
/**
|
|
4
|
+
* faqir-validate — declarative form validation for the field-group contract. [0.6-02 · §7.1, §A5]
|
|
5
|
+
*
|
|
6
|
+
* <form l-validate>
|
|
7
|
+
* <div data-ui="field-group">
|
|
8
|
+
* <label data-part="label" for="email">Work email</label>
|
|
9
|
+
* <input data-part="input" id="email" name="email" type="email" required
|
|
10
|
+
* l-validate:company="isCompanyEmail(value)"
|
|
11
|
+
* data-error-company="Use your company address.">
|
|
12
|
+
* <p data-part="error"></p>
|
|
13
|
+
* </div>
|
|
14
|
+
* </form>
|
|
15
|
+
*
|
|
16
|
+
* Reflects each control's native `ValidityState` (plus any custom validators)
|
|
17
|
+
* into the enclosing `[data-ui="field-group"]`: sets `data-state="invalid"`,
|
|
18
|
+
* fills the group's `[data-part="error"]` with a message, and wires
|
|
19
|
+
* `aria-invalid` (+ `aria-describedby` to the error part when it has an id).
|
|
20
|
+
* No JavaScript is required from the page author for the full validation UX.
|
|
21
|
+
*
|
|
22
|
+
* ── Revalidation policy (the exact UX contract) ─────────────────────────────
|
|
23
|
+
* • On **submit**: every field is validated. If any is invalid the submit is
|
|
24
|
+
* blocked (`preventDefault`) and focus moves to the first offender. When all
|
|
25
|
+
* are clean the browser submits natively — unless `l-validate` carries an
|
|
26
|
+
* expression, which is then treated as an on-valid hook (SPA submit) and run
|
|
27
|
+
* while the native submit is suppressed.
|
|
28
|
+
* • **After the first submit attempt**, each field additionally revalidates
|
|
29
|
+
* live — on `blur` (error surfaces when you leave it) and on `input` (error
|
|
30
|
+
* clears as you fix it). Before that first attempt, typing is never
|
|
31
|
+
* interrupted with errors. (`blur` doesn't bubble, so both are captured.)
|
|
32
|
+
*
|
|
33
|
+
* ── Custom validators ───────────────────────────────────────────────────────
|
|
34
|
+
* `l-validate:<name>="expr"` on a control runs only after native constraints
|
|
35
|
+
* pass. `expr` is evaluated with the control's current `value` (and `$el`) in
|
|
36
|
+
* scope; a falsy result marks the field invalid. Its message comes from
|
|
37
|
+
* `data-error-<name>`, then `data-error`, then a default. Multiple named
|
|
38
|
+
* validators may coexist on one control; the first to fail wins.
|
|
39
|
+
*
|
|
40
|
+
* ── Message resolution (when a field is invalid) ────────────────────────────
|
|
41
|
+
* custom-validator message → `data-error-<constraint>` → `data-error`
|
|
42
|
+
* → the browser's native `validationMessage` → a built-in default.
|
|
43
|
+
*
|
|
44
|
+
* Escape hatch: `data-validate-ignore` on a control opts it out entirely.
|
|
45
|
+
*
|
|
46
|
+
* Self-registers via `Faqir.plugin` when a global `Faqir` is present (load it
|
|
47
|
+
* after faqir-core) and is also exported for bundlers/tests. Zero dependencies,
|
|
48
|
+
* ≤ 2 KB gzip. The only attributes it owns are `data-state="invalid"` on the
|
|
49
|
+
* group and `aria-invalid`/`aria-describedby` on the control — the frozen
|
|
50
|
+
* five-attribute protocol is untouched.
|
|
51
|
+
*/
|
|
52
|
+
(function () {
|
|
53
|
+
"use strict";
|
|
54
|
+
|
|
55
|
+
// Non-field / non-validatable input types — skipped when scanning a form.
|
|
56
|
+
var SKIP_TYPES = { submit: 1, button: 1, reset: 1, image: 1, hidden: 1 };
|
|
57
|
+
|
|
58
|
+
// ValidityState flag → (author-message attribute suffix, built-in fallback).
|
|
59
|
+
// Checked in order; the first failing flag decides the message.
|
|
60
|
+
var CONSTRAINTS = [
|
|
61
|
+
["valueMissing", "required", "This field is required."],
|
|
62
|
+
["typeMismatch", "type", "Please enter a valid value."],
|
|
63
|
+
["patternMismatch", "pattern", "Please match the requested format."],
|
|
64
|
+
["tooShort", "short", "Please lengthen this value."],
|
|
65
|
+
["tooLong", "long", "Please shorten this value."],
|
|
66
|
+
["rangeUnderflow", "min", "Value is too small."],
|
|
67
|
+
["rangeOverflow", "max", "Value is too large."],
|
|
68
|
+
["stepMismatch", "step", "Please enter a valid value."],
|
|
69
|
+
["badInput", "input", "Please enter a valid value."]
|
|
70
|
+
];
|
|
71
|
+
|
|
72
|
+
function isFormField(el) {
|
|
73
|
+
var tag = el.tagName;
|
|
74
|
+
if (tag !== "INPUT" && tag !== "SELECT" && tag !== "TEXTAREA") return false;
|
|
75
|
+
if (el.disabled) return false;
|
|
76
|
+
if (el.hasAttribute("data-validate-ignore")) return false;
|
|
77
|
+
if (tag === "INPUT" && SKIP_TYPES[el.type]) return false;
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function fields(form) {
|
|
82
|
+
var out = [];
|
|
83
|
+
var all = form.querySelectorAll("input, select, textarea");
|
|
84
|
+
for (var i = 0; i < all.length; i++) if (isFormField(all[i])) out.push(all[i]);
|
|
85
|
+
return out;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Suffixes of the custom `l-validate:<name>` validators declared on a control.
|
|
89
|
+
function customNames(el) {
|
|
90
|
+
var names = [];
|
|
91
|
+
for (var i = 0; i < el.attributes.length; i++) {
|
|
92
|
+
var n = el.attributes[i].name;
|
|
93
|
+
if (n.indexOf("l-validate:") === 0) names.push(n.slice(11));
|
|
94
|
+
}
|
|
95
|
+
return names;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Message shown once a control is invalid — see header for the priority order.
|
|
99
|
+
function messageFor(el) {
|
|
100
|
+
var v = el.validity;
|
|
101
|
+
if (v.customError) return el.validationMessage || "This field is invalid.";
|
|
102
|
+
for (var i = 0; i < CONSTRAINTS.length; i++) {
|
|
103
|
+
if (v[CONSTRAINTS[i][0]]) {
|
|
104
|
+
return (
|
|
105
|
+
el.getAttribute("data-error-" + CONSTRAINTS[i][1]) ||
|
|
106
|
+
el.getAttribute("data-error") ||
|
|
107
|
+
el.validationMessage ||
|
|
108
|
+
CONSTRAINTS[i][2]
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return el.validationMessage || "This field is invalid.";
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function group(el) {
|
|
116
|
+
return el.closest('[data-ui="field-group"]');
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Reflect a control's verdict onto itself + its field-group. The plugin owns
|
|
120
|
+
// the group's `[data-part="error"]` text and toggles `data-state` only between
|
|
121
|
+
// "invalid" and cleared (an author's "disabled"/"validating" state is left be).
|
|
122
|
+
function applyState(el, valid, message) {
|
|
123
|
+
var g = group(el);
|
|
124
|
+
var err = g && g.querySelector('[data-part="error"]');
|
|
125
|
+
if (valid) {
|
|
126
|
+
el.removeAttribute("aria-invalid");
|
|
127
|
+
if (g && g.getAttribute("data-state") === "invalid") g.removeAttribute("data-state");
|
|
128
|
+
if (err) {
|
|
129
|
+
err.textContent = "";
|
|
130
|
+
if (err.id && el.getAttribute("aria-describedby") === err.id)
|
|
131
|
+
el.removeAttribute("aria-describedby");
|
|
132
|
+
}
|
|
133
|
+
} else {
|
|
134
|
+
el.setAttribute("aria-invalid", "true");
|
|
135
|
+
if (g) g.setAttribute("data-state", "invalid");
|
|
136
|
+
if (err) {
|
|
137
|
+
err.textContent = message;
|
|
138
|
+
if (err.id && !el.getAttribute("aria-describedby"))
|
|
139
|
+
el.setAttribute("aria-describedby", err.id);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function install(Faqir) {
|
|
145
|
+
Faqir.directive("validate", function (form, dir, scope) {
|
|
146
|
+
var submitted = false;
|
|
147
|
+
|
|
148
|
+
// Run a control's custom validators (native constraints already passed).
|
|
149
|
+
// `value` and `$el` are exposed so `l-validate:x="fn(value)"` resolves; the
|
|
150
|
+
// first falsy verdict sets a custom validity message and stops.
|
|
151
|
+
function runCustom(el) {
|
|
152
|
+
var names = customNames(el);
|
|
153
|
+
if (!names.length) return;
|
|
154
|
+
var local = Object.create(scope);
|
|
155
|
+
local.value = "value" in el ? el.value : null;
|
|
156
|
+
local.$el = el;
|
|
157
|
+
for (var i = 0; i < names.length; i++) {
|
|
158
|
+
var ok = !!Faqir.evaluate(el.getAttribute("l-validate:" + names[i]), local, el);
|
|
159
|
+
if (!ok) {
|
|
160
|
+
el.setCustomValidity(
|
|
161
|
+
el.getAttribute("data-error-" + names[i]) ||
|
|
162
|
+
el.getAttribute("data-error") ||
|
|
163
|
+
"This field is invalid."
|
|
164
|
+
);
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function validateField(el) {
|
|
171
|
+
el.setCustomValidity(""); // clear any prior custom verdict
|
|
172
|
+
if (el.validity.valid) runCustom(el); // native ok → try custom
|
|
173
|
+
var valid = el.validity.valid;
|
|
174
|
+
applyState(el, valid, valid ? "" : messageFor(el));
|
|
175
|
+
return valid;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Submit: validate everything; block + focus the first offender when dirty.
|
|
179
|
+
form.addEventListener("submit", function (e) {
|
|
180
|
+
submitted = true;
|
|
181
|
+
var list = fields(form),
|
|
182
|
+
firstBad = null;
|
|
183
|
+
for (var i = 0; i < list.length; i++) {
|
|
184
|
+
if (!validateField(list[i]) && !firstBad) firstBad = list[i];
|
|
185
|
+
}
|
|
186
|
+
if (firstBad) {
|
|
187
|
+
e.preventDefault();
|
|
188
|
+
if (typeof firstBad.focus === "function") firstBad.focus();
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
// Clean. A non-empty `l-validate` expression is an on-valid hook (SPA
|
|
192
|
+
// submit): run it and keep the browser from navigating. Otherwise let
|
|
193
|
+
// the native submit proceed.
|
|
194
|
+
if (dir.expression && dir.expression.trim()) {
|
|
195
|
+
e.preventDefault();
|
|
196
|
+
Faqir.evaluateAssignment(dir.expression, scope, form);
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
// After the first submit attempt, revalidate the touched field live.
|
|
201
|
+
function live(e) {
|
|
202
|
+
if (submitted && isFormField(e.target)) validateField(e.target);
|
|
203
|
+
}
|
|
204
|
+
form.addEventListener("blur", live, true);
|
|
205
|
+
form.addEventListener("input", live, true);
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// CommonJS / bundler export (tests import the installer directly).
|
|
210
|
+
if (typeof module !== "undefined" && module.exports) {
|
|
211
|
+
module.exports = install;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Browser self-registration: attach as soon as a global Faqir exists.
|
|
215
|
+
var F =
|
|
216
|
+
(typeof globalThis !== "undefined" && globalThis.Faqir) ||
|
|
217
|
+
(typeof window !== "undefined" && window.Faqir);
|
|
218
|
+
if (F && typeof F.plugin === "function") {
|
|
219
|
+
F.plugin(install);
|
|
220
|
+
}
|
|
221
|
+
})();
|
package/dist/sri.json
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"faqir-core.js": "sha384-vH2gsWr7R/OAJLvYeBC4txN6mleQzdRiuLsc5AH2xbdge5+zQipF5AqSJS7Yc4Vz",
|
|
3
|
+
"faqir-core.min.js": "sha384-BHt1gSursbCylX/U3FT6iW5dWukyZcjDXKWOanw4baYgwn8qj1LIpI1It8ct7CyK",
|
|
4
|
+
"faqir-core.min.js.map": "sha384-io5FkzZ1meZXs3UY/kEJkmvB8YXwO/JfezQe4yZop6KZZjjwn6UCjeDUSh0FG1sz",
|
|
5
|
+
"faqir-core.mjs": "sha384-jqsZ/HqqXSuUjt6SikvDjynm7EdDVAfVLXYSoJHjzvOF+3V5CDmEZgzXDJPWvQ4I",
|
|
6
|
+
"faqir.aurora.css": "sha384-M7yDwTC7MuWfGfiIFLv9MFibCkYDDSHx6RFszEQ7iyd4Lp/bEG/SIyRcvSsfn2Vv",
|
|
7
|
+
"faqir.brutalist.css": "sha384-aRtTqpJetmoMa6uL2krMPEHsjTwF11oNlTzS9zZrdtqXSvzkSUCKKoHyr1WbCBnb",
|
|
8
|
+
"faqir.contrast.css": "sha384-Fy3tZU7cwb7LGNm9B/5nFNK1yRgwAZqv+FF69pY/w6AEGykZA4jSoDeFKU4hXdBf",
|
|
9
|
+
"faqir.default.css": "sha384-w2sH1K7AwYMeLQIhxBnAU1LuRrexg/xJx/eLLMgAjG7avxLYhYeuvM2pTu1yJJVi",
|
|
10
|
+
"faqir.document-serif.css": "sha384-2xWhB1DahXndhbbk7D+RV6w1NI7lPoO3tPgVtDvgWp8G+qE4jjI5nCWZa0IC1XiV",
|
|
11
|
+
"faqir.document.css": "sha384-loA3uWvclgLBd8OntpB4YrQP/jBsLScb06KI61EMauFjYxOadmCszUBkYYNoo4Wc",
|
|
12
|
+
"faqir.glass.css": "sha384-OQOzZy8BxLxPl1pFNkCwizX0oqOsmPf49FKhFa4d9HUM6bcJrJnzw3XU8IZGqSkG",
|
|
13
|
+
"faqir.midnight.css": "sha384-J0SQyAklYwD+yJ7jubtIJ+0t9DWBymcJikhI242PpTvzAL9SU4iWTYCwJBqSUbj0",
|
|
14
|
+
"faqir.paper.css": "sha384-OCmkztWvVBgacdb9HlGiRl+RRQ1QaFU8MCrQIYLeL17EMsT2tG8lZeU0mfQKLo6N",
|
|
15
|
+
"faqir.slate.css": "sha384-BxHSyxsLdi690Q1LBWIyAXodugjLNT764ZjqBRAaCtAaQeei81qzV46C5rMYonb4",
|
|
16
|
+
"faqir.soft.css": "sha384-a77S0Tcfd1I2V+r6Prt4gTtIzSZahltCuQ0jEzOKXUd+taDRjM0EsIy3n5VycLJT",
|
|
17
|
+
"faqir.terminal.css": "sha384-YvP4CkNUhcsFYJjBgnhcSMthsEheRg2RlZ0837GBzDVfmeJx9IYpMSrG7BkAb697",
|
|
18
|
+
"plugins/faqir-collapse.js": "sha384-y16PnFkcJiJLQIxuQMtdS+QalrC6lJ0EsPT/oceZWpLMGdqASXq3HhVHebfUWktm",
|
|
19
|
+
"plugins/faqir-intersect.js": "sha384-AfbB6icyS1ngl1JdiAMZ9//m5lHTMdgXXHWCdi5INfE9FmLbYRUeiyKTv4E4g6ri",
|
|
20
|
+
"plugins/faqir-mask.js": "sha384-cSq2DpUnoJCxEDAQjsbzAa+jSuHf4WsOaKT8t7b9p8pA7MaaRhA1DLzjMNChSa/I",
|
|
21
|
+
"plugins/faqir-persist.js": "sha384-o0jfddebC8QZ3S26VXUlOme723BNMlb/N7vqpn+fWpSeEFXzpq+3MoeXhVL/0PSv",
|
|
22
|
+
"plugins/faqir-validate.js": "sha384-DSzQ7I94Qm+VKjpgBo3qgALyuU3zgQZ+jXOIqgRHkJ6utr8nqF7tQMKWnOv1/YH2"
|
|
23
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en" data-theme="light">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>Faqir UI — two-tag CDN demo</title>
|
|
7
|
+
|
|
8
|
+
<!-- The two tags. Local dist paths (swap for the jsDelivr URLs in production). -->
|
|
9
|
+
<link rel="stylesheet" href="../dist/faqir.default.css">
|
|
10
|
+
<script src="../dist/faqir-core.min.js" defer></script>
|
|
11
|
+
</head>
|
|
12
|
+
<body>
|
|
13
|
+
<main l-data="{ count: 0 }" data-ui="stack" data-gap="lg"
|
|
14
|
+
style="max-width: 40rem; margin: 3rem auto; padding: 0 1rem;">
|
|
15
|
+
|
|
16
|
+
<div data-ui="card">
|
|
17
|
+
<div data-part="header">
|
|
18
|
+
<h1 data-part="title">Faqir UI is running</h1>
|
|
19
|
+
<p data-part="description">
|
|
20
|
+
Styled by <code>faqir.default.css</code>, made interactive by
|
|
21
|
+
<code>faqir-core.min.js</code> — no build step, no install.
|
|
22
|
+
</p>
|
|
23
|
+
</div>
|
|
24
|
+
|
|
25
|
+
<div data-part="body" data-ui="stack" data-gap="md">
|
|
26
|
+
<p>
|
|
27
|
+
Clicked <span data-ui="badge" data-variant="primary" l-text="count">0</span> times.
|
|
28
|
+
</p>
|
|
29
|
+
<div data-ui="button-group">
|
|
30
|
+
<button data-ui="button" data-variant="primary" l-on:click="count++">Increment</button>
|
|
31
|
+
<button data-ui="button" data-variant="outline" l-on:click="count = 0">Reset</button>
|
|
32
|
+
</div>
|
|
33
|
+
<p data-ui="callout" data-variant="success" l-show="count >= 5">
|
|
34
|
+
🎉 Reactivity works — this callout appears once you reach 5.
|
|
35
|
+
</p>
|
|
36
|
+
</div>
|
|
37
|
+
|
|
38
|
+
<div data-part="footer">
|
|
39
|
+
<button data-ui="button" data-variant="ghost"
|
|
40
|
+
l-on:click="document.documentElement.dataset.theme =
|
|
41
|
+
document.documentElement.dataset.theme === 'dark' ? 'light' : 'dark'">
|
|
42
|
+
Toggle dark mode
|
|
43
|
+
</button>
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
|
|
47
|
+
</main>
|
|
48
|
+
</body>
|
|
49
|
+
</html>
|