riftt 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/dist/rift.min.js +18 -0
- package/package.json +22 -0
- package/rollup.config.js +15 -0
- package/src/index.js +56 -0
- package/src/modules.js +111 -0
- package/src/router.js +535 -0
- package/src/sprae/core.js +220 -0
- package/src/sprae/directive/aria.js +9 -0
- package/src/sprae/directive/class.js +28 -0
- package/src/sprae/directive/data.js +5 -0
- package/src/sprae/directive/default.js +210 -0
- package/src/sprae/directive/each.js +100 -0
- package/src/sprae/directive/fx.js +3 -0
- package/src/sprae/directive/if.js +46 -0
- package/src/sprae/directive/ref.js +13 -0
- package/src/sprae/directive/style.js +22 -0
- package/src/sprae/directive/text.js +13 -0
- package/src/sprae/directive/value.js +100 -0
- package/src/sprae/directive/with.js +20 -0
- package/src/sprae/persist.js +214 -0
- package/src/sprae/signal.js +106 -0
- package/src/sprae/sprae.js +20 -0
- package/src/sprae/store.js +197 -0
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { use, effect, untracked } from "./signal.js";
|
|
2
|
+
import { store } from "./store.js";
|
|
3
|
+
|
|
4
|
+
export const _dispose = (Symbol.dispose ||= Symbol("dispose"));
|
|
5
|
+
export const _state = Symbol("state");
|
|
6
|
+
export const _on = Symbol("on");
|
|
7
|
+
export const _off = Symbol("off");
|
|
8
|
+
|
|
9
|
+
export const directive = {};
|
|
10
|
+
|
|
11
|
+
const memo = {};
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Compiles an expression into an evaluator function.
|
|
15
|
+
*
|
|
16
|
+
* @type {(expr: string) => Function}
|
|
17
|
+
*/
|
|
18
|
+
export let compile;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Attributes prefix, by default ':'
|
|
22
|
+
*/
|
|
23
|
+
export let prefix = ":";
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Register a directive with a parsed expression and evaluator.
|
|
27
|
+
* @param {string} name - The name of the directive.
|
|
28
|
+
* @param {(el: Element, state: Object, expr: string, name: string) => (value: any) => void} create - A function to create the directive.
|
|
29
|
+
* @param {(expr: string) => (state: Object) => any} [p=parse] - Create evaluator from expression string.
|
|
30
|
+
*/
|
|
31
|
+
export const dir = (name, create, toEvaluator = parse) => {
|
|
32
|
+
directive[name] = (element, expression, state, directiveName) => {
|
|
33
|
+
const update = create(element, state, expression, directiveName);
|
|
34
|
+
const evaluate = toEvaluator(expression, ":" + directiveName);
|
|
35
|
+
return () => update(evaluate(state));
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Parses an expression into an evaluator function, caching the result for reuse.
|
|
41
|
+
*
|
|
42
|
+
* @param {string} expr - The expression to parse and compile into a function.
|
|
43
|
+
* @param {string} dir - The directive associated with the expression (used for error reporting).
|
|
44
|
+
* @returns {Function} The compiled evaluator function for the expression.
|
|
45
|
+
*/
|
|
46
|
+
export const parse = (expression, directive) => {
|
|
47
|
+
const key = expression.trim();
|
|
48
|
+
|
|
49
|
+
if (memo[key]) {
|
|
50
|
+
return memo[key];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
let compiledFunction;
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
compiledFunction = compile(key);
|
|
57
|
+
} catch (error) {
|
|
58
|
+
err(error, directive, key);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const evaluator = (state) => {
|
|
62
|
+
try {
|
|
63
|
+
return compiledFunction(state);
|
|
64
|
+
} catch (error) {
|
|
65
|
+
err(error, directive, key);
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
memo[key] = evaluator;
|
|
70
|
+
return evaluator;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Branded sprae error with context about the directive and expression
|
|
75
|
+
*
|
|
76
|
+
* @param {Error} e - The original error object to enhance.
|
|
77
|
+
* @param {string} dir - The directive where the error occurred.
|
|
78
|
+
* @param {string} [expr=''] - The expression associated with the error, if any.
|
|
79
|
+
* @throws {Error} The enhanced error object with a formatted message.
|
|
80
|
+
*/
|
|
81
|
+
export const err = (error, directive = "", expression = "") => {
|
|
82
|
+
const contextMessage = expression ? `="${expression}"\n\n` : "";
|
|
83
|
+
const enhancedMessage = `◆ ${error.message}\n\n${directive}${contextMessage}`;
|
|
84
|
+
|
|
85
|
+
throw Object.assign(error, {
|
|
86
|
+
message: enhancedMessage,
|
|
87
|
+
expr: expression,
|
|
88
|
+
});
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const initializeElement = (element, attributes = element.attributes, state, effects, cleanupFunctions) => {
|
|
92
|
+
if (!attributes) return;
|
|
93
|
+
|
|
94
|
+
let attributeIndex = 0;
|
|
95
|
+
|
|
96
|
+
while (attributeIndex < attributes.length) {
|
|
97
|
+
const attribute = attributes[attributeIndex];
|
|
98
|
+
const { name: attributeName, value: attributeValue } = attribute;
|
|
99
|
+
|
|
100
|
+
if (attributeName.startsWith(prefix)) {
|
|
101
|
+
element.removeAttribute(attributeName);
|
|
102
|
+
|
|
103
|
+
const directiveNames = attributeName.slice(prefix.length).split(":");
|
|
104
|
+
|
|
105
|
+
for (const directiveName of directiveNames) {
|
|
106
|
+
const directiveHandler = directive[directiveName] || directive.default;
|
|
107
|
+
const update = directiveHandler(element, attributeValue, state, directiveName);
|
|
108
|
+
|
|
109
|
+
effects.push(update);
|
|
110
|
+
cleanupFunctions.push(effect(update));
|
|
111
|
+
|
|
112
|
+
if (element[_state] === null) {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
} else {
|
|
117
|
+
attributeIndex++;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
for (const childNode of element.childNodes) {
|
|
122
|
+
if (childNode.nodeType === 1) {
|
|
123
|
+
initializeElement(childNode, childNode.attributes, state, effects, cleanupFunctions);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Applies directives to an HTML element and manages its reactive state.
|
|
130
|
+
*
|
|
131
|
+
* @param {Element} [el=document.body] - The target HTML element to apply directives to.
|
|
132
|
+
* @param {Object} [values] - Initial values to populate the element's reactive state.
|
|
133
|
+
* @returns {Object} The reactive state object associated with the element.
|
|
134
|
+
*/
|
|
135
|
+
export const sprae = (element = document.body, values) => {
|
|
136
|
+
if (element[_state]) {
|
|
137
|
+
return Object.assign(element[_state], values);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const state = store(values || {});
|
|
141
|
+
const cleanupFunctions = [];
|
|
142
|
+
const effects = [];
|
|
143
|
+
|
|
144
|
+
initializeElement(element, element.attributes, state, effects, cleanupFunctions);
|
|
145
|
+
|
|
146
|
+
if (!(_state in element)) {
|
|
147
|
+
element[_state] = state;
|
|
148
|
+
|
|
149
|
+
element[_off] = () => {
|
|
150
|
+
cleanupFunctions.forEach((cleanup) => cleanup());
|
|
151
|
+
cleanupFunctions.length = 0;
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
element[_on] = () => {
|
|
155
|
+
cleanupFunctions.length = 0;
|
|
156
|
+
cleanupFunctions.push(...effects.map((effectFunction) => effect(effectFunction)));
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
element[_dispose] = () => {
|
|
160
|
+
element[_off]();
|
|
161
|
+
element[_off] = null;
|
|
162
|
+
element[_on] = null;
|
|
163
|
+
element[_dispose] = null;
|
|
164
|
+
element[_state] = null;
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return state;
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
sprae.use = (configuration) => {
|
|
172
|
+
if (configuration.signal) {
|
|
173
|
+
use(configuration);
|
|
174
|
+
}
|
|
175
|
+
if (configuration.compile) {
|
|
176
|
+
compile = configuration.compile;
|
|
177
|
+
}
|
|
178
|
+
if (configuration.prefix) {
|
|
179
|
+
prefix = configuration.prefix;
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
export const frag = (template) => {
|
|
184
|
+
if (!template.nodeType) {
|
|
185
|
+
return template;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const content = template.content.cloneNode(true);
|
|
189
|
+
const attributes = [...template.attributes];
|
|
190
|
+
const referenceNode = document.createTextNode("");
|
|
191
|
+
|
|
192
|
+
content.append(referenceNode);
|
|
193
|
+
const childNodes = [...content.childNodes];
|
|
194
|
+
|
|
195
|
+
return {
|
|
196
|
+
childNodes,
|
|
197
|
+
content,
|
|
198
|
+
|
|
199
|
+
remove() {
|
|
200
|
+
content.append(...childNodes);
|
|
201
|
+
},
|
|
202
|
+
|
|
203
|
+
replaceWith(element) {
|
|
204
|
+
if (element === referenceNode) return;
|
|
205
|
+
referenceNode.before(element);
|
|
206
|
+
content.append(...childNodes);
|
|
207
|
+
},
|
|
208
|
+
|
|
209
|
+
attributes,
|
|
210
|
+
|
|
211
|
+
removeAttribute(name) {
|
|
212
|
+
const attributeIndex = attributes.findIndex((attr) => attr.name === name);
|
|
213
|
+
if (attributeIndex !== -1) {
|
|
214
|
+
attributes.splice(attributeIndex, 1);
|
|
215
|
+
}
|
|
216
|
+
},
|
|
217
|
+
};
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
export default sprae;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { dir } from "../core.js";
|
|
2
|
+
|
|
3
|
+
dir("class", (el) => {
|
|
4
|
+
let previous = new Set();
|
|
5
|
+
|
|
6
|
+
return (value) => {
|
|
7
|
+
const next = new Set();
|
|
8
|
+
|
|
9
|
+
if (value) {
|
|
10
|
+
if (typeof value === "string") {
|
|
11
|
+
value.split(" ").forEach((cls) => next.add(cls));
|
|
12
|
+
} else if (Array.isArray(value)) {
|
|
13
|
+
value.forEach((cls) => cls && next.add(cls));
|
|
14
|
+
} else {
|
|
15
|
+
Object.entries(value).forEach(([name, on]) => on && next.add(name));
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
for (const cls of previous) {
|
|
20
|
+
if (next.has(cls)) next.delete(cls);
|
|
21
|
+
else el.classList.remove(cls);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
for (const cls of next) el.classList.add(cls);
|
|
25
|
+
|
|
26
|
+
previous = next;
|
|
27
|
+
};
|
|
28
|
+
});
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
// generic property directive
|
|
2
|
+
import { dir, err } from "../core.js";
|
|
3
|
+
|
|
4
|
+
dir("default", (target, state, expr, name) => {
|
|
5
|
+
if (!name.startsWith("on")) {
|
|
6
|
+
if (name) {
|
|
7
|
+
return (value) => attr(target, name, value);
|
|
8
|
+
}
|
|
9
|
+
return (value) => {
|
|
10
|
+
for (const key in value) attr(target, dashcase(key), value[key]);
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const contexts = name.split("..").map((part) => parseEventContext(part, target));
|
|
15
|
+
|
|
16
|
+
const bind = (fn, ctx) => {
|
|
17
|
+
const { evt, target, test, defer, stop, prevent, immediate, ...opts } = ctx;
|
|
18
|
+
const handler = (e) => {
|
|
19
|
+
try {
|
|
20
|
+
if (!test(e)) return;
|
|
21
|
+
if (stop) immediate ? e.stopImmediatePropagation() : e.stopPropagation();
|
|
22
|
+
if (prevent) e.preventDefault();
|
|
23
|
+
fn?.call(state, e);
|
|
24
|
+
} catch (error) {
|
|
25
|
+
err(error, `:on${evt}`, fn);
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
const cb = defer ? defer(handler) : handler;
|
|
29
|
+
target.addEventListener(evt, cb, opts);
|
|
30
|
+
return () => target.removeEventListener(evt, cb, opts);
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
if (contexts.length === 1) return (v) => bind(v, contexts[0]);
|
|
34
|
+
|
|
35
|
+
let startFn,
|
|
36
|
+
nextFn,
|
|
37
|
+
off,
|
|
38
|
+
idx = 0;
|
|
39
|
+
|
|
40
|
+
const nextListener = (fn) => {
|
|
41
|
+
off = bind((e) => {
|
|
42
|
+
off();
|
|
43
|
+
nextFn = fn?.(e);
|
|
44
|
+
idx = (idx + 1) % contexts.length;
|
|
45
|
+
if (idx) nextListener(nextFn);
|
|
46
|
+
else if (startFn) nextListener(startFn);
|
|
47
|
+
}, contexts[idx]);
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
return (value) => {
|
|
51
|
+
startFn = value;
|
|
52
|
+
if (!off) nextListener(startFn);
|
|
53
|
+
return () => {
|
|
54
|
+
startFn = null;
|
|
55
|
+
};
|
|
56
|
+
};
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
function parseEventContext(part, target) {
|
|
60
|
+
const ctx = { evt: "", target, test: () => true };
|
|
61
|
+
ctx.evt = (part.startsWith("on") ? part.slice(2) : part).replace(/\.(\w+)?-?([-\w]+)?/g, (_, mod, param = "") => {
|
|
62
|
+
ctx.test = mods[mod]?.(ctx, ...param.split("-")) || ctx.test;
|
|
63
|
+
return "";
|
|
64
|
+
});
|
|
65
|
+
return ctx;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// event modifiers
|
|
69
|
+
const mods = {
|
|
70
|
+
// actions
|
|
71
|
+
prevent(ctx) {
|
|
72
|
+
ctx.prevent = true;
|
|
73
|
+
},
|
|
74
|
+
stop(ctx) {
|
|
75
|
+
ctx.stop = true;
|
|
76
|
+
},
|
|
77
|
+
immediate(ctx) {
|
|
78
|
+
ctx.immediate = true;
|
|
79
|
+
},
|
|
80
|
+
|
|
81
|
+
// options
|
|
82
|
+
once(ctx) {
|
|
83
|
+
ctx.once = true;
|
|
84
|
+
},
|
|
85
|
+
passive(ctx) {
|
|
86
|
+
ctx.passive = true;
|
|
87
|
+
},
|
|
88
|
+
capture(ctx) {
|
|
89
|
+
ctx.capture = true;
|
|
90
|
+
},
|
|
91
|
+
|
|
92
|
+
// target
|
|
93
|
+
window(ctx) {
|
|
94
|
+
ctx.target = window;
|
|
95
|
+
},
|
|
96
|
+
document(ctx) {
|
|
97
|
+
ctx.target = document;
|
|
98
|
+
},
|
|
99
|
+
parent(ctx) {
|
|
100
|
+
ctx.target = ctx.target.parentNode;
|
|
101
|
+
},
|
|
102
|
+
|
|
103
|
+
throttle(ctx, limit = 108) {
|
|
104
|
+
ctx.defer = (fn) => throttle(fn, limit);
|
|
105
|
+
},
|
|
106
|
+
debounce(ctx, wait = 108) {
|
|
107
|
+
ctx.defer = (fn) => debounce(fn, wait);
|
|
108
|
+
},
|
|
109
|
+
|
|
110
|
+
// test
|
|
111
|
+
outside: (ctx) => (e) => {
|
|
112
|
+
const el = ctx.target;
|
|
113
|
+
if (el.contains(e.target)) return false;
|
|
114
|
+
if (e.target.isConnected === false) return false;
|
|
115
|
+
if (el.offsetWidth < 1 && el.offsetHeight < 1) return false;
|
|
116
|
+
return true;
|
|
117
|
+
},
|
|
118
|
+
self: (ctx) => (e) => e.target === ctx.target,
|
|
119
|
+
|
|
120
|
+
// keyboard
|
|
121
|
+
ctrl:
|
|
122
|
+
(_ctx, ...param) =>
|
|
123
|
+
(e) =>
|
|
124
|
+
keys.ctrl(e) && param.every((p) => (keys[p] ? keys[p](e) : e.key === p)),
|
|
125
|
+
shift:
|
|
126
|
+
(_ctx, ...param) =>
|
|
127
|
+
(e) =>
|
|
128
|
+
keys.shift(e) && param.every((p) => (keys[p] ? keys[p](e) : e.key === p)),
|
|
129
|
+
alt:
|
|
130
|
+
(_ctx, ...param) =>
|
|
131
|
+
(e) =>
|
|
132
|
+
keys.alt(e) && param.every((p) => (keys[p] ? keys[p](e) : e.key === p)),
|
|
133
|
+
meta:
|
|
134
|
+
(_ctx, ...param) =>
|
|
135
|
+
(e) =>
|
|
136
|
+
keys.meta(e) && param.every((p) => (keys[p] ? keys[p](e) : e.key === p)),
|
|
137
|
+
arrow: () => keys.arrow,
|
|
138
|
+
enter: () => keys.enter,
|
|
139
|
+
esc: () => keys.esc,
|
|
140
|
+
tab: () => keys.tab,
|
|
141
|
+
space: () => keys.space,
|
|
142
|
+
delete: () => keys.delete,
|
|
143
|
+
digit: () => keys.digit,
|
|
144
|
+
letter: () => keys.letter,
|
|
145
|
+
char: () => keys.char,
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
// key testers
|
|
149
|
+
const keys = {
|
|
150
|
+
ctrl: (e) => e.ctrlKey || e.key === "Control" || e.key === "Ctrl",
|
|
151
|
+
shift: (e) => e.shiftKey || e.key === "Shift",
|
|
152
|
+
alt: (e) => e.altKey || e.key === "Alt",
|
|
153
|
+
meta: (e) => e.metaKey || e.key === "Meta" || e.key === "Command",
|
|
154
|
+
arrow: (e) => e.key.startsWith("Arrow"),
|
|
155
|
+
enter: (e) => e.key === "Enter",
|
|
156
|
+
esc: (e) => e.key.startsWith("Esc"),
|
|
157
|
+
tab: (e) => e.key === "Tab",
|
|
158
|
+
space: (e) => e.key === " " || e.key === "Space" || e.key === " ",
|
|
159
|
+
delete: (e) => e.key === "Delete" || e.key === "Backspace",
|
|
160
|
+
digit: (e) => /^\d$/.test(e.key),
|
|
161
|
+
letter: (e) => /^\p{L}$/gu.test(e.key),
|
|
162
|
+
char: (e) => /^\S$/.test(e.key),
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
// create delayed fns
|
|
166
|
+
const throttle = (fn, limit) => {
|
|
167
|
+
let paused = false;
|
|
168
|
+
let planned = false;
|
|
169
|
+
|
|
170
|
+
const block = (e) => {
|
|
171
|
+
paused = true;
|
|
172
|
+
setTimeout(() => {
|
|
173
|
+
paused = false;
|
|
174
|
+
if (!planned) {
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
planned = false;
|
|
178
|
+
block(e);
|
|
179
|
+
fn(e);
|
|
180
|
+
}, limit);
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
return (e) => {
|
|
184
|
+
if (paused) {
|
|
185
|
+
planned = true;
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
block(e);
|
|
189
|
+
return fn(e);
|
|
190
|
+
};
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
const debounce = (fn, wait) => {
|
|
194
|
+
let timeout;
|
|
195
|
+
return (e) => {
|
|
196
|
+
clearTimeout(timeout);
|
|
197
|
+
timeout = setTimeout(() => {
|
|
198
|
+
timeout = null;
|
|
199
|
+
fn(e);
|
|
200
|
+
}, wait);
|
|
201
|
+
};
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
// set attr
|
|
205
|
+
export const attr = (el, name, v) => {
|
|
206
|
+
if (v == null || v === false) el.removeAttribute(name);
|
|
207
|
+
else el.setAttribute(name, v === true ? "" : typeof v === "number" || typeof v === "string" ? v : "");
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
export const dashcase = (str) => str.replace(/[A-Z\u00C0-\u00D6\u00D8-\u00DE]/g, (match, i) => (i ? "-" : "") + match.toLowerCase());
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import sprae, { _state, dir, frag, parse } from "../core.js";
|
|
2
|
+
import store, { _change, _signals } from "../store.js";
|
|
3
|
+
import { effect } from "../signal.js";
|
|
4
|
+
|
|
5
|
+
dir(
|
|
6
|
+
"each",
|
|
7
|
+
(tpl, state, expr) => {
|
|
8
|
+
const [itemVar, idxVar = "$"] = expr
|
|
9
|
+
.split(/\bin\b/)[0]
|
|
10
|
+
.trim()
|
|
11
|
+
.split(/\s*,\s*/);
|
|
12
|
+
|
|
13
|
+
const holder = document.createTextNode("");
|
|
14
|
+
|
|
15
|
+
let current;
|
|
16
|
+
let keys;
|
|
17
|
+
let items;
|
|
18
|
+
let prevLen = 0;
|
|
19
|
+
|
|
20
|
+
const render = () => {
|
|
21
|
+
const newItems = items;
|
|
22
|
+
const newLen = newItems.length;
|
|
23
|
+
let i = 0;
|
|
24
|
+
|
|
25
|
+
if (current && !current[_change]) {
|
|
26
|
+
for (const s of current[_signals] || []) s[Symbol.dispose]();
|
|
27
|
+
current = null;
|
|
28
|
+
prevLen = 0;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (newLen < prevLen) {
|
|
32
|
+
current.length = newLen;
|
|
33
|
+
} else {
|
|
34
|
+
if (!current) {
|
|
35
|
+
current = newItems;
|
|
36
|
+
} else {
|
|
37
|
+
while (i < prevLen) {
|
|
38
|
+
current[i] = newItems[i];
|
|
39
|
+
i++;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
for (; i < newLen; i++) {
|
|
44
|
+
current[i] = newItems[i];
|
|
45
|
+
const idx = i;
|
|
46
|
+
|
|
47
|
+
const scope = store(
|
|
48
|
+
{
|
|
49
|
+
[itemVar]: current[_signals]?.[idx] || current[idx],
|
|
50
|
+
[idxVar]: keys ? keys[idx] : idx,
|
|
51
|
+
},
|
|
52
|
+
state,
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
const el = tpl.content ? frag(tpl) : tpl.cloneNode(true);
|
|
56
|
+
|
|
57
|
+
holder.before(el.content || el);
|
|
58
|
+
sprae(el, scope);
|
|
59
|
+
|
|
60
|
+
const prevDispose = ((current[_signals] ||= [])[i] ||= {})[Symbol.dispose];
|
|
61
|
+
current[_signals][i][Symbol.dispose] = () => {
|
|
62
|
+
prevDispose?.();
|
|
63
|
+
el[Symbol.dispose]?.();
|
|
64
|
+
el.remove();
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
prevLen = newLen;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
tpl.replaceWith(holder);
|
|
73
|
+
tpl[_state] = null;
|
|
74
|
+
|
|
75
|
+
return (value) => {
|
|
76
|
+
keys = null;
|
|
77
|
+
if (typeof value === "number") {
|
|
78
|
+
items = Array.from({ length: value }, (_, i) => i + 1);
|
|
79
|
+
} else if (value?.constructor === Object) {
|
|
80
|
+
keys = Object.keys(value);
|
|
81
|
+
items = Object.values(value);
|
|
82
|
+
} else {
|
|
83
|
+
items = value || [];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
let planned = 0;
|
|
87
|
+
return effect(() => {
|
|
88
|
+
items[_change]?.value;
|
|
89
|
+
if (!planned++) {
|
|
90
|
+
render();
|
|
91
|
+
queueMicrotask(() => {
|
|
92
|
+
if (planned > 1) render();
|
|
93
|
+
planned = 0;
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
};
|
|
98
|
+
},
|
|
99
|
+
(expr) => parse(expr.split(/\bin\b/)[1]),
|
|
100
|
+
);
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import sprae, { dir, _state, _on, _off, frag } from "../core.js";
|
|
2
|
+
|
|
3
|
+
// :if is interchangeable with :each depending on order, :if :each or :each :if have different meanings
|
|
4
|
+
// as for :if :with - :if must init first, since it is lazy, to avoid initializing component ahead of time by :with
|
|
5
|
+
// we consider :with={x} :if={x} case insignificant
|
|
6
|
+
const _prevIf = Symbol("if");
|
|
7
|
+
|
|
8
|
+
dir("if", (el, state) => {
|
|
9
|
+
const holder = document.createTextNode("");
|
|
10
|
+
|
|
11
|
+
let nextEl = el.nextElementSibling;
|
|
12
|
+
let curEl;
|
|
13
|
+
let ifEl;
|
|
14
|
+
let elseEl;
|
|
15
|
+
|
|
16
|
+
el.replaceWith(holder);
|
|
17
|
+
|
|
18
|
+
ifEl = el.content ? frag(el) : el;
|
|
19
|
+
ifEl[_state] = null; // mark el as fake-spraed to hold-on init, since we sprae rest when branch matches
|
|
20
|
+
|
|
21
|
+
// FIXME: instead of nextEl / el we should use elseEl / ifEl
|
|
22
|
+
if (nextEl?.hasAttribute(":else")) {
|
|
23
|
+
nextEl.removeAttribute(":else");
|
|
24
|
+
// if nextEl is :else :if - leave it for its own :if handler
|
|
25
|
+
if (!nextEl.hasAttribute(":if")) (nextEl.remove(), (elseEl = nextEl.content ? frag(nextEl) : nextEl), (elseEl[_state] = null));
|
|
26
|
+
} else nextEl = null;
|
|
27
|
+
|
|
28
|
+
return (value, newEl = el[_prevIf] ? null : value ? ifEl : elseEl) => {
|
|
29
|
+
if (nextEl) nextEl[_prevIf] = el[_prevIf] || newEl == ifEl;
|
|
30
|
+
if (curEl != newEl) {
|
|
31
|
+
// disable effects on child elements when element is not matched
|
|
32
|
+
if (curEl) (curEl.remove(), curEl[_off]?.());
|
|
33
|
+
if ((curEl = newEl)) {
|
|
34
|
+
holder.before(curEl.content || curEl);
|
|
35
|
+
// remove fake memo to sprae as new
|
|
36
|
+
if (curEl[_state] === null) {
|
|
37
|
+
delete curEl[_state];
|
|
38
|
+
sprae(curEl, state);
|
|
39
|
+
} else {
|
|
40
|
+
// enable effects if branch is matched
|
|
41
|
+
curEl[_on]();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { dir, parse } from "../core.js";
|
|
2
|
+
import { setter } from "../store.js";
|
|
3
|
+
|
|
4
|
+
dir("ref", (el, state, expr) => {
|
|
5
|
+
const refTarget = parse(expr)(state);
|
|
6
|
+
|
|
7
|
+
if (typeof refTarget === "function") {
|
|
8
|
+
return (fn) => fn?.call(null, el);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
setter(expr)(state, el);
|
|
12
|
+
return (v) => v;
|
|
13
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { dir } from "../core.js";
|
|
2
|
+
|
|
3
|
+
dir("style", (el) => {
|
|
4
|
+
const initial = el.getAttribute("style") || "";
|
|
5
|
+
|
|
6
|
+
return (value) => {
|
|
7
|
+
if (typeof value === "string") {
|
|
8
|
+
const needsSep = initial && !initial.trim().endsWith(";");
|
|
9
|
+
const next = initial + (initial ? (needsSep ? "; " : "") : "") + value;
|
|
10
|
+
el.setAttribute("style", next);
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if (initial) el.setAttribute("style", initial);
|
|
15
|
+
|
|
16
|
+
for (const key in value) {
|
|
17
|
+
const val = value[key];
|
|
18
|
+
if (key.startsWith("-")) el.style.setProperty(key, val);
|
|
19
|
+
else el.style[key] = val;
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
});
|