@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,110 @@
|
|
|
1
|
+
// @ui:plugin faqir-collapse
|
|
2
|
+
// @ui:provides l-collapse
|
|
3
|
+
/**
|
|
4
|
+
* faqir-collapse — height auto-animation for a boolean expression. [0.4-11 · §A5]
|
|
5
|
+
*
|
|
6
|
+
* <div l-collapse="open"> … </div>
|
|
7
|
+
*
|
|
8
|
+
* When the bound expression is truthy the element animates from 0 → its natural
|
|
9
|
+
* height and settles at `height: auto`; when falsy it animates back to 0 and is
|
|
10
|
+
* hidden. Honors `prefers-reduced-motion` (snaps, no animation) and leaves NO
|
|
11
|
+
* inline `height` residue in either resting state — so content stays responsive.
|
|
12
|
+
*
|
|
13
|
+
* Self-registers via `Faqir.plugin` when a global `Faqir` is present (load it
|
|
14
|
+
* after faqir-core), and is also exported for bundlers/tests. Zero dependencies,
|
|
15
|
+
* ≤ 2 KB gzip. State it manipulates is inline style only — the DOM contract's
|
|
16
|
+
* `data-*` attributes are untouched.
|
|
17
|
+
*/
|
|
18
|
+
(function () {
|
|
19
|
+
"use strict";
|
|
20
|
+
|
|
21
|
+
function prefersReducedMotion() {
|
|
22
|
+
return (
|
|
23
|
+
typeof window !== "undefined" &&
|
|
24
|
+
typeof window.matchMedia === "function" &&
|
|
25
|
+
window.matchMedia("(prefers-reduced-motion: reduce)").matches
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Settle on transitionend, or a timeout fallback so we never hang when the
|
|
30
|
+
// height does not actually change or the event is missed.
|
|
31
|
+
function whenSettled(el, cb) {
|
|
32
|
+
var done = false;
|
|
33
|
+
function finish(e) {
|
|
34
|
+
if (e && e.target !== el) return;
|
|
35
|
+
if (done) return;
|
|
36
|
+
done = true;
|
|
37
|
+
clearTimeout(timer);
|
|
38
|
+
el.removeEventListener("transitionend", finish);
|
|
39
|
+
cb();
|
|
40
|
+
}
|
|
41
|
+
el.addEventListener("transitionend", finish);
|
|
42
|
+
var timer = setTimeout(finish, 400);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Reset every inline property this plugin owns → no height/overflow/transition
|
|
46
|
+
// residue in a resting state.
|
|
47
|
+
function clearInline(el) {
|
|
48
|
+
el.style.height = "";
|
|
49
|
+
el.style.overflow = "";
|
|
50
|
+
el.style.transition = "";
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function install(Faqir) {
|
|
54
|
+
Faqir.directive("collapse", function (el, dir, scope) {
|
|
55
|
+
var open = null; // unknown until the first effect run
|
|
56
|
+
|
|
57
|
+
Faqir.effect(function () {
|
|
58
|
+
var next = !!Faqir.evaluate(dir.expression, scope, el);
|
|
59
|
+
if (next === open) return;
|
|
60
|
+
var firstRun = open === null;
|
|
61
|
+
open = next;
|
|
62
|
+
|
|
63
|
+
// First paint or reduced motion: snap to the resting state, no animation.
|
|
64
|
+
if (firstRun || prefersReducedMotion()) {
|
|
65
|
+
clearInline(el);
|
|
66
|
+
el.style.display = next ? "" : "none";
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
el.style.overflow = "hidden";
|
|
71
|
+
el.style.transition =
|
|
72
|
+
"height var(--motion-enter-duration, var(--duration-normal, 200ms)) " +
|
|
73
|
+
"var(--motion-enter-ease, var(--ease-out, ease))";
|
|
74
|
+
|
|
75
|
+
if (next) {
|
|
76
|
+
// opening: 0 → scrollHeight → auto
|
|
77
|
+
el.style.display = "";
|
|
78
|
+
el.style.height = "0px";
|
|
79
|
+
void el.offsetHeight; // reflow so the 0 start commits
|
|
80
|
+
el.style.height = el.scrollHeight + "px";
|
|
81
|
+
whenSettled(el, function () {
|
|
82
|
+
clearInline(el); // height: auto, no residue
|
|
83
|
+
});
|
|
84
|
+
} else {
|
|
85
|
+
// closing: current height → 0 → hidden
|
|
86
|
+
el.style.height = el.scrollHeight + "px";
|
|
87
|
+
void el.offsetHeight; // reflow so the explicit start commits
|
|
88
|
+
el.style.height = "0px";
|
|
89
|
+
whenSettled(el, function () {
|
|
90
|
+
clearInline(el);
|
|
91
|
+
el.style.display = "none";
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// CommonJS / bundler export (tests import the installer directly).
|
|
99
|
+
if (typeof module !== "undefined" && module.exports) {
|
|
100
|
+
module.exports = install;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Browser self-registration: attach as soon as a global Faqir exists.
|
|
104
|
+
var F =
|
|
105
|
+
(typeof globalThis !== "undefined" && globalThis.Faqir) ||
|
|
106
|
+
(typeof window !== "undefined" && window.Faqir);
|
|
107
|
+
if (F && typeof F.plugin === "function") {
|
|
108
|
+
F.plugin(install);
|
|
109
|
+
}
|
|
110
|
+
})();
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// @ui:plugin faqir-intersect
|
|
2
|
+
// @ui:provides l-intersect
|
|
3
|
+
/**
|
|
4
|
+
* faqir-intersect — declarative IntersectionObserver hooks. [0.6-05 · §A5]
|
|
5
|
+
*
|
|
6
|
+
* <section l-intersect="visible = true"
|
|
7
|
+
* l-intersect.leave="visible = false"></section>
|
|
8
|
+
* <div l-intersect.once="loadMore()"></div>
|
|
9
|
+
*
|
|
10
|
+
* The base directive runs on enter; `.leave` runs when the element leaves;
|
|
11
|
+
* `.once` disconnects after its first matching observation. The active entry is
|
|
12
|
+
* available to expressions as `$intersection`. Returning the observer cleanup
|
|
13
|
+
* to faqir-core makes scope teardown disconnect it automatically.
|
|
14
|
+
* Self-registering, zero dependencies, and ≤ 2 KB gzip.
|
|
15
|
+
*/
|
|
16
|
+
(function () {
|
|
17
|
+
"use strict";
|
|
18
|
+
|
|
19
|
+
function install(Faqir) {
|
|
20
|
+
Faqir.directive("intersect", function (el, dir, scope) {
|
|
21
|
+
var IO =
|
|
22
|
+
(typeof globalThis !== "undefined" && globalThis.IntersectionObserver) ||
|
|
23
|
+
(typeof window !== "undefined" && window.IntersectionObserver);
|
|
24
|
+
if (typeof IO !== "function") return;
|
|
25
|
+
|
|
26
|
+
var leave = dir.modifiers.indexOf("leave") >= 0;
|
|
27
|
+
var once = dir.modifiers.indexOf("once") >= 0;
|
|
28
|
+
var stopped = false;
|
|
29
|
+
var observer = new IO(function (entries) {
|
|
30
|
+
for (var i = 0; i < entries.length && !stopped; i++) {
|
|
31
|
+
var entry = entries[i];
|
|
32
|
+
var matches = leave ? !entry.isIntersecting : entry.isIntersecting;
|
|
33
|
+
if (!matches) continue;
|
|
34
|
+
|
|
35
|
+
scope.$intersection = entry;
|
|
36
|
+
try {
|
|
37
|
+
Faqir.evaluateAssignment(dir.expression, scope, el);
|
|
38
|
+
} finally {
|
|
39
|
+
delete scope.$intersection;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (once) {
|
|
43
|
+
stopped = true;
|
|
44
|
+
observer.disconnect();
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
observer.observe(el);
|
|
50
|
+
return function () {
|
|
51
|
+
if (stopped) return;
|
|
52
|
+
stopped = true;
|
|
53
|
+
observer.disconnect();
|
|
54
|
+
};
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (typeof module !== "undefined" && module.exports) module.exports = install;
|
|
59
|
+
|
|
60
|
+
var F =
|
|
61
|
+
(typeof globalThis !== "undefined" && globalThis.Faqir) ||
|
|
62
|
+
(typeof window !== "undefined" && window.Faqir);
|
|
63
|
+
if (F && typeof F.plugin === "function") F.plugin(install);
|
|
64
|
+
})();
|
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
// @ui:plugin faqir-mask
|
|
2
|
+
// @ui:provides l-mask
|
|
3
|
+
/**
|
|
4
|
+
* faqir-mask — caret-safe input masking. [0.6-06 · §A5]
|
|
5
|
+
*
|
|
6
|
+
* <input l-model="phone" l-mask="(999) 999-9999">
|
|
7
|
+
*
|
|
8
|
+
* Pattern tokens are `9` (digit), `a` (ASCII letter), and `*` (any character);
|
|
9
|
+
* every other character is a literal inserted by the formatter. The displayed
|
|
10
|
+
* input value is masked while `l-model` receives the raw token characters.
|
|
11
|
+
* `faqir:mask` bubbles after edits with `{ raw, value, caret }` for controllers
|
|
12
|
+
* such as input-otp. The pure `maskEdit()` function owns all edit/caret math;
|
|
13
|
+
* this directive is only a thin beforeinput/paste/model bridge.
|
|
14
|
+
*
|
|
15
|
+
* Self-registering, zero dependencies, and ≤ 2 KB gzip.
|
|
16
|
+
*/
|
|
17
|
+
(function () {
|
|
18
|
+
"use strict";
|
|
19
|
+
|
|
20
|
+
var TOKENS = { "9": /[0-9]/, a: /[A-Za-z]/, "*": /[^]/ };
|
|
21
|
+
|
|
22
|
+
function isToken(ch) {
|
|
23
|
+
return Object.prototype.hasOwnProperty.call(TOKENS, ch);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function tokenTypes(mask) {
|
|
27
|
+
var out = [];
|
|
28
|
+
for (var i = 0; i < mask.length; i++) if (isToken(mask[i])) out.push(mask[i]);
|
|
29
|
+
return out;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function accepts(token, ch) {
|
|
33
|
+
return !!ch && TOKENS[token].test(ch);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Extract token characters from either raw or already-formatted input. */
|
|
37
|
+
function readRaw(mask, value, startSlot) {
|
|
38
|
+
var types = tokenTypes(mask);
|
|
39
|
+
var slot = startSlot || 0;
|
|
40
|
+
var raw = "";
|
|
41
|
+
var text = String(value == null ? "" : value);
|
|
42
|
+
var maskIndex = 0;
|
|
43
|
+
var seenSlots = 0;
|
|
44
|
+
|
|
45
|
+
while (maskIndex < mask.length && seenSlots < slot) {
|
|
46
|
+
if (isToken(mask[maskIndex])) seenSlots++;
|
|
47
|
+
maskIndex++;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
for (var i = 0; i < text.length && slot < types.length; i++) {
|
|
51
|
+
var ch = text[i];
|
|
52
|
+
while (maskIndex < mask.length && !isToken(mask[maskIndex])) {
|
|
53
|
+
if (ch === mask[maskIndex]) {
|
|
54
|
+
maskIndex++;
|
|
55
|
+
ch = "";
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
maskIndex++;
|
|
59
|
+
}
|
|
60
|
+
if (!ch) continue;
|
|
61
|
+
if (accepts(types[slot], ch)) {
|
|
62
|
+
raw += ch;
|
|
63
|
+
slot++;
|
|
64
|
+
while (maskIndex < mask.length && !isToken(mask[maskIndex])) maskIndex++;
|
|
65
|
+
maskIndex++;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return raw;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Format raw token characters and retain raw-index ↔ display-position maps. */
|
|
72
|
+
function format(mask, rawValue) {
|
|
73
|
+
var raw = readRaw(mask, rawValue, 0);
|
|
74
|
+
var value = "";
|
|
75
|
+
var starts = [];
|
|
76
|
+
var ends = [];
|
|
77
|
+
var rawIndex = 0;
|
|
78
|
+
|
|
79
|
+
for (var i = 0; i < mask.length; i++) {
|
|
80
|
+
var part = mask[i];
|
|
81
|
+
if (isToken(part)) {
|
|
82
|
+
if (rawIndex >= raw.length) break;
|
|
83
|
+
starts[rawIndex] = value.length;
|
|
84
|
+
value += raw[rawIndex++];
|
|
85
|
+
ends[rawIndex - 1] = value.length;
|
|
86
|
+
} else if (raw.length > 0) {
|
|
87
|
+
value += part;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return { raw: raw, value: value, starts: starts, ends: ends };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function rawIndexAt(formatted, displayPosition) {
|
|
94
|
+
var position = Math.max(0, Number(displayPosition) || 0);
|
|
95
|
+
var rawIndex = 0;
|
|
96
|
+
for (var i = 0; i < formatted.ends.length; i++) {
|
|
97
|
+
if (formatted.ends[i] <= position) rawIndex = i + 1;
|
|
98
|
+
else break;
|
|
99
|
+
}
|
|
100
|
+
return rawIndex;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function caretAt(formatted, rawIndex) {
|
|
104
|
+
if (rawIndex < formatted.raw.length) return formatted.starts[rawIndex];
|
|
105
|
+
return formatted.value.length;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Pure edit engine.
|
|
110
|
+
* @param {string} mask
|
|
111
|
+
* @param {string} priorValue canonical masked value before the edit
|
|
112
|
+
* @param {{inputType?:string,data?:string|null,selectionStart?:number,selectionEnd?:number}} edit
|
|
113
|
+
* @returns {{raw:string,value:string,caret:number}}
|
|
114
|
+
*/
|
|
115
|
+
function maskEdit(mask, priorValue, edit) {
|
|
116
|
+
mask = String(mask || "");
|
|
117
|
+
var types = tokenTypes(mask);
|
|
118
|
+
if (!types.length) throw new Error("l-mask pattern requires at least one 9, a, or * token");
|
|
119
|
+
|
|
120
|
+
edit = edit || {};
|
|
121
|
+
var prior = format(mask, priorValue);
|
|
122
|
+
var displayStart = edit.selectionStart == null ? prior.value.length : edit.selectionStart;
|
|
123
|
+
var displayEnd = edit.selectionEnd == null ? displayStart : edit.selectionEnd;
|
|
124
|
+
var start = rawIndexAt(prior, Math.min(displayStart, displayEnd));
|
|
125
|
+
var end = rawIndexAt(prior, Math.max(displayStart, displayEnd));
|
|
126
|
+
var inputType = edit.inputType || "insertText";
|
|
127
|
+
var raw = prior.raw;
|
|
128
|
+
var caretRaw = start;
|
|
129
|
+
|
|
130
|
+
if (inputType.indexOf("delete") === 0) {
|
|
131
|
+
if (start !== end) {
|
|
132
|
+
raw = raw.slice(0, start) + raw.slice(end);
|
|
133
|
+
} else if (inputType === "deleteContentBackward" && start > 0) {
|
|
134
|
+
raw = raw.slice(0, start - 1) + raw.slice(start);
|
|
135
|
+
caretRaw = start - 1;
|
|
136
|
+
} else if (inputType === "deleteContentForward") {
|
|
137
|
+
raw = raw.slice(0, start) + raw.slice(start + 1);
|
|
138
|
+
}
|
|
139
|
+
} else {
|
|
140
|
+
var inserted = readRaw(mask, edit.data == null ? "" : edit.data, start);
|
|
141
|
+
raw = (raw.slice(0, start) + inserted + raw.slice(end)).slice(0, types.length);
|
|
142
|
+
caretRaw = start + inserted.length;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
var next = format(mask, raw);
|
|
146
|
+
return { raw: next.raw, value: next.value, caret: caretAt(next, caretRaw) };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function modelExpression(el) {
|
|
150
|
+
for (var i = 0; i < el.attributes.length; i++) {
|
|
151
|
+
if (el.attributes[i].name.indexOf("l-model") === 0) {
|
|
152
|
+
return el.attributes[i].value;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return "";
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function install(Faqir) {
|
|
159
|
+
Faqir.directive("mask", function (el, dir, scope) {
|
|
160
|
+
if (!el || !/^(INPUT|TEXTAREA)$/.test(el.tagName)) {
|
|
161
|
+
console.warn("[Faqir] l-mask must be used on an input or textarea");
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
if (el._faqirMask) {
|
|
165
|
+
if (el._faqirMask._scope === scope) return;
|
|
166
|
+
if (typeof el._faqirMask.destroy === "function") el._faqirMask.destroy();
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
var pattern = String(dir.expression || "").trim();
|
|
170
|
+
try {
|
|
171
|
+
maskEdit(pattern, "", {});
|
|
172
|
+
} catch (error) {
|
|
173
|
+
console.warn("[Faqir] " + error.message);
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
var model = modelExpression(el);
|
|
178
|
+
var destroyed = false;
|
|
179
|
+
var renderId = 0;
|
|
180
|
+
var pendingCaret = null;
|
|
181
|
+
var initial = maskEdit(pattern, "", {
|
|
182
|
+
inputType: "insertFromPaste",
|
|
183
|
+
data: el.value,
|
|
184
|
+
selectionStart: 0,
|
|
185
|
+
selectionEnd: 0,
|
|
186
|
+
});
|
|
187
|
+
var raw = initial.raw;
|
|
188
|
+
|
|
189
|
+
function paint(result, caret) {
|
|
190
|
+
raw = result.raw;
|
|
191
|
+
if (el.value !== result.value) el.value = result.value;
|
|
192
|
+
if (caret !== false && typeof el.setSelectionRange === "function") {
|
|
193
|
+
try {
|
|
194
|
+
el.setSelectionRange(result.caret, result.caret);
|
|
195
|
+
} catch (e) {}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function emit(result) {
|
|
200
|
+
el.dispatchEvent(
|
|
201
|
+
new CustomEvent("faqir:mask", {
|
|
202
|
+
bubbles: true,
|
|
203
|
+
detail: { raw: result.raw, value: result.value, caret: result.caret },
|
|
204
|
+
}),
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function commit(result, shouldEmit) {
|
|
209
|
+
paint(result, true);
|
|
210
|
+
if (model) {
|
|
211
|
+
pendingCaret = result.caret;
|
|
212
|
+
Faqir.evaluateAssignment(model + " = " + JSON.stringify(result.raw), scope, el);
|
|
213
|
+
// l-model also reflects the raw scope value into el.value. Repaint after
|
|
214
|
+
// the reactive flush so the visible value remains masked regardless of
|
|
215
|
+
// directive attribute order.
|
|
216
|
+
var id = ++renderId;
|
|
217
|
+
Faqir.nextTick(function () {
|
|
218
|
+
if (!destroyed && id === renderId) {
|
|
219
|
+
var repaint = formatResult(raw);
|
|
220
|
+
repaint.caret = pendingCaret == null ? repaint.caret : pendingCaret;
|
|
221
|
+
pendingCaret = null;
|
|
222
|
+
paint(repaint, true);
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
if (shouldEmit !== false) emit(result);
|
|
227
|
+
return result;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function formatResult(value) {
|
|
231
|
+
return maskEdit(pattern, "", {
|
|
232
|
+
inputType: "insertFromPaste",
|
|
233
|
+
data: value,
|
|
234
|
+
selectionStart: 0,
|
|
235
|
+
selectionEnd: 0,
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function edit(inputType, data, selectionStart, selectionEnd) {
|
|
240
|
+
return commit(
|
|
241
|
+
maskEdit(pattern, el.value, {
|
|
242
|
+
inputType: inputType,
|
|
243
|
+
data: data,
|
|
244
|
+
selectionStart: selectionStart == null ? el.selectionStart : selectionStart,
|
|
245
|
+
selectionEnd: selectionEnd == null ? el.selectionEnd : selectionEnd,
|
|
246
|
+
}),
|
|
247
|
+
true,
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function setRaw(value, caret, shouldEmit) {
|
|
252
|
+
var result = formatResult(value);
|
|
253
|
+
if (caret != null) {
|
|
254
|
+
var formatted = format(pattern, result.raw);
|
|
255
|
+
result.caret = caretAt(formatted, Math.max(0, Math.min(Number(caret) || 0, result.raw.length)));
|
|
256
|
+
}
|
|
257
|
+
return commit(result, shouldEmit);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function onBeforeInput(event) {
|
|
261
|
+
var type = event.inputType || "";
|
|
262
|
+
if (type.indexOf("insert") !== 0 && type.indexOf("delete") !== 0) return;
|
|
263
|
+
event.preventDefault();
|
|
264
|
+
event.stopImmediatePropagation();
|
|
265
|
+
edit(type, event.data);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function onPaste(event) {
|
|
269
|
+
var data = event.clipboardData;
|
|
270
|
+
if (!data || typeof data.getData !== "function") return;
|
|
271
|
+
event.preventDefault();
|
|
272
|
+
event.stopImmediatePropagation();
|
|
273
|
+
edit("insertFromPaste", data.getData("text"));
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function onInput(event) {
|
|
277
|
+
// Fallback for autofill/IME paths that skip cancelable beforeinput.
|
|
278
|
+
event.stopImmediatePropagation();
|
|
279
|
+
var caret = el.selectionStart;
|
|
280
|
+
var result = formatResult(el.value);
|
|
281
|
+
result.caret = Math.min(caret == null ? result.value.length : caret, result.value.length);
|
|
282
|
+
commit(result, true);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
el.addEventListener("beforeinput", onBeforeInput, true);
|
|
286
|
+
el.addEventListener("paste", onPaste, true);
|
|
287
|
+
el.addEventListener("input", onInput, true);
|
|
288
|
+
|
|
289
|
+
var api;
|
|
290
|
+
function cleanup() {
|
|
291
|
+
if (destroyed) return;
|
|
292
|
+
destroyed = true;
|
|
293
|
+
el.removeEventListener("beforeinput", onBeforeInput, true);
|
|
294
|
+
el.removeEventListener("paste", onPaste, true);
|
|
295
|
+
el.removeEventListener("input", onInput, true);
|
|
296
|
+
if (typeof stopModel === "function") stopModel();
|
|
297
|
+
if (el._faqirMask === api) delete el._faqirMask;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
api = {
|
|
301
|
+
edit: edit,
|
|
302
|
+
getRaw: function () { return raw; },
|
|
303
|
+
setRaw: setRaw,
|
|
304
|
+
destroy: cleanup,
|
|
305
|
+
_scope: scope,
|
|
306
|
+
};
|
|
307
|
+
el._faqirMask = api;
|
|
308
|
+
paint(initial, false);
|
|
309
|
+
|
|
310
|
+
var stopModel = null;
|
|
311
|
+
if (model) {
|
|
312
|
+
stopModel = Faqir.effect(function () {
|
|
313
|
+
var value = Faqir.evaluate(model, scope, el);
|
|
314
|
+
var id = ++renderId;
|
|
315
|
+
Faqir.nextTick(function () {
|
|
316
|
+
if (!destroyed && id === renderId) {
|
|
317
|
+
var repaint = formatResult(value);
|
|
318
|
+
if (pendingCaret != null && repaint.raw === raw) {
|
|
319
|
+
repaint.caret = pendingCaret;
|
|
320
|
+
pendingCaret = null;
|
|
321
|
+
paint(repaint, true);
|
|
322
|
+
} else {
|
|
323
|
+
paint(repaint, false);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
});
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
return cleanup;
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
install.maskEdit = maskEdit;
|
|
335
|
+
if (typeof module !== "undefined" && module.exports) module.exports = install;
|
|
336
|
+
|
|
337
|
+
var F = typeof globalThis !== "undefined" ? globalThis.Faqir : window.Faqir;
|
|
338
|
+
if (F && typeof F.plugin === "function") F.plugin(install);
|
|
339
|
+
})();
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
// @ui:plugin faqir-persist
|
|
2
|
+
// @ui:provides l-persist, $persist()
|
|
3
|
+
/**
|
|
4
|
+
* faqir-persist — localStorage-backed reactive state. [0.6-05 · §A5]
|
|
5
|
+
*
|
|
6
|
+
* <div l-data="{ count: 0 }" l-persist="count">
|
|
7
|
+
* <button @click="count++" l-text="count"></button>
|
|
8
|
+
* </div>
|
|
9
|
+
*
|
|
10
|
+
* `l-persist="expression"` restores the expression before its first effect and
|
|
11
|
+
* writes every reactive change as JSON. Keys are scoped as
|
|
12
|
+
* `faqir:<namespace>:<key>`; set `data-persist-namespace` or
|
|
13
|
+
* `data-persist-key` when the location-derived defaults are not specific enough.
|
|
14
|
+
* `$persist(key)` reads the current namespace and `$persist(key, value)` writes
|
|
15
|
+
* it, which is useful from `l-init` and event expressions.
|
|
16
|
+
*
|
|
17
|
+
* Storage access is guarded because localStorage can be absent or can throw in
|
|
18
|
+
* privacy/quota modes. Failed writes fall back to module-local memory, keeping
|
|
19
|
+
* the page reactive without throwing. Self-registering, zero dependencies,
|
|
20
|
+
* and ≤ 2 KB gzip.
|
|
21
|
+
*/
|
|
22
|
+
(function () {
|
|
23
|
+
"use strict";
|
|
24
|
+
|
|
25
|
+
var memory = Object.create(null);
|
|
26
|
+
|
|
27
|
+
function storage() {
|
|
28
|
+
try {
|
|
29
|
+
var g = typeof globalThis !== "undefined" ? globalThis : window;
|
|
30
|
+
return g.localStorage || null;
|
|
31
|
+
} catch (e) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function namespace(el) {
|
|
37
|
+
var owner = el.closest && el.closest("[data-persist-namespace]");
|
|
38
|
+
if (owner) return owner.getAttribute("data-persist-namespace") || "default";
|
|
39
|
+
try {
|
|
40
|
+
return (location && location.pathname) || "default";
|
|
41
|
+
} catch (e) {
|
|
42
|
+
return "default";
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function fullKey(el, key) {
|
|
47
|
+
return "faqir:" + namespace(el) + ":" + key;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function readRaw(key) {
|
|
51
|
+
if (Object.prototype.hasOwnProperty.call(memory, key)) return memory[key];
|
|
52
|
+
try {
|
|
53
|
+
var s = storage();
|
|
54
|
+
return s ? s.getItem(key) : null;
|
|
55
|
+
} catch (e) {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function writeRaw(key, value) {
|
|
61
|
+
try {
|
|
62
|
+
var s = storage();
|
|
63
|
+
if (!s) throw new Error("storage unavailable");
|
|
64
|
+
if (value == null) s.removeItem(key);
|
|
65
|
+
else s.setItem(key, value);
|
|
66
|
+
delete memory[key];
|
|
67
|
+
} catch (e) {
|
|
68
|
+
if (value == null) delete memory[key];
|
|
69
|
+
else memory[key] = value;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function decode(raw) {
|
|
74
|
+
if (raw == null) return { found: false };
|
|
75
|
+
try {
|
|
76
|
+
return { found: true, value: JSON.parse(raw) };
|
|
77
|
+
} catch (e) {
|
|
78
|
+
return { found: false };
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function encode(value) {
|
|
83
|
+
try {
|
|
84
|
+
return JSON.stringify(value);
|
|
85
|
+
} catch (e) {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function install(Faqir) {
|
|
91
|
+
Faqir.magic("persist", function (el) {
|
|
92
|
+
return function (key, value) {
|
|
93
|
+
var storageKey = fullKey(el, String(key));
|
|
94
|
+
if (arguments.length > 1) {
|
|
95
|
+
writeRaw(storageKey, encode(value));
|
|
96
|
+
return value;
|
|
97
|
+
}
|
|
98
|
+
var saved = decode(readRaw(storageKey));
|
|
99
|
+
return saved.found ? saved.value : undefined;
|
|
100
|
+
};
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
Faqir.directive("persist", function (el, dir, scope) {
|
|
104
|
+
var expression = (dir.expression || "").trim();
|
|
105
|
+
if (!expression) {
|
|
106
|
+
console.warn("[Faqir] l-persist requires a state expression");
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
var key = el.getAttribute("data-persist-key") || expression;
|
|
111
|
+
var storageKey = fullKey(el, key);
|
|
112
|
+
var saved = decode(readRaw(storageKey));
|
|
113
|
+
if (saved.found) {
|
|
114
|
+
Faqir.evaluateAssignment(expression + " = " + JSON.stringify(saved.value), scope, el);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return Faqir.effect(function () {
|
|
118
|
+
writeRaw(storageKey, encode(Faqir.evaluate(expression, scope, el)));
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (typeof module !== "undefined" && module.exports) module.exports = install;
|
|
124
|
+
|
|
125
|
+
var F =
|
|
126
|
+
(typeof globalThis !== "undefined" && globalThis.Faqir) ||
|
|
127
|
+
(typeof window !== "undefined" && window.Faqir);
|
|
128
|
+
if (F && typeof F.plugin === "function") F.plugin(install);
|
|
129
|
+
})();
|