-
Notifications
You must be signed in to change notification settings - Fork 6
/
Copy pathcore.js
129 lines (100 loc) · 3.77 KB
/
core.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
import { use, effect } from "./signal.js";
import store, { _signals } from './store.js';
// polyfill
const _dispose = (Symbol.dispose ||= Symbol("dispose"));
// reserved directives - order matters!
export const directive = {};
// every element that's in cache === directly spraed and un subsequent sprae is just updated (like each)
export const memo = new WeakMap();
// sprae element: apply directives
export default function sprae(el, values) {
// text nodes, comments etc
if (!el?.childNodes) return
// repeated call can be caused by :each with new objects with old keys needs an update
if (memo.has(el)) {
// we rewrite signals instead of update, because user should have what he provided
return Object.assign(memo.get(el), values)
}
// take over existing state instead of creating clone
const state = store(values || {}), disposes = []
init(el);
// if element was spraed by :with or :each instruction - skip, otherwise save
if (!memo.has(el)) memo.set(el, state);
// disposer unspraes all internal elements
el[_dispose] = () => {
while (disposes.length) disposes.pop()();
memo.delete(el);
el[_dispose] = null;
}
return state;
function init(el, parent = el.parentNode) {
if (!el.childNodes) return // ignore text nodes, comments etc
// init generic-name attributes second
for (let i = 0; i < el.attributes?.length;) {
let attr = el.attributes[i];
if (attr.name[0] === ':') {
el.removeAttribute(attr.name);
// multiple attributes like :id:for=""
let names = attr.name.slice(1).split(':')
for (let name of names) {
let dir = directive[name] || directive.default
let evaluate = (dir.parse || parse)(attr.value)
let fn = dir(el, evaluate, state, name);
if (fn) disposes.push(effect(fn))
disposes.push(() => el.setAttributeNode(attr)) // recover attribute
}
// stop if element was spraed by internal directive
if (memo.has(el)) return el[_dispose] && disposes.push(el[_dispose])
// stop if element is skipped/detached like in case of :if or :each
if (el.parentNode !== parent) return
} else i++;
}
for (let child of [...el.childNodes])
// adjust for template container - parent is overlooked
init(child, el.content ? el.childNodes[0].parentNode : el);
};
}
// parse expression into evaluator fn
const evalMemo = {};
export const parse = (expr, dir, fn) => {
if (fn = evalMemo[expr = expr.trim()]) return fn
// static-time errors
try { fn = compile(expr) }
catch (e) { err(e, dir, expr) }
// runtime errors
return evalMemo[expr] = fn
}
// wrapped call
export const err = (e, dir, expr = '') => {
throw Object.assign(e, { message: `∴ ${e.message}\n\n${dir}${expr ? `="${expr}"\n\n` : ""}`, expr })
}
export let compile
// configure signals/compile
// it's more compact than using sprae.signal = signal etc.
sprae.use = s => {
s.signal && use(s);
s.compile && (compile = s.compile);
}
// instantiated <template> fragment holder, like persisting fragment but with minimal API surface
export const frag = (tpl) => {
if (!tpl.nodeType) return tpl // existing tpl
let content = tpl.content.cloneNode(true),
attributes = [...tpl.attributes],
ref = document.createTextNode(''),
// ensure at least one node
childNodes = (content.append(ref), [...content.childNodes])
return {
// get parentNode() { return childNodes[0].parentNode },
childNodes,
content,
remove: () => content.append(...childNodes),
replaceWith(el) {
if (el === ref) return
ref.before(el)
content.append(...childNodes)
},
attributes,
removeAttribute(name) { attributes.splice(attributes.findIndex(a => a.name === name), 1) },
setAttributeNode() { }
}
}