@panphora/sapjs 0.2.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 +350 -0
- package/dist/sap.js +2490 -0
- package/dist/sap.min.js +6 -0
- package/package.json +52 -0
- package/src/actions.js +310 -0
- package/src/carrier.js +128 -0
- package/src/compile.js +91 -0
- package/src/control-serialize.js +85 -0
- package/src/debug.js +0 -0
- package/src/dom.js +182 -0
- package/src/errors.js +190 -0
- package/src/helpers.js +119 -0
- package/src/lint.js +229 -0
- package/src/mount.js +84 -0
- package/src/mutation-bridge.js +195 -0
- package/src/pass.js +472 -0
- package/src/platform.js +78 -0
- package/src/sap.js +178 -0
- package/src/scheduler.js +65 -0
- package/src/scope.js +265 -0
package/src/sap.js
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
// The public entry. Wires the scheduler, the Sap(el) accessor, the delegated
|
|
2
|
+
// action layer, the console surface, and the optional platform bridges, then
|
|
3
|
+
// auto-mounts every [sap] root on DOMContentLoaded.
|
|
4
|
+
//
|
|
5
|
+
// Sap(el) -> live write-through proxy onto el's scope/row
|
|
6
|
+
// Sap.refresh() -> synchronous full pass (set state, refresh, read on next line)
|
|
7
|
+
// Sap.batch(l, fn) -> one labeled undo entry
|
|
8
|
+
// Sap.status() -> machine-readable green-line twin
|
|
9
|
+
// Sap.report() -> JSON twin of every error
|
|
10
|
+
// Sap.why(el) -> resolution inspector
|
|
11
|
+
// Sap.debug(bool) -> per-pass paint headers
|
|
12
|
+
// Sap.doctor() -> full-page audit
|
|
13
|
+
// Sap.formats.x -> register a format
|
|
14
|
+
// Sap.mount(root?) -> mount a root (or all) explicitly
|
|
15
|
+
|
|
16
|
+
import { createScheduler } from "./scheduler.js";
|
|
17
|
+
import { runPass } from "./pass.js";
|
|
18
|
+
import { mountApp } from "./mount.js";
|
|
19
|
+
import { createAccessor } from "./scope.js";
|
|
20
|
+
import { createActions } from "./actions.js";
|
|
21
|
+
import { createDebug } from "./debug.js";
|
|
22
|
+
import { batch, installBridges } from "./platform.js";
|
|
23
|
+
import { installMutationBridge, resetMutationBridge } from "./mutation-bridge.js";
|
|
24
|
+
import { formats } from "./helpers.js";
|
|
25
|
+
import { serializeControlToAttributes, finalizeControlForSave, rehydrateControlFromAttributes } from "./control-serialize.js";
|
|
26
|
+
|
|
27
|
+
const VERSION = "0.2.0";
|
|
28
|
+
|
|
29
|
+
const registry = new Map(); // root element -> appRec
|
|
30
|
+
const order = []; // appRecs in mount order
|
|
31
|
+
|
|
32
|
+
const scheduler = createScheduler(runPass);
|
|
33
|
+
|
|
34
|
+
function appFor(el) {
|
|
35
|
+
let cur = el;
|
|
36
|
+
while (cur) {
|
|
37
|
+
if (cur.nodeType === 1 && cur.hasAttribute && cur.hasAttribute("sap")) {
|
|
38
|
+
return registry.get(cur) || null;
|
|
39
|
+
}
|
|
40
|
+
cur = cur.parentNode && cur.parentNode.host ? cur.parentNode.host : cur.parentElement;
|
|
41
|
+
}
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Splice apps whose roots have left the DOM out of order/registry. The mutation
|
|
46
|
+
// bridge calls this each batch so a disconnected app cannot force a full-document
|
|
47
|
+
// re-scan on every change; it also fixes the pre-existing append-only order leak.
|
|
48
|
+
function pruneDisconnected() {
|
|
49
|
+
for (let i = order.length - 1; i >= 0; i--) {
|
|
50
|
+
const app = order[i];
|
|
51
|
+
if (!app.root.isConnected) {
|
|
52
|
+
registry.delete(app.root);
|
|
53
|
+
order.splice(i, 1);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function moveInto(parent, el) {
|
|
59
|
+
if (parent.moveBefore) {
|
|
60
|
+
try { parent.moveBefore(el, null); return; } catch { /* fall through */ }
|
|
61
|
+
}
|
|
62
|
+
parent.appendChild(el);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function placeBefore(el, ref) {
|
|
66
|
+
const parent = ref.parentElement;
|
|
67
|
+
if (!parent) return;
|
|
68
|
+
if (parent.moveBefore) {
|
|
69
|
+
try { parent.moveBefore(el, ref); return; } catch { /* fall through */ }
|
|
70
|
+
}
|
|
71
|
+
parent.insertBefore(el, ref);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const runtime = {
|
|
75
|
+
apps: () => order.slice(),
|
|
76
|
+
appFor,
|
|
77
|
+
isRegistered: (el) => registry.has(el),
|
|
78
|
+
pruneDisconnected,
|
|
79
|
+
schedule: (app, trigger) => scheduler.schedule(app, trigger),
|
|
80
|
+
runNow: (app, trigger) => scheduler.runNow(app, trigger),
|
|
81
|
+
moveInto,
|
|
82
|
+
placeBefore,
|
|
83
|
+
remountIfPresent: () => mountAll(),
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const accessor = createAccessor(runtime);
|
|
87
|
+
const actions = createActions(runtime, accessor);
|
|
88
|
+
const debugApi = createDebug(runtime);
|
|
89
|
+
|
|
90
|
+
let installed = false;
|
|
91
|
+
function installOnce() {
|
|
92
|
+
if (installed) return;
|
|
93
|
+
installed = true;
|
|
94
|
+
actions.install(document);
|
|
95
|
+
installBridges(runtime);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function mountAll(docRoot = document) {
|
|
99
|
+
installOnce();
|
|
100
|
+
installMutationBridge(runtime);
|
|
101
|
+
const roots = docRoot.querySelectorAll("[sap]");
|
|
102
|
+
const fresh = [];
|
|
103
|
+
for (const root of roots) {
|
|
104
|
+
if (registry.has(root)) continue;
|
|
105
|
+
const appRec = mountApp(root);
|
|
106
|
+
registry.set(root, appRec);
|
|
107
|
+
order.push(appRec);
|
|
108
|
+
fresh.push(appRec);
|
|
109
|
+
}
|
|
110
|
+
for (const app of fresh) {
|
|
111
|
+
const line = debugApi.greenLine(app);
|
|
112
|
+
if (app.diag.errors.length || app.halted) console.error(line);
|
|
113
|
+
else console.log(line);
|
|
114
|
+
}
|
|
115
|
+
return fresh;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function mount(rootOrSel) {
|
|
119
|
+
installOnce();
|
|
120
|
+
installMutationBridge(runtime);
|
|
121
|
+
if (!rootOrSel) return mountAll();
|
|
122
|
+
const root = typeof rootOrSel === "string" ? document.querySelector(rootOrSel) : rootOrSel;
|
|
123
|
+
if (!root) return null;
|
|
124
|
+
if (registry.has(root)) return registry.get(root);
|
|
125
|
+
const appRec = mountApp(root);
|
|
126
|
+
registry.set(root, appRec);
|
|
127
|
+
order.push(appRec);
|
|
128
|
+
const line = debugApi.greenLine(appRec);
|
|
129
|
+
if (appRec.diag.errors.length || appRec.halted) console.error(line);
|
|
130
|
+
else console.log(line);
|
|
131
|
+
return appRec;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// The public Sap is the accessor function with the API hung off it.
|
|
135
|
+
const Sap = accessor.Sap;
|
|
136
|
+
|
|
137
|
+
Sap.version = VERSION;
|
|
138
|
+
Sap.refresh = function refresh() {
|
|
139
|
+
for (const app of order) if (app.root.isConnected) scheduler.runNow(app, "refresh");
|
|
140
|
+
};
|
|
141
|
+
Sap.batch = batch;
|
|
142
|
+
Sap.status = debugApi.status;
|
|
143
|
+
Sap.report = debugApi.report;
|
|
144
|
+
Sap.why = debugApi.why;
|
|
145
|
+
Sap.debug = debugApi.debug;
|
|
146
|
+
Sap.doctor = debugApi.doctor;
|
|
147
|
+
Sap.greenLine = debugApi.greenLine;
|
|
148
|
+
Sap.formats = formats;
|
|
149
|
+
Sap.serializeControlToAttributes = serializeControlToAttributes;
|
|
150
|
+
Sap.finalizeControlForSave = finalizeControlForSave;
|
|
151
|
+
Sap.rehydrateControlFromAttributes = rehydrateControlFromAttributes;
|
|
152
|
+
Sap.mount = mount;
|
|
153
|
+
Sap.config = function config(options = {}) {
|
|
154
|
+
if (options.formats) Object.assign(formats, options.formats);
|
|
155
|
+
return Sap;
|
|
156
|
+
};
|
|
157
|
+
Sap._registry = registry;
|
|
158
|
+
Sap._reset = function reset() {
|
|
159
|
+
registry.clear();
|
|
160
|
+
order.length = 0;
|
|
161
|
+
resetMutationBridge();
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
function autoMount() {
|
|
165
|
+
if (typeof document === "undefined") return;
|
|
166
|
+
if (document.readyState === "loading") {
|
|
167
|
+
document.addEventListener("DOMContentLoaded", () => mountAll());
|
|
168
|
+
} else {
|
|
169
|
+
mountAll();
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
autoMount();
|
|
174
|
+
|
|
175
|
+
if (typeof window !== "undefined") window.Sap = Sap;
|
|
176
|
+
|
|
177
|
+
export default Sap;
|
|
178
|
+
export { Sap, mountAll, mount, runtime };
|
package/src/scheduler.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// The only door to a pass. A burst of N synthetic events coalesces into ONE
|
|
2
|
+
// microtask pass (the `queued` flag is the transaction). The circuit breaker
|
|
3
|
+
// stops runaway recompute loops and names the culprit from a trigger ring.
|
|
4
|
+
|
|
5
|
+
const MAX_PASSES = 50; // per macrotask, per app; a true loop trips this
|
|
6
|
+
|
|
7
|
+
export function createScheduler(runPass) {
|
|
8
|
+
let queued = false;
|
|
9
|
+
const dirty = new Set();
|
|
10
|
+
const ring = []; // recent trigger descriptions, for the breaker message
|
|
11
|
+
|
|
12
|
+
function note(trigger) {
|
|
13
|
+
if (!trigger) return;
|
|
14
|
+
ring.push(trigger);
|
|
15
|
+
if (ring.length > 8) ring.shift();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function schedule(app, trigger) {
|
|
19
|
+
note(trigger);
|
|
20
|
+
if (app._broken) return;
|
|
21
|
+
dirty.add(app);
|
|
22
|
+
if (queued) return;
|
|
23
|
+
queued = true;
|
|
24
|
+
queueMicrotask(drain);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function drain() {
|
|
28
|
+
queued = false;
|
|
29
|
+
const apps = [...dirty];
|
|
30
|
+
dirty.clear();
|
|
31
|
+
for (const app of apps) runWithBreaker(app);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function runWithBreaker(app) {
|
|
35
|
+
if (app._broken) return;
|
|
36
|
+
app._passes = (app._passes || 0) + 1;
|
|
37
|
+
if (app._passes > MAX_PASSES) {
|
|
38
|
+
app._broken = true;
|
|
39
|
+
app._breakerRing = ring.slice();
|
|
40
|
+
runPass(app, { breaker: true });
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
if (!app._resetArmed) {
|
|
44
|
+
app._resetArmed = true;
|
|
45
|
+
setTimeout(() => {
|
|
46
|
+
app._passes = 0;
|
|
47
|
+
app._resetArmed = false;
|
|
48
|
+
}, 0);
|
|
49
|
+
}
|
|
50
|
+
runPass(app);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Synchronous pass (Sap.refresh / livesync resync). Still breaker-guarded.
|
|
54
|
+
function runNow(app, trigger) {
|
|
55
|
+
note(trigger);
|
|
56
|
+
runWithBreaker(app);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function rearm(app) {
|
|
60
|
+
app._broken = false;
|
|
61
|
+
app._passes = 0;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return { schedule, runNow, rearm, ring };
|
|
65
|
+
}
|
package/src/scope.js
ADDED
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
// The Sap(el) accessor. Returns a live write-through proxy onto the scope or row
|
|
2
|
+
// that owns `el`. Reads come from the DOM (carrier or last pass); writes go through
|
|
3
|
+
// the one write path (carrier property + synthetic input/change) and schedule a pass.
|
|
4
|
+
// $-verbs ($add/$reset/$remove/$move) expose the row/list machinery to handlers.
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
nearestScopeEl, nearestItemEl, isInsideDetail, ensureId,
|
|
8
|
+
ownedBind, ownedItems, rowsOf, templateOf, resolveListEl,
|
|
9
|
+
parseStateDecl, parseTyped, serializeTyped, readTransient, writeTransient,
|
|
10
|
+
} from "./dom.js";
|
|
11
|
+
import { carrierFor } from "./carrier.js";
|
|
12
|
+
import { walkOwned } from "./dom.js";
|
|
13
|
+
import { RESERVED, HTML_GLOBALS, didYouMean } from "./errors.js";
|
|
14
|
+
|
|
15
|
+
function ownedScope(scopeEl, name) {
|
|
16
|
+
let found = null;
|
|
17
|
+
walkOwned(scopeEl, (el) => {
|
|
18
|
+
if (!found && el.getAttribute("scope") === name) found = el;
|
|
19
|
+
});
|
|
20
|
+
return found;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function declarerOf(fromEl, appRoot, field) {
|
|
24
|
+
let cur = nearestScopeEl(fromEl) || appRoot;
|
|
25
|
+
while (cur) {
|
|
26
|
+
for (const d of parseStateDecl(cur.getAttribute("state") || "")) {
|
|
27
|
+
if (d.name === field) return { el: cur, decl: d };
|
|
28
|
+
}
|
|
29
|
+
if (cur.hasAttribute("sap")) break;
|
|
30
|
+
cur = nearestScopeEl(cur.parentElement);
|
|
31
|
+
}
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function inferType(v) {
|
|
36
|
+
if (typeof v === "number") return "num";
|
|
37
|
+
if (typeof v === "boolean") return "bool";
|
|
38
|
+
return "string";
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function createAccessor(runtime) {
|
|
42
|
+
// runtime: { appFor(el), schedule(app, trigger), moveInto(parent, el) }
|
|
43
|
+
|
|
44
|
+
function contextElFor(el) {
|
|
45
|
+
const appRec = runtime.appFor(el);
|
|
46
|
+
if (!appRec) return null;
|
|
47
|
+
const detailEl = isInsideDetail(el);
|
|
48
|
+
if (detailEl) {
|
|
49
|
+
if (detailEl._sapDetailRow) return { ctxEl: detailEl._sapDetailRow, appRec, isRow: true };
|
|
50
|
+
const sEl = nearestScopeEl(detailEl.parentElement) || appRec.root;
|
|
51
|
+
return { ctxEl: sEl, appRec, isRow: false };
|
|
52
|
+
}
|
|
53
|
+
const itemEl = nearestItemEl(el);
|
|
54
|
+
if (itemEl && itemEl.hasAttribute("item")) return { ctxEl: itemEl, appRec, isRow: true };
|
|
55
|
+
const scopeEl = nearestScopeEl(el) || appRec.root;
|
|
56
|
+
return { ctxEl: scopeEl, appRec, isRow: scopeEl.hasAttribute("item") };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function Sap(elOrSel) {
|
|
60
|
+
const el = typeof elOrSel === "string" ? document.querySelector(elOrSel) : elOrSel;
|
|
61
|
+
if (!el || el.nodeType !== 1) return null;
|
|
62
|
+
const res = contextElFor(el);
|
|
63
|
+
if (!res) return null;
|
|
64
|
+
return makeProxy(res.ctxEl, res.appRec, res.isRow);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function readField(ctxEl, appRec, field) {
|
|
68
|
+
const control = ownedBind(ctxEl, field);
|
|
69
|
+
if (control) return carrierFor(control).read();
|
|
70
|
+
const listEl = ownedItems(ctxEl, field);
|
|
71
|
+
if (listEl) return rowsOf(listEl).map((r) => makeProxy(r, appRec, true));
|
|
72
|
+
const sub = ownedScope(ctxEl, field);
|
|
73
|
+
if (sub) return makeProxy(sub, appRec, false);
|
|
74
|
+
const decl = declarerOf(ctxEl, appRec.root, field);
|
|
75
|
+
if (decl) {
|
|
76
|
+
if (decl.decl.transient) return readTransient(decl.el, decl.decl);
|
|
77
|
+
return parseTyped(decl.el.getAttribute(field), decl.decl.type);
|
|
78
|
+
}
|
|
79
|
+
const snap = ctxEl._sapScope;
|
|
80
|
+
return snap ? snap[field] : undefined;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function setStateAttr(el, field, value, decl, appRec) {
|
|
84
|
+
if (decl.transient) writeTransient(el, decl, value);
|
|
85
|
+
else el.setAttribute(field, serializeTyped(value, decl.type));
|
|
86
|
+
runtime.schedule(appRec, "set:" + field);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Writable field names a typo could have meant: state declarations up the scope
|
|
90
|
+
// chain plus controls bound in the current scope.
|
|
91
|
+
function knownFieldsFor(ctxEl, appRoot) {
|
|
92
|
+
const names = new Set();
|
|
93
|
+
let cur = nearestScopeEl(ctxEl) || appRoot;
|
|
94
|
+
while (cur) {
|
|
95
|
+
for (const d of parseStateDecl(cur.getAttribute("state") || "")) names.add(d.name);
|
|
96
|
+
if (cur.hasAttribute("sap")) break;
|
|
97
|
+
cur = nearestScopeEl(cur.parentElement);
|
|
98
|
+
}
|
|
99
|
+
walkOwned(ctxEl, (el) => {
|
|
100
|
+
const b = el.getAttribute("bind");
|
|
101
|
+
if (b) names.add(b);
|
|
102
|
+
});
|
|
103
|
+
return names;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function writeField(ctxEl, appRec, field, value) {
|
|
107
|
+
const control = ownedBind(ctxEl, field);
|
|
108
|
+
if (control) {
|
|
109
|
+
carrierFor(control).write(value);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
let probe = ctxEl;
|
|
113
|
+
while (probe) {
|
|
114
|
+
for (const attr of probe.attributes) {
|
|
115
|
+
if (attr.name === "calc:" + field) {
|
|
116
|
+
appRec.diag.error("E16", probe, { attr: "calc:" + field, problem: `"${field}" is a computed field; computed fields are read-only` });
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
if (probe.hasAttribute("sap")) break;
|
|
121
|
+
probe = probe.parentElement;
|
|
122
|
+
}
|
|
123
|
+
const decl = declarerOf(ctxEl, appRec.root, field);
|
|
124
|
+
if (decl) {
|
|
125
|
+
setStateAttr(decl.el, field, value, decl.decl, appRec);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
// Undeclared field. A near-match to a known field is almost certainly a typo,
|
|
129
|
+
// so fail loud (E12) instead of auto-declaring a phantom field. A reserved or
|
|
130
|
+
// global HTML name can never be a valid state field (E15). Anything else is a
|
|
131
|
+
// genuinely new field: auto-declare on the app root (DOM-as-truth).
|
|
132
|
+
const known = knownFieldsFor(ctxEl, appRec.root);
|
|
133
|
+
known.delete(field);
|
|
134
|
+
// Short names are inherently ambiguous (x, qty), so only flag a likely typo
|
|
135
|
+
// when the name is long enough for an edit-distance match to mean something.
|
|
136
|
+
const guess = field.length >= 4 ? didYouMean(field, [...known]) : null;
|
|
137
|
+
if (guess) {
|
|
138
|
+
appRec.diag.error("E12", ctxEl, { key: field, problem: `unknown state key "${field}"`, didYouMean: guess });
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
if (RESERVED.has(field) || HTML_GLOBALS.has(field)) {
|
|
142
|
+
appRec.diag.error("E15", ctxEl, {
|
|
143
|
+
key: field,
|
|
144
|
+
problem: `cannot write undeclared field "${field}" — it is a reserved/global name and cannot become state`,
|
|
145
|
+
fix: "pick a different field name, or declare it explicitly in a state= attribute",
|
|
146
|
+
});
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
const root = appRec.root;
|
|
150
|
+
const type = inferType(value);
|
|
151
|
+
const decls = root.getAttribute("state") || "";
|
|
152
|
+
const token = type === "string" ? field : `${field}:${type}`;
|
|
153
|
+
root.setAttribute("state", decls ? `${decls} ${token}` : token);
|
|
154
|
+
setStateAttr(root, field, value, { name: field, type, transient: false }, appRec);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function freshIds(rootClone) {
|
|
158
|
+
const stamp = (el) => {
|
|
159
|
+
if (el.id) {
|
|
160
|
+
el.id = "";
|
|
161
|
+
ensureId(el, "row");
|
|
162
|
+
}
|
|
163
|
+
if (el.hasAttribute("sap-error")) el.removeAttribute("sap-error");
|
|
164
|
+
};
|
|
165
|
+
rootClone.id = "";
|
|
166
|
+
ensureId(rootClone, "row");
|
|
167
|
+
rootClone.querySelectorAll("[id]").forEach(stamp);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function doAdd(ctxEl, appRec, name) {
|
|
171
|
+
const listEl = resolveListEl(appRec.root, ctxEl, name);
|
|
172
|
+
if (!listEl) {
|
|
173
|
+
appRec.diag.error("E17", ctxEl, { problem: `cannot resolve a list named "${name}" from here` });
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
const tmpl = templateOf(listEl);
|
|
177
|
+
if (!tmpl) {
|
|
178
|
+
appRec.diag.error("E17", listEl, { problem: `items="${name}" has no [item template] to clone` });
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
const clone = tmpl.cloneNode(true);
|
|
182
|
+
clone.removeAttribute("template");
|
|
183
|
+
freshIds(clone);
|
|
184
|
+
listEl.appendChild(clone);
|
|
185
|
+
runtime.schedule(appRec, "trigger-add:" + name);
|
|
186
|
+
return makeProxy(clone, appRec, true);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function doReset(ctxEl, appRec) {
|
|
190
|
+
walkOwned(ctxEl, (el) => {
|
|
191
|
+
if (el.hasAttribute("bind")) {
|
|
192
|
+
const c = carrierFor(el);
|
|
193
|
+
const def = el.getAttribute("default");
|
|
194
|
+
if (c.kind === "checkbox" || c.kind === "radio") {
|
|
195
|
+
c.write(def != null ? def === "" || def === "true" || el.value === def : el.hasAttribute("checked"));
|
|
196
|
+
} else {
|
|
197
|
+
c.write(def != null ? def : el.getAttribute("value") || "");
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
for (const d of parseStateDecl(ctxEl.getAttribute("state") || "")) {
|
|
202
|
+
const dflt = d.default != null ? d.default : d.type === "num" ? "0" : d.type === "bool" ? "false" : "";
|
|
203
|
+
if (d.transient) writeTransient(ctxEl, d, parseTyped(dflt, d.type));
|
|
204
|
+
else ctxEl.setAttribute(d.name, dflt);
|
|
205
|
+
}
|
|
206
|
+
runtime.schedule(appRec, "trigger-reset");
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function doRemove(ctxEl, appRec) {
|
|
210
|
+
if (!ctxEl.isConnected) throw new Error("stale row handle: $remove() on a removed row");
|
|
211
|
+
ctxEl.remove();
|
|
212
|
+
runtime.schedule(appRec, "trigger-remove");
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function doMove(ctxEl, appRec, target) {
|
|
216
|
+
if (!ctxEl.isConnected) throw new Error("stale row handle: $move() on a removed row");
|
|
217
|
+
let listEl = null;
|
|
218
|
+
if (target && target.nodeType === 1) listEl = target;
|
|
219
|
+
else if (typeof target === "string") listEl = resolveListEl(appRec.root, ctxEl, target);
|
|
220
|
+
if (!listEl) {
|
|
221
|
+
appRec.diag.error("E17", ctxEl, { problem: `$move() cannot resolve target "${target}"` });
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
runtime.moveInto(listEl, ctxEl);
|
|
225
|
+
runtime.schedule(appRec, "trigger-move");
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function rowIndex(ctxEl) {
|
|
229
|
+
const list = ctxEl.parentElement;
|
|
230
|
+
if (!list) return -1;
|
|
231
|
+
return rowsOf(list).indexOf(ctxEl);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function makeProxy(ctxEl, appRec, isRow) {
|
|
235
|
+
const verbs = {
|
|
236
|
+
get $el() { return ctxEl; },
|
|
237
|
+
get $key() { return ensureId(ctxEl, isRow ? "row" : "scope"); },
|
|
238
|
+
get $index() { return rowIndex(ctxEl); },
|
|
239
|
+
$add(name) { return doAdd(ctxEl, appRec, name); },
|
|
240
|
+
$reset() { doReset(ctxEl, appRec); return proxy; },
|
|
241
|
+
$remove() { doRemove(ctxEl, appRec); },
|
|
242
|
+
$move(target) { doMove(ctxEl, appRec, target); },
|
|
243
|
+
get $appRec() { return appRec; },
|
|
244
|
+
};
|
|
245
|
+
const proxy = new Proxy(verbs, {
|
|
246
|
+
get(t, prop) {
|
|
247
|
+
if (typeof prop === "symbol") return t[prop];
|
|
248
|
+
if (prop[0] === "$") return t[prop];
|
|
249
|
+
return readField(ctxEl, appRec, prop);
|
|
250
|
+
},
|
|
251
|
+
set(t, prop, value) {
|
|
252
|
+
if (typeof prop === "string" && prop[0] === "$") { t[prop] = value; return true; }
|
|
253
|
+
writeField(ctxEl, appRec, prop, value);
|
|
254
|
+
return true;
|
|
255
|
+
},
|
|
256
|
+
has(t, prop) {
|
|
257
|
+
if (typeof prop === "string" && prop[0] === "$") return prop in t;
|
|
258
|
+
return readField(ctxEl, appRec, prop) !== undefined;
|
|
259
|
+
},
|
|
260
|
+
});
|
|
261
|
+
return proxy;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return { Sap, contextElFor, readField, writeField, makeProxy };
|
|
265
|
+
}
|