sprae 8.1.2 → 9.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/core.js ADDED
@@ -0,0 +1,127 @@
1
+ import swapdom from 'swapdom'
2
+ import * as signals from 'ulive'
3
+ import justin from 'subscript/justin.js'
4
+
5
+ // polyfill
6
+ const _dispose = (Symbol.dispose ||= Symbol("dispose"));
7
+
8
+ // mark
9
+ const SPRAE = `∴`
10
+
11
+ // signals impl
12
+ export let { signal, effect, batch, computed, untracked } = signals;
13
+
14
+ // reserved directives - order matters!
15
+ export const directive = {};
16
+
17
+ // sprae element: apply directives
18
+ const memo = new WeakMap();
19
+ export default function sprae(container, values) {
20
+ if (!container.children) return // text nodes, comments etc
21
+
22
+ // repeated call can be caused by :each with new objects with old keys needs an update
23
+ if (memo.has(container)) {
24
+ const [state, effects] = memo.get(container)
25
+ // we rewrite signals instead of update, because user should have what he provided
26
+ // console.log(container, state, values)
27
+ for (let k in values) state[k] = values[k]
28
+ for (let fx of effects) fx()
29
+ }
30
+
31
+ // take over existing state instead of creating clone
32
+ const state = values || {};
33
+ const effects = [];
34
+
35
+ // init directives on element
36
+ const init = (el, parent = el.parentNode) => {
37
+ if (el.attributes) {
38
+ // init generic-name attributes second
39
+ for (let i = 0; i < el.attributes.length;) {
40
+ let attr = el.attributes[i];
41
+
42
+ if (attr.name[0] === ':') {
43
+ el.removeAttribute(attr.name);
44
+
45
+ // multiple attributes like :id:for=""
46
+ let names = attr.name.slice(1).split(':')
47
+
48
+ // NOTE: secondary directives don't stop flow nor extend state, so no need to check
49
+ for (let name of names) {
50
+ let update = (directive[name] || directive.default)(el, attr.value, state, name);
51
+ if (update) {
52
+ update[_dispose] = effect(update);
53
+ effects.push(update);
54
+ }
55
+ }
56
+
57
+ // stop if element was spraed by directive or skipped (detached) like in case of :if or :each
58
+ if (memo.has(el)) return;
59
+ if (el.parentNode !== parent) return false;
60
+ } else i++;
61
+ }
62
+ }
63
+
64
+ for (let i = 0, child; child = el.children[i]; i++) {
65
+ // if element was removed from parent (skipped) - reduce index
66
+ if (init(child, el) === false) i--;
67
+ }
68
+ };
69
+
70
+ init(container);
71
+
72
+ // if element was spraed by :scope or :each instruction - skip
73
+ if (memo.has(container)) return state// memo.get(container)
74
+
75
+ // save
76
+ memo.set(container, [state, effects]);
77
+ container.classList?.add(SPRAE); // mark spraed element
78
+
79
+ // expose dispose
80
+ container[_dispose] = () => {
81
+ while (effects.length) effects.pop()[_dispose]();
82
+ container.classList.remove(SPRAE)
83
+ memo.delete(container);
84
+ let els = container.getElementsByClassName(SPRAE);
85
+ while (els.length) els[0][_dispose]?.()
86
+ }
87
+
88
+ return state;
89
+ }
90
+
91
+ // default compiler
92
+ const evalMemo = {};
93
+
94
+ export let compile = (expr, dir, evaluate) => {
95
+ if (evaluate = evalMemo[expr = expr.trim()]) return evaluate
96
+
97
+ // static-time errors
98
+ try {
99
+ // evaluate = new Function(`__scope`, `with (__scope) { return ${expr} };`);
100
+ evaluate = justin(expr);
101
+ }
102
+ catch (e) { throw Object.assign(e, { message: `${SPRAE} ${e.message}\n\n${dir}${expr ? `="${expr}"\n\n` : ""}`, expr }) }
103
+
104
+ // runtime errors
105
+ return evalMemo[expr] = evaluate
106
+ }
107
+
108
+ // DOM swapper
109
+ export let swap = swapdom
110
+
111
+ // interpolate a$<b> fields from context
112
+ export const ipol = (v, state) => {
113
+ return v?.replace ? v.replace(/\$<([^>]+)>/g, (match, field) => state[field]?.valueOf?.() ?? '') : v
114
+ };
115
+
116
+ // configure signals/compiler/differ
117
+ // it's more compact than using sprae.signal = signal etc.
118
+ sprae.use = s => {
119
+ s.signal && (
120
+ signal = s.signal,
121
+ effect = s.effect,
122
+ computed = s.computed,
123
+ batch = s.batch || (fn => fn()),
124
+ untracked = s.untracked || batch
125
+ );
126
+ s.swap && (swap = s.swap)
127
+ }
@@ -0,0 +1,10 @@
1
+ import { directive, compile } from "../core.js";
2
+ import { attr, dashcase } from './default.js'
3
+
4
+ directive['aria'] = (el, expr, state) => {
5
+ let evaluate = compile(expr, 'aria')
6
+ const update = (value) => {
7
+ for (let key in value) attr(el, 'aria-' + dashcase(key), value[key] == null ? null : value[key] + '');
8
+ }
9
+ return () => update(evaluate(state)?.valueOf())
10
+ }
@@ -0,0 +1,17 @@
1
+ import { directive, compile, ipol } from "../core.js";
2
+
3
+ directive.class = (el, expr, state) => {
4
+ let evaluate = compile(expr, 'class');
5
+ let cur = new Set
6
+ return () => {
7
+ let v = evaluate(state);
8
+ let clsx = new Set;
9
+ if (v) {
10
+ if (typeof v === "string") ipol(v?.valueOf?.(), state).split(' ').map(cls => clsx.add(cls));
11
+ else if (Array.isArray(v)) v.map(v => (v = ipol(v?.valueOf?.(), state)) && clsx.add(v));
12
+ else Object.entries(v).map(([k, v]) => v?.valueOf?.() && clsx.add(k));
13
+ }
14
+ for (let cls of cur) if (clsx.has(cls)) clsx.delete(cls); else el.classList.remove(cls);
15
+ for (let cls of cur = clsx) el.classList.add(cls)
16
+ };
17
+ };
@@ -0,0 +1,10 @@
1
+ import { directive, compile } from "../core.js";
2
+
3
+ directive['data'] = (el, expr, state) => {
4
+ let evaluate = compile(expr, 'data')
5
+
6
+ return () => {
7
+ let value = evaluate(state)?.valueOf()
8
+ for (let key in value) el.dataset[key] = value[key];
9
+ }
10
+ }
@@ -0,0 +1,148 @@
1
+ import { directive, compile, ipol } from "../core.js";
2
+
3
+ // set generic property directive
4
+ directive.default = (el, expr, state, name) => {
5
+ let evt = name.startsWith("on") && name.slice(2);
6
+ let evaluate = compile(expr, name);
7
+
8
+ if (evt) {
9
+ let off
10
+ return () => (
11
+ off?.(), // intermediate teardown
12
+ off = on(el, evt, evaluate(state))
13
+ );
14
+ }
15
+
16
+ return () => {
17
+ let value = evaluate(state)?.valueOf();
18
+ if (name) attr(el, name, ipol(value, state))
19
+ else for (let key in value) attr(el, dashcase(key), ipol(value[key], state));
20
+ };
21
+ };
22
+
23
+
24
+ // bind event to a target
25
+ const on = (el, e, fn = () => { }) => {
26
+ const ctx = { evt: "", target: el, test: () => true };
27
+
28
+ // onevt.debounce-108 -> evt.debounce-108
29
+ ctx.evt = e.replace(
30
+ /\.(\w+)?-?([-\w]+)?/g,
31
+ (match, mod, param = "") => ((ctx.test = mods[mod]?.(ctx, ...param.split("-")) || ctx.test), ""),
32
+ );
33
+
34
+ // add listener applying the context
35
+ const { evt, target, test, defer, stop, prevent, ...opts } = ctx;
36
+
37
+ if (defer) fn = defer(fn);
38
+
39
+ const cb = (e) =>
40
+ test(e) && (stop && e.stopPropagation(), prevent && e.preventDefault(), fn.call(target, e));
41
+
42
+ target.addEventListener(evt, cb, opts);
43
+
44
+ // return off
45
+ return () => target.removeEventListener(evt, cb, opts);
46
+ };
47
+
48
+ // event modifiers
49
+ const mods = {
50
+ // actions
51
+ prevent(ctx) { ctx.prevent = true; },
52
+ stop(ctx) { ctx.stop = true; },
53
+
54
+ // options
55
+ once(ctx) { ctx.once = true; },
56
+ passive(ctx) { ctx.passive = true; },
57
+ capture(ctx) { ctx.capture = true; },
58
+
59
+ // target
60
+ window(ctx) { ctx.target = window; },
61
+ document(ctx) { ctx.target = document; },
62
+
63
+ throttle(ctx, limit) { ctx.defer = (fn) => throttle(fn, limit ? Number(limit) || 0 : 108); },
64
+ debounce(ctx, wait) { ctx.defer = (fn) => debounce(fn, wait ? Number(wait) || 0 : 108); },
65
+
66
+ // test
67
+ outside: (ctx) => (e) => {
68
+ let target = ctx.target;
69
+ if (target.contains(e.target)) return false;
70
+ if (e.target.isConnected === false) return false;
71
+ if (target.offsetWidth < 1 && target.offsetHeight < 1) return false;
72
+ return true;
73
+ },
74
+ self: (ctx) => (e) => e.target === ctx.target,
75
+
76
+ // keyboard
77
+ ctrl: (_, ...param) => (e) => keys.ctrl(e) && param.every((p) => (keys[p] ? keys[p](e) : e.key === p)),
78
+ shift: (_, ...param) => (e) => keys.shift(e) && param.every((p) => (keys[p] ? keys[p](e) : e.key === p)),
79
+ alt: (_, ...param) => (e) => keys.alt(e) && param.every((p) => (keys[p] ? keys[p](e) : e.key === p)),
80
+ meta: (_, ...param) => (e) => keys.meta(e) && param.every((p) => (keys[p] ? keys[p](e) : e.key === p)),
81
+ arrow: () => keys.arrow,
82
+ enter: () => keys.enter,
83
+ escape: () => keys.escape,
84
+ tab: () => keys.tab,
85
+ space: () => keys.space,
86
+ backspace: () => keys.backspace,
87
+ delete: () => keys.delete,
88
+ digit: () => keys.digit,
89
+ letter: () => keys.letter,
90
+ character: () => keys.character,
91
+ };
92
+
93
+ // key testers
94
+ const keys = {
95
+ ctrl: (e) => e.ctrlKey || e.key === "Control" || e.key === "Ctrl",
96
+ shift: (e) => e.shiftKey || e.key === "Shift",
97
+ alt: (e) => e.altKey || e.key === "Alt",
98
+ meta: (e) => e.metaKey || e.key === "Meta" || e.key === "Command",
99
+ arrow: (e) => e.key.startsWith("Arrow"),
100
+ enter: (e) => e.key === "Enter",
101
+ escape: (e) => e.key.startsWith("Esc"),
102
+ tab: (e) => e.key === "Tab",
103
+ space: (e) => e.key === " " || e.key === "Space" || e.key === " ",
104
+ backspace: (e) => e.key === "Backspace",
105
+ delete: (e) => e.key === "Delete",
106
+ digit: (e) => /^\d$/.test(e.key),
107
+ letter: (e) => /^[a-zA-Z]$/.test(e.key),
108
+ character: (e) => /^\S$/.test(e.key),
109
+ };
110
+
111
+ // set attr
112
+ export const attr = (el, name, v) => {
113
+ if (v == null || v === false) el.removeAttribute(name);
114
+ else el.setAttribute(name, v === true ? "" : typeof v === "number" || typeof v === "string" ? v : "");
115
+ }
116
+
117
+ // create delayed fns
118
+ const throttle = (fn, limit) => {
119
+ let pause, planned,
120
+ block = (e) => {
121
+ pause = true;
122
+ setTimeout(() => {
123
+ pause = false;
124
+ // if event happened during blocked time, it schedules call by the end
125
+ if (planned) return (planned = false), block(e), fn(e);
126
+ }, limit);
127
+ };
128
+ return (e) => {
129
+ if (pause) return (planned = true);
130
+ block(e);
131
+ return fn(e);
132
+ };
133
+ };
134
+
135
+ const debounce = (fn, wait) => {
136
+ let timeout;
137
+ return (e) => {
138
+ clearTimeout(timeout);
139
+ timeout = setTimeout(() => {
140
+ timeout = null;
141
+ fn(e);
142
+ }, wait);
143
+ };
144
+ };
145
+
146
+ export const dashcase = (str) => {
147
+ return str.replace(/[A-Z\u00C0-\u00D6\u00D8-\u00DE]/g, (match) => "-" + match.toLowerCase());
148
+ }
@@ -0,0 +1,64 @@
1
+ import sprae, { directive, compile, swap } from "../core.js";
2
+
3
+ export const _each = Symbol(":each");
4
+
5
+ const keys = {}; // boxed primitives pool
6
+
7
+ // :each must init before :ref, :id or any others, since it defines scope
8
+ directive.each = (tpl, expr, state, name) => {
9
+ let [leftSide, itemsExpr] = expr.split(/\s+in\s+/);
10
+ let [itemVar, idxVar = "_$"] = leftSide.split(/\s*,\s*/);
11
+
12
+ // we need :if to be able to replace holder instead of tpl for :if :each case
13
+ const holder = (tpl[_each] = document.createTextNode(""));
14
+ tpl.replaceWith(holder);
15
+
16
+ const evaluate = compile(itemsExpr, name);
17
+ const memo = new WeakMap;
18
+
19
+ tpl.removeAttribute(':key')
20
+
21
+ let cur = [];
22
+
23
+ return () => {
24
+ // naive approach: whenever items change we replace full list
25
+ let items = evaluate(state)?.valueOf(), els = [];
26
+ if (typeof items === "number") items = Array.from({ length: items }, (_, i) => i);
27
+
28
+ const count = new WeakSet
29
+ for (let idx in items) {
30
+ let item = items[idx]
31
+ // creating via prototype is faster in both creation time & reading time
32
+ let substate = Object.create(state, { [idxVar]: { value: idx } });
33
+ substate[itemVar] = item; // can be changed by subsequent updates, need to be writable
34
+ item = item.peek?.() ?? item; // unwrap signal
35
+ let key = item.key ?? item.id ?? item;
36
+ let el;
37
+
38
+ if (key == null) el = tpl.cloneNode(true)
39
+ else {
40
+ // make sure key is object
41
+ if (Object(key) !== key) key = (keys[key] ||= Object(key));
42
+
43
+ if (count.has(key)) {
44
+ console.warn('Duplicate key', key), el = tpl.cloneNode(true);
45
+ }
46
+ else {
47
+ console.log(key, count.has(key))
48
+ count.add(key);
49
+ el = memo.get(key) || memo.set(key, tpl.cloneNode(true)).get(key);
50
+ }
51
+ }
52
+
53
+ if (el.content) el = el.content.cloneNode(true) // <template>
54
+
55
+ sprae(el, substate)
56
+
57
+ // document fragment
58
+ if (el.nodeType === 11) els.push(...el.childNodes);
59
+ else els.push(el);
60
+ }
61
+
62
+ swap(holder.parentNode, cur, cur = els, holder);
63
+ };
64
+ };
@@ -0,0 +1,6 @@
1
+ import { directive, compile } from "../core.js";
2
+
3
+ directive.fx = (el, expr, state, name) => {
4
+ let evaluate = compile(expr, name);
5
+ return () => evaluate(state);
6
+ };
@@ -0,0 +1,11 @@
1
+ import sprae, { directive, compile } from "../core.js";
2
+
3
+ directive.html = (el, expr, state, name) => {
4
+ let evaluate = compile(expr, name), tpl = evaluate(state);
5
+
6
+ if (!tpl) return
7
+
8
+ let content = (tpl.content || tpl).cloneNode(true);
9
+ el.replaceChildren(content);
10
+ sprae(el, state);
11
+ };
@@ -0,0 +1,40 @@
1
+ import sprae, { compile, directive, swap } from "../core.js";
2
+ import { _each } from './each.js';
3
+
4
+ // :if is interchangeable with :each depending on order, :if :each or :each :if have different meanings
5
+ // as for :if :scope - :if must init first, since it is lazy, to avoid initializing component ahead of time by :scope
6
+ // we consider :scope={x} :if={x} case insignificant
7
+ const _prevIf = Symbol("if");
8
+ directive.if = (ifEl, expr, state, name) => {
9
+ let parent = ifEl.parentNode,
10
+ next = ifEl.nextElementSibling,
11
+ holder = document.createTextNode(''),
12
+
13
+ evaluate = compile(expr, name),
14
+
15
+ // actual replaceable els (takes <template>)
16
+ cur, ifs, elses, none = [];
17
+
18
+ ifEl.after(holder) // mark end of modifying section
19
+
20
+ if (ifEl.content) cur = none, ifEl.remove(), ifs = [...ifEl.content.childNodes]
21
+ else ifs = cur = [ifEl]
22
+
23
+ if (next?.hasAttribute(":else")) {
24
+ next.removeAttribute(":else");
25
+ // if next is :else :if - leave it for its own :if handler
26
+ if (next.hasAttribute(":if")) elses = none;
27
+ else next.remove(), elses = next.content ? [...next.content.childNodes] : [next];
28
+ } else elses = none
29
+
30
+ return () => {
31
+ const newEls = evaluate(state)?.valueOf() ? ifs : ifEl[_prevIf] ? none : elses;
32
+ if (next) next[_prevIf] = newEls === ifs
33
+ if (cur != newEls) {
34
+ // :if :each
35
+ if (cur[0]?.[_each]) cur = [cur[0][_each]]
36
+ swap(parent, cur, cur = newEls, holder);
37
+ for (let el of cur) sprae(el, state);
38
+ }
39
+ };
40
+ };
@@ -0,0 +1,10 @@
1
+ import { directive, ipol } from "../core.js";
2
+
3
+ // ref must be last within primaries, since that must be skipped by :each, but before secondaries
4
+ directive.ref = (el, expr, state) => {
5
+ let prev;
6
+ return () => {
7
+ if (prev) delete state[prev]
8
+ state[prev = ipol(expr, state)] = el;
9
+ }
10
+ };
@@ -0,0 +1,11 @@
1
+ import sprae, { directive, compile } from "../core.js";
2
+
3
+ // `:each` can redefine scope as `:each="a in {myScope}"`,
4
+ // same time per-item scope as `:each="..." :scope="{collapsed:true}"` is useful
5
+ directive.scope = (el, expr, rootState, name) => {
6
+ let evaluate = compile(expr, name);
7
+ // local state may contain signals that update, so we take them over
8
+ return () => {
9
+ sprae(el, { ...rootState, ...(evaluate(rootState)?.valueOf?.() || {}) });
10
+ }
11
+ };
@@ -0,0 +1,16 @@
1
+ import { directive, compile, ipol } from "../core.js";
2
+
3
+ directive.style = (el, expr, state) => {
4
+ let evaluate = compile(expr, 'style');
5
+ let initStyle = el.getAttribute("style") || "";
6
+ if (!initStyle.endsWith(";")) initStyle += "; ";
7
+
8
+ return () => {
9
+ let v = evaluate(state)?.valueOf();
10
+ if (typeof v === "string") el.setAttribute("style", initStyle + ipol(v, state));
11
+ else {
12
+ el.setAttribute("style", initStyle);
13
+ for (let k in v) el.style.setProperty(k, ipol(v[k], state));
14
+ }
15
+ };
16
+ };
@@ -0,0 +1,12 @@
1
+ import { directive, compile } from "../core.js";
2
+
3
+ // set text content
4
+ directive.text = (el, expr, state) => {
5
+ let evaluate = compile(expr, 'text');
6
+ if (el.content) el.replaceWith(el = document.createTextNode('')) // <template :text="abc"/>
7
+
8
+ return () => {
9
+ let value = evaluate(state)?.valueOf();
10
+ el.textContent = value == null ? "" : value;
11
+ };
12
+ };
@@ -0,0 +1,31 @@
1
+ import { directive, compile } from "../core.js";
2
+ import { attr } from './default.js';
3
+
4
+ // connect expr to element value
5
+ directive.value = (el, expr, state) => {
6
+ let evaluate = compile(expr, 'value');
7
+
8
+ let from, to;
9
+ let update = el.type === "text" || el.type === ""
10
+ ? (value) => el.setAttribute("value", (el.value = value == null ? "" : value))
11
+ : el.tagName === "TEXTAREA" || el.type === "text" || el.type === ""
12
+ ? (value) =>
13
+ (
14
+ // we retain selection in input
15
+ (from = el.selectionStart),
16
+ (to = el.selectionEnd),
17
+ el.setAttribute("value", (el.value = value == null ? "" : value)),
18
+ from && el.setSelectionRange(from, to)
19
+ )
20
+ : el.type === "checkbox"
21
+ ? (value) => (el.checked = value, attr(el, "checked", value))
22
+ : el.type === "select-one"
23
+ ? (value) => {
24
+ for (let option in el.options) option.removeAttribute("selected");
25
+ el.value = value;
26
+ el.selectedOptions[0]?.setAttribute("selected", "");
27
+ }
28
+ : (value) => (el.value = value);
29
+
30
+ return () => (update(evaluate(state)?.valueOf?.()));
31
+ };