@jsswift/core 1.0.41

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.
@@ -0,0 +1,270 @@
1
+ /* ===============================
2
+ ROD v3 PRO (batch + bindings + dispose hooks + key mapping)
3
+ =============================== */
4
+
5
+ function createMicrotaskScheduler() {
6
+ let queued = false;
7
+ const queue = new Set();
8
+ function flush() {
9
+ queued = false;
10
+ JSswift.debug?.inc("rodFlushes");
11
+ JSswift.perf?.inc("rodFlushes");
12
+ JSswift.perf?.mark("rod:flush");
13
+ const jobs = Array.from(queue);
14
+ queue.clear();
15
+ for (const fn of jobs) {
16
+ try { fn(); } catch (e) { console.error("[rod] flush error:", e); }
17
+ }
18
+ }
19
+ return function schedule(fn) {
20
+ queue.add(fn);
21
+ if (!queued) {
22
+ queued = true;
23
+ queueMicrotask(flush);
24
+ }
25
+ };
26
+ }
27
+ const rodSchedule = createMicrotaskScheduler();
28
+
29
+ function rodApplyBinding(el, key, value) {
30
+ if (!el) return;
31
+
32
+ if (el.nodeType === 3) {
33
+ el.textContent = value ?? "";
34
+ return;
35
+ }
36
+
37
+ const domBridge = createDomPropBridge(el, {
38
+ isSVG: el.namespaceURI === SVG_NS,
39
+ normalizeClass,
40
+ isContentProp
41
+ });
42
+ domBridge.applyBindingValue(key, value);
43
+ }
44
+
45
+ function rodCreateComponent(initialValue) {
46
+ const comp = {
47
+ type: "rod",
48
+ value: initialValue,
49
+
50
+ _bindings: [],
51
+ _actions: [],
52
+ _disposeHooks: [],
53
+
54
+ _disposed: false,
55
+ _silentDepth: 0,
56
+ _pending: false,
57
+
58
+ bindings() {
59
+ return this._bindings.map(b => ({ el: b.el, key: b.key }));
60
+ },
61
+
62
+ onDispose(fn) {
63
+ if (typeof fn === "function") this._disposeHooks.push(fn);
64
+ return this;
65
+ },
66
+
67
+ _bind(el, config = { key: "auto" }) {
68
+ if (this._disposed) return () => { };
69
+ if (!el) return () => { };
70
+
71
+ const b = { el, key: (config?.key ?? "auto") };
72
+
73
+ const exists = this._bindings.some(x => x.el === b.el && x.key === b.key);
74
+ if (!exists) this._bindings.push(b);
75
+
76
+ rodApplyBinding(b.el, b.key, this.value);
77
+
78
+ return () => {
79
+ this._bindings = this._bindings.filter(x => !(x.el === b.el && x.key === b.key));
80
+ };
81
+ },
82
+
83
+ action(fn) {
84
+ if (!this._disposed && typeof fn === "function") this._actions.push(fn);
85
+ return this;
86
+ },
87
+
88
+ _setSilent(v) {
89
+ this._silentDepth++;
90
+ try { this.value = v; } finally { this._silentDepth--; }
91
+ return this;
92
+ },
93
+
94
+ _scheduleNotify() {
95
+ if (this._disposed || this._pending) return;
96
+ this._pending = true;
97
+ rodSchedule(() => {
98
+ this._pending = false;
99
+ this._notifyNow();
100
+ });
101
+ },
102
+
103
+ _notifyNow() {
104
+ if (this._disposed) return;
105
+ const __t0 = performance.now();
106
+
107
+ JSswift.perf?.inc("rodNotifies");
108
+
109
+ this._bindings = this._bindings.filter(b => {
110
+ const el = b.el;
111
+ if (!el) return false;
112
+ if (el.nodeType === 3) return !!el.parentNode;
113
+ return el.isConnected !== false;
114
+ });
115
+
116
+ for (const b of this._bindings) {
117
+ rodApplyBinding(b.el, b.key, this.value);
118
+ }
119
+
120
+ if (this._silentDepth > 0) return;
121
+
122
+ for (const fn of this._actions) {
123
+ try { fn(this.value); } catch (e) { console.error("[rod] action error:", e); }
124
+ }
125
+ JSswift.debug?.inc("rodNotifies");
126
+ JSswift.perf?.tick("rod:notify", performance.now() - __t0, {
127
+ bindings: this._bindings?.length || 0,
128
+ actions: this._actions?.length || 0
129
+ });
130
+ },
131
+
132
+ dispose() {
133
+ if (this._disposed) return;
134
+ this._disposed = true;
135
+ for (const fn of this._disposeHooks) {
136
+ try { fn(); } catch (e) { console.error("[rod] dispose hook error:", e); }
137
+ }
138
+ this._bindings.length = 0;
139
+ this._actions.length = 0;
140
+ this._disposeHooks.length = 0;
141
+ }
142
+ };
143
+ return comp;
144
+ }
145
+
146
+ function rodMakeReactive(obj, key = "value") {
147
+ let _val = obj[key];
148
+ let _get = null;
149
+ let _set = null;
150
+ let _disposeSignal = null;
151
+
152
+ if (JSswift?.reactive?.signal) {
153
+ [_get, _set, _disposeSignal] = JSswift.reactive.signal(_val);
154
+ if (_disposeSignal && typeof obj.onDispose === "function") {
155
+ obj.onDispose(_disposeSignal);
156
+ }
157
+ }
158
+
159
+ Object.defineProperty(obj, key, {
160
+ get() { return _get ? _get() : _val; },
161
+ set(v) {
162
+ if (_set) _set(v);
163
+ else _val = v;
164
+ obj._scheduleNotify();
165
+ },
166
+ configurable: true
167
+ });
168
+
169
+ if (typeof obj.val !== "function") {
170
+ Object.defineProperty(obj, "val", {
171
+ value(v) {
172
+ if (arguments.length) {
173
+ this.value = v;
174
+ return v;
175
+ }
176
+ return this.value;
177
+ },
178
+ configurable: true
179
+ });
180
+ }
181
+
182
+ if (!obj[Symbol.toPrimitive]) {
183
+ Object.defineProperty(obj, Symbol.toPrimitive, {
184
+ value(hint) {
185
+ const v = this.value;
186
+ if (v == null) return v;
187
+ const t = typeof v;
188
+ if (hint === "number") return Number(v);
189
+ if (t !== "symbol") trackRodInterpolation(this, v);
190
+ if (t === "string" || t === "number" || t === "boolean" || t === "bigint" || t === "symbol") return v;
191
+ const out = String(v);
192
+ return out;
193
+ },
194
+ configurable: true
195
+ });
196
+ }
197
+
198
+ if (!obj.valueOf) {
199
+ Object.defineProperty(obj, "valueOf", {
200
+ value() { return this.value; },
201
+ configurable: true
202
+ });
203
+ }
204
+
205
+ if (!obj.toString) {
206
+ Object.defineProperty(obj, "toString", {
207
+ value() { return String(this.value); },
208
+ configurable: true
209
+ });
210
+ }
211
+
212
+ return obj;
213
+ }
214
+
215
+ _.rod = function _rod(data, key = "value") {
216
+ const comp = rodCreateComponent(data);
217
+ rodMakeReactive(comp, key);
218
+
219
+ JSswift.rod._all = JSswift.rod._all || new Set();
220
+ JSswift.rod._all.add(comp);
221
+
222
+ if (typeof comp.onDispose === "function") {
223
+ comp.onDispose(() => JSswift.rod._all.delete(comp));
224
+ }
225
+ return comp;
226
+ }; // legacy alias, deprecated: use `_.rod`
227
+
228
+ JSswift.rodBind = function (el, react, config = { key: "auto" }) {
229
+ JSswift.debug?.inc("rodBinds");
230
+
231
+ if (!el) {
232
+ JSswift.debug?.inc("rodBindNull");
233
+ JSswift.debug?.warn("rodBind: target element è null/undefined", config);
234
+ return () => { };
235
+ }
236
+ if (!react || react.type !== "rod") {
237
+ throw new Error("JSswift.rodBind: react must be a rod object");
238
+ }
239
+ return react._bind(el, config);
240
+ };
241
+
242
+
243
+ JSswift.rodFromSignal = function (get, set) {
244
+ const r = _.rod(get());
245
+ let syncing = false;
246
+
247
+ const stopRodToSignal = JSswift.reactive.effect(() => {
248
+ const v = r.value;
249
+ const currentSignal = JSswift.reactive.untracked(() => get());
250
+ if (syncing) return;
251
+ if (currentSignal === v) return;
252
+ syncing = true;
253
+ try { set(v); } finally { syncing = false; }
254
+ });
255
+
256
+ const stopEffect = JSswift.reactive.effect(() => {
257
+ const v = get();
258
+ const currentRod = JSswift.reactive.untracked(() => r.value);
259
+ if (currentRod === v) return;
260
+ syncing = true;
261
+ try { r._setSilent(v); } finally { syncing = false; }
262
+ });
263
+
264
+ if (typeof r.onDispose === "function") {
265
+ r.onDispose(stopRodToSignal);
266
+ r.onDispose(stopEffect);
267
+ }
268
+
269
+ return r;
270
+ };
@@ -0,0 +1,157 @@
1
+ // ===============================
2
+ // Two-way binding: input <-> rod
3
+ // ===============================
4
+ // ===============================
5
+ // Two-way binding avanzato
6
+ // input / checkbox / radio / select
7
+ // ===============================
8
+
9
+ function getInputKind(el) {
10
+ if (!el || !el.tagName) return null;
11
+
12
+ const tag = el.tagName.toLowerCase();
13
+
14
+ if (tag === "input") {
15
+ const type = (el.type || "text").toLowerCase();
16
+ if (type === "checkbox") return "checkbox";
17
+ if (type === "radio") return "radio";
18
+ if (type === "number") return "number";
19
+ return "text";
20
+ }
21
+
22
+ if (tag === "select") {
23
+ return el.multiple ? "select-multiple" : "select";
24
+ }
25
+
26
+ return null;
27
+ }
28
+
29
+ JSswift.rodModel = function (target, rodObj, opts = {}) {
30
+ if (!rodObj || rodObj.type !== "rod") {
31
+ throw new Error("[JSswift.rodModel] rodObj deve essere un rod");
32
+ }
33
+
34
+ // Supporta singolo elemento o lista (radio)
35
+ const elements = Array.isArray(target) || target instanceof NodeList
36
+ ? Array.from(target)
37
+ : [target];
38
+
39
+ const eventName = opts.event ?? "change";
40
+ const parse = opts.parse ?? (v => v);
41
+ const format = opts.format ?? (v => v);
42
+
43
+ let syncing = false;
44
+ const disposers = [];
45
+
46
+ // === ROD → DOM ===
47
+ const updateDOM = (value) => {
48
+ for (const el of elements) {
49
+ if (!el) continue;
50
+ const kind = getInputKind(el);
51
+
52
+ switch (kind) {
53
+ case "checkbox":
54
+ el.checked = !!value;
55
+ break;
56
+
57
+ case "radio":
58
+ el.checked = el.value == value;
59
+ break;
60
+
61
+ case "select":
62
+ el.value = value ?? "";
63
+ break;
64
+
65
+ case "select-multiple":
66
+ const arr = Array.isArray(value) ? value.map(String) : [];
67
+ for (const opt of el.options) {
68
+ opt.selected = arr.includes(opt.value);
69
+ }
70
+ break;
71
+
72
+ case "number":
73
+ case "text":
74
+ default:
75
+ el.value = value ?? "";
76
+ }
77
+ }
78
+ };
79
+
80
+ // bind rod → DOM (usa rod.action)
81
+ rodObj.action((v) => {
82
+ if (syncing) return;
83
+ syncing = true;
84
+ try {
85
+ updateDOM(format(v));
86
+ } finally {
87
+ syncing = false;
88
+ }
89
+ });
90
+
91
+ // sync iniziale
92
+ updateDOM(format(rodObj.value));
93
+
94
+ // === DOM → ROD ===
95
+ for (const el of elements) {
96
+ if (!el) continue;
97
+
98
+ const kind = getInputKind(el);
99
+
100
+ const handler = () => {
101
+ if (syncing) return;
102
+
103
+ let next;
104
+
105
+ switch (kind) {
106
+ case "checkbox":
107
+ next = el.checked;
108
+ break;
109
+
110
+ case "radio":
111
+ if (!el.checked) return;
112
+ next = el.value;
113
+ break;
114
+
115
+ case "select":
116
+ next = el.value;
117
+ break;
118
+
119
+ case "select-multiple":
120
+ next = Array.from(el.selectedOptions).map(o => o.value);
121
+ break;
122
+
123
+ case "number":
124
+ next = parse(el.value === "" ? null : Number(el.value));
125
+ break;
126
+
127
+ case "text":
128
+ default:
129
+ next = parse(el.value);
130
+ }
131
+
132
+ syncing = true;
133
+ try {
134
+ if (rodObj.value !== next) rodObj.value = next;
135
+ } finally {
136
+ syncing = false;
137
+ }
138
+ };
139
+
140
+ el.addEventListener(eventName, handler);
141
+ disposers.push(() => el.removeEventListener(eventName, handler));
142
+ }
143
+
144
+ // cleanup automatico
145
+ if (typeof rodObj.onDispose === "function") {
146
+ rodObj.onDispose(() => disposers.forEach(fn => fn()));
147
+ }
148
+
149
+ return () => disposers.forEach(fn => fn());
150
+ };
151
+
152
+
153
+ // input <-> signal (sugar)
154
+ JSswift.signalModel = function (inputEl, get, set, opts = {}) {
155
+ const r = JSswift.rodFromSignal(get, set);
156
+ return JSswift.rodModel(inputEl, r, opts);
157
+ };
@@ -0,0 +1,84 @@
1
+ // ===============================
2
+ // Lifecycle helpers
3
+ // ===============================
4
+
5
+ function cleanupNodeTree(node) {
6
+ if (!node) return;
7
+
8
+ const disposers = JSswift._cleanupRegistry.get(node);
9
+ if (disposers) {
10
+ for (const d of disposers) {
11
+ try { d(); } catch (e) { console.error("[cleanup] error:", e); }
12
+ }
13
+ JSswift._cleanupRegistry.delete(node);
14
+ }
15
+
16
+ if (node.childNodes && node.childNodes.length) {
17
+ for (const child of node.childNodes) {
18
+ cleanupNodeTree(child);
19
+ }
20
+ }
21
+ }
22
+
23
+ function toMountTargets(target) {
24
+ const toEl = (t) => (typeof t === "string" ? JSswift.dom.q(t) : t);
25
+ return Array.isArray(target) ? target.map(toEl).filter(Boolean) : [toEl(target)].filter(Boolean);
26
+ }
27
+
28
+ function normalizeMountContent(content) {
29
+ const nodes = [];
30
+ const disposers = [];
31
+
32
+ const add = (value) => {
33
+ if (value == null) return;
34
+
35
+ if (typeof value === "function") {
36
+ add(value());
37
+ return;
38
+ }
39
+
40
+ if (value && typeof value === "object" && "node" in value) {
41
+ if (typeof value.dispose === "function") disposers.push(value.dispose);
42
+ add(value.node);
43
+ return;
44
+ }
45
+
46
+ if (Array.isArray(value)) {
47
+ for (const item of value) add(item);
48
+ return;
49
+ }
50
+
51
+ if (typeof value === "string" || typeof value === "number") {
52
+ nodes.push(document.createTextNode(String(value)));
53
+ return;
54
+ }
55
+
56
+ if (value.nodeType) {
57
+ nodes.push(value);
58
+ return;
59
+ }
60
+
61
+ console.warn("[JSswift.mount] contenuto non supportato:", value);
62
+ };
63
+
64
+ add(content);
65
+ return { nodes, disposers };
66
+ }
67
+
68
+ function createOnceDisposer(disposers = [], label = "[JSswift.mount] dispose error:") {
69
+ let done = false;
70
+ return () => {
71
+ if (done) return;
72
+ done = true;
73
+ for (const d of disposers) {
74
+ try { d(); } catch (e) { console.error(label, e); }
75
+ }
76
+ };
77
+ }
78
+
79
+ function createComponentDisposer(disposers = [], userDispose = null) {
80
+ return createOnceDisposer([
81
+ ...(userDispose ? [userDispose] : []),
82
+ ...disposers
83
+ ], "[component] dispose error:");
84
+ }
@@ -0,0 +1,131 @@
1
+ // ===============================
2
+ // JSswift.mount (Node | array | string | function)
3
+ // ===============================
4
+ // ===============================
5
+ // JSswift.mount (target singolo o array) + component cleanup
6
+ // content può essere:
7
+ // - Node | array | string/number
8
+ // - function -> uno dei precedenti
9
+ // - { node, dispose }
10
+ // - function -> { node, dispose } (component instance)
11
+ // ===============================
12
+ JSswift.mount = function (target, content, opts = {}) {
13
+ const targets = toMountTargets(target);
14
+
15
+ const clear = opts.clear ?? true;
16
+ const isMulti = targets.length > 1;
17
+
18
+ if (targets.length === 0) {
19
+ console.warn("[JSswift.mount] nessun target valido:", target);
20
+ return () => { };
21
+ }
22
+
23
+ const mounted = []; // [{ root, nodes, disposers }]
24
+
25
+ for (const root of targets) {
26
+ if (clear) {
27
+ while (root.firstChild) {
28
+ cleanupNodeTree(root.firstChild);
29
+ root.removeChild(root.firstChild);
30
+ }
31
+ }
32
+
33
+ // per multi-target: se non passi una function, cloneremo i nodi (ma NON possiamo clonare cleanup)
34
+ // quindi: consigliamo content come function quando mounti su più target.
35
+ const { nodes: rawNodes, disposers } = normalizeMountContent(content);
36
+
37
+ let nodes = rawNodes;
38
+
39
+ if (isMulti && typeof content !== "function") {
40
+ nodes = rawNodes.map(n => n.cloneNode(true));
41
+ if (disposers.length) {
42
+ console.warn("[JSswift.mount] multi-target con dispose: usa content come function per istanze separate.");
43
+ }
44
+ }
45
+
46
+ const disposeMounted = createOnceDisposer(disposers);
47
+
48
+ for (const n of nodes) root.appendChild(n);
49
+
50
+ mounted.push({ root, nodes, dispose: disposeMounted });
51
+
52
+ // registra cleanup automatico per ogni nodo root montato
53
+ for (const n of nodes) {
54
+ if (!n || !n.nodeType) continue;
55
+ JSswift._registerCleanup(n, disposeMounted);
56
+ }
57
+ }
58
+
59
+ // unmount: rimuove DOM e chiama cleanup
60
+ const unmount = () => {
61
+ for (const m of mounted) {
62
+ // remove nodes
63
+ for (const n of m.nodes) {
64
+ if (!n) continue;
65
+ cleanupNodeTree(n);
66
+ if (n.parentNode === m.root) m.root.removeChild(n);
67
+ }
68
+ m.dispose?.();
69
+ }
70
+ };
71
+
72
+ return unmount;
73
+ };
74
+
75
+ // ===============================
76
+ // JSswift.component (istanza con cleanup)
77
+ // ===============================
78
+ JSswift.component = function (renderFn) {
79
+ if (typeof renderFn !== "function") throw new Error("JSswift.component: renderFn must be a function");
80
+
81
+ // ritorna una factory: props -> { node(s), dispose }
82
+ return function ComponentInstance(props = {}) {
83
+ const disposers = [];
84
+
85
+ const ctx = {
86
+ onDispose(fn) {
87
+ if (typeof fn === "function") disposers.push(fn);
88
+ }
89
+ };
90
+
91
+ const out = renderFn(props, ctx);
92
+
93
+ // supporta:
94
+ // - Node / array / string/number
95
+ // - { node, dispose }
96
+ if (out && typeof out === "object" && "node" in out) {
97
+ return {
98
+ node: out.node,
99
+ dispose: createComponentDisposer(disposers, typeof out.dispose === "function" ? out.dispose : null)
100
+ };
101
+ }
102
+
103
+ return {
104
+ node: out,
105
+ dispose: createComponentDisposer(disposers)
106
+ };
107
+ };
108
+ };
109
+
110
+ // ===============================
111
+ // Auto cleanup observer (opt-in)
112
+ // ===============================
113
+ JSswift.enableAutoCleanup = function () {
114
+ if (JSswift._autoCleanupEnabled) return;
115
+ JSswift._autoCleanupEnabled = true;
116
+
117
+ const observer = new MutationObserver((mutations) => {
118
+ for (const m of mutations) {
119
+ for (const node of m.removedNodes) {
120
+ cleanupNodeTree(node);
121
+ }
122
+ }
123
+ });
124
+
125
+ observer.observe(document.body, {
126
+ childList: true,
127
+ subtree: true
128
+ });
129
+
130
+ JSswift._cleanupObserver = observer;
131
+ };