@erickxavier/no-js 1.0.2 → 1.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/README.md +2 -2
- package/dist/cjs/no.js +44 -5
- package/dist/cjs/no.js.map +4 -4
- package/dist/esm/no.js +44 -5
- package/dist/esm/no.js.map +4 -4
- package/dist/iife/no.js +44 -5
- package/dist/iife/no.js.map +4 -4
- package/package.json +2 -2
- package/src/animations.js +12 -9
- package/src/context.js +28 -5
- package/src/directives/binding.js +1 -1
- package/src/directives/conditionals.js +4 -4
- package/src/directives/dnd.js +1150 -0
- package/src/directives/events.js +8 -3
- package/src/directives/http.js +17 -12
- package/src/directives/i18n.js +31 -5
- package/src/directives/loops.js +21 -3
- package/src/directives/refs.js +4 -0
- package/src/directives/state.js +3 -3
- package/src/directives/styling.js +11 -6
- package/src/directives/validation.js +5 -3
- package/src/evaluate.js +9 -3
- package/src/globals.js +27 -3
- package/src/i18n.js +80 -2
- package/src/index.js +37 -4
- package/src/registry.js +30 -0
- package/src/router.js +47 -7
package/src/directives/events.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import { evaluate, _execStatement } from "../evaluate.js";
|
|
6
6
|
import { findContext } from "../dom.js";
|
|
7
7
|
import { registerDirective } from "../registry.js";
|
|
8
|
+
import { _onDispose } from "../globals.js";
|
|
8
9
|
|
|
9
10
|
registerDirective("on:*", {
|
|
10
11
|
priority: 20,
|
|
@@ -28,14 +29,17 @@ registerDirective("on:*", {
|
|
|
28
29
|
_execStatement(expr, ctx, { $el: el });
|
|
29
30
|
});
|
|
30
31
|
updatedObserver.observe(el, { childList: true, subtree: true, characterData: true, attributes: true });
|
|
32
|
+
_onDispose(() => updatedObserver.disconnect());
|
|
31
33
|
return;
|
|
32
34
|
}
|
|
33
35
|
if (event === "error") {
|
|
34
|
-
|
|
36
|
+
const errorHandler = (e) => {
|
|
35
37
|
if (el.contains(e.target) || e.target === el) {
|
|
36
38
|
_execStatement(expr, ctx, { $el: el, $error: e.error || e.message });
|
|
37
39
|
}
|
|
38
|
-
}
|
|
40
|
+
};
|
|
41
|
+
window.addEventListener("error", errorHandler);
|
|
42
|
+
_onDispose(() => window.removeEventListener("error", errorHandler));
|
|
39
43
|
return;
|
|
40
44
|
}
|
|
41
45
|
if (event === "unmounted") {
|
|
@@ -51,7 +55,8 @@ registerDirective("on:*", {
|
|
|
51
55
|
}
|
|
52
56
|
});
|
|
53
57
|
if (el.parentElement)
|
|
54
|
-
observer.observe(el.parentElement, { childList: true });
|
|
58
|
+
observer.observe(el.parentElement, { childList: true, subtree: true });
|
|
59
|
+
_onDispose(() => observer.disconnect());
|
|
55
60
|
return;
|
|
56
61
|
}
|
|
57
62
|
|
package/src/directives/http.js
CHANGED
|
@@ -4,12 +4,12 @@
|
|
|
4
4
|
|
|
5
5
|
import {
|
|
6
6
|
_config,
|
|
7
|
-
_log,
|
|
8
7
|
_warn,
|
|
9
8
|
_stores,
|
|
10
9
|
_notifyStoreWatchers,
|
|
11
10
|
_emitEvent,
|
|
12
11
|
_routerInstance,
|
|
12
|
+
_onDispose,
|
|
13
13
|
} from "../globals.js";
|
|
14
14
|
import { createContext } from "../context.js";
|
|
15
15
|
import { evaluate, _execStatement, _interpolate } from "../evaluate.js";
|
|
@@ -123,16 +123,20 @@ for (const method of HTTP_METHODS) {
|
|
|
123
123
|
const savedRetryDelay = _config.retryDelay;
|
|
124
124
|
_config.retries = retryCount;
|
|
125
125
|
_config.retryDelay = retryDelay;
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
126
|
+
let data;
|
|
127
|
+
try {
|
|
128
|
+
data = await _doFetch(
|
|
129
|
+
resolvedUrl,
|
|
130
|
+
method,
|
|
131
|
+
reqBody,
|
|
132
|
+
extraHeaders,
|
|
133
|
+
el,
|
|
134
|
+
_activeAbort.signal,
|
|
135
|
+
);
|
|
136
|
+
} finally {
|
|
137
|
+
_config.retries = savedRetries;
|
|
138
|
+
_config.retryDelay = savedRetryDelay;
|
|
139
|
+
}
|
|
136
140
|
|
|
137
141
|
// Cache response
|
|
138
142
|
if (method === "get") _cacheSet(cacheKey, data, cacheStrategy);
|
|
@@ -281,7 +285,8 @@ for (const method of HTTP_METHODS) {
|
|
|
281
285
|
|
|
282
286
|
// Polling
|
|
283
287
|
if (refreshInterval > 0) {
|
|
284
|
-
setInterval(doRequest, refreshInterval);
|
|
288
|
+
const id = setInterval(doRequest, refreshInterval);
|
|
289
|
+
_onDispose(() => clearInterval(id));
|
|
285
290
|
}
|
|
286
291
|
},
|
|
287
292
|
});
|
package/src/directives/i18n.js
CHANGED
|
@@ -1,30 +1,56 @@
|
|
|
1
1
|
// ═══════════════════════════════════════════════════════════════════════
|
|
2
2
|
// DIRECTIVE: t (i18n translations)
|
|
3
|
+
// DIRECTIVE: i18n-ns (load namespace before children)
|
|
3
4
|
// ═══════════════════════════════════════════════════════════════════════
|
|
4
5
|
|
|
5
|
-
import { _i18n, _watchI18n } from "../i18n.js";
|
|
6
|
+
import { _i18n, _watchI18n, _loadI18nNamespace, _notifyI18n } from "../i18n.js";
|
|
7
|
+
import { _watchExpr } from "../globals.js";
|
|
6
8
|
import { evaluate } from "../evaluate.js";
|
|
7
9
|
import { findContext } from "../dom.js";
|
|
8
|
-
import { registerDirective } from "../registry.js";
|
|
10
|
+
import { registerDirective, processTree } from "../registry.js";
|
|
9
11
|
|
|
10
12
|
registerDirective("t", {
|
|
11
13
|
priority: 20,
|
|
12
14
|
init(el, name, key) {
|
|
13
15
|
const ctx = findContext(el);
|
|
16
|
+
const useHtml = el.hasAttribute("t-html");
|
|
14
17
|
|
|
15
18
|
function update() {
|
|
16
19
|
const params = {};
|
|
17
20
|
for (const attr of [...el.attributes]) {
|
|
18
|
-
if (attr.name.startsWith("t-") && attr.name !== "t") {
|
|
21
|
+
if (attr.name.startsWith("t-") && attr.name !== "t" && attr.name !== "t-html") {
|
|
19
22
|
const paramName = attr.name.replace("t-", "");
|
|
20
23
|
params[paramName] = evaluate(attr.value, ctx) ?? attr.value;
|
|
21
24
|
}
|
|
22
25
|
}
|
|
23
|
-
|
|
26
|
+
const text = _i18n.t(key, params);
|
|
27
|
+
if (useHtml) {
|
|
28
|
+
el.innerHTML = text;
|
|
29
|
+
} else {
|
|
30
|
+
el.textContent = text;
|
|
31
|
+
}
|
|
24
32
|
}
|
|
25
33
|
|
|
26
|
-
ctx
|
|
34
|
+
_watchExpr(key, ctx, update);
|
|
27
35
|
_watchI18n(update);
|
|
28
36
|
update();
|
|
29
37
|
},
|
|
30
38
|
});
|
|
39
|
+
|
|
40
|
+
registerDirective("i18n-ns", {
|
|
41
|
+
priority: 1,
|
|
42
|
+
init(el, name, ns) {
|
|
43
|
+
// Empty ns = marker attribute (e.g. route-view); skip loading
|
|
44
|
+
if (!ns) return;
|
|
45
|
+
|
|
46
|
+
// Save children to prevent premature t resolution
|
|
47
|
+
const saved = document.createDocumentFragment();
|
|
48
|
+
while (el.firstChild) saved.appendChild(el.firstChild);
|
|
49
|
+
|
|
50
|
+
_loadI18nNamespace(ns).then(() => {
|
|
51
|
+
el.appendChild(saved);
|
|
52
|
+
processTree(el);
|
|
53
|
+
_notifyI18n();
|
|
54
|
+
});
|
|
55
|
+
},
|
|
56
|
+
});
|
package/src/directives/loops.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// ═══════════════════════════════════════════════════════════════════════
|
|
4
4
|
|
|
5
5
|
import { createContext } from "../context.js";
|
|
6
|
+
import { _watchExpr } from "../globals.js";
|
|
6
7
|
import { evaluate, resolve } from "../evaluate.js";
|
|
7
8
|
import { findContext, _cloneTemplate } from "../dom.js";
|
|
8
9
|
import { registerDirective, processTree } from "../registry.js";
|
|
@@ -23,11 +24,26 @@ registerDirective("each", {
|
|
|
23
24
|
const animLeave = el.getAttribute("animate-leave");
|
|
24
25
|
const stagger = parseInt(el.getAttribute("animate-stagger")) || 0;
|
|
25
26
|
const animDuration = parseInt(el.getAttribute("animate-duration")) || 0;
|
|
27
|
+
let prevList = null;
|
|
26
28
|
|
|
27
29
|
function update() {
|
|
28
|
-
let list =
|
|
30
|
+
let list = /[\[\]()\s+\-*\/!?:&|]/.test(listPath)
|
|
31
|
+
? evaluate(listPath, ctx)
|
|
32
|
+
: resolve(listPath, ctx);
|
|
29
33
|
if (!Array.isArray(list)) return;
|
|
30
34
|
|
|
35
|
+
// If same list reference and items are rendered, skip re-render
|
|
36
|
+
// and just propagate the notification to child contexts so their
|
|
37
|
+
// watchers (bind, show, model, etc.) can react to parent changes
|
|
38
|
+
// without destroying/recreating the DOM (preserves input focus).
|
|
39
|
+
if (list === prevList && list.length > 0 && el.children.length > 0) {
|
|
40
|
+
for (const child of el.children) {
|
|
41
|
+
if (child.__ctx && child.__ctx.$notify) child.__ctx.$notify();
|
|
42
|
+
}
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
prevList = list;
|
|
46
|
+
|
|
31
47
|
// Empty state
|
|
32
48
|
if (list.length === 0 && elseTpl) {
|
|
33
49
|
const clone = _cloneTemplate(elseTpl);
|
|
@@ -90,6 +106,7 @@ registerDirective("each", {
|
|
|
90
106
|
const firstChild = wrapper.firstElementChild;
|
|
91
107
|
if (firstChild) {
|
|
92
108
|
firstChild.classList.add(animEnter);
|
|
109
|
+
firstChild.addEventListener("animationend", () => firstChild.classList.remove(animEnter), { once: true });
|
|
93
110
|
// Stagger animation — delay must be on the child, not the wrapper
|
|
94
111
|
if (stagger) {
|
|
95
112
|
firstChild.style.animationDelay = i * stagger + "ms";
|
|
@@ -99,7 +116,7 @@ registerDirective("each", {
|
|
|
99
116
|
});
|
|
100
117
|
}
|
|
101
118
|
|
|
102
|
-
ctx
|
|
119
|
+
_watchExpr(expr, ctx, update);
|
|
103
120
|
update();
|
|
104
121
|
},
|
|
105
122
|
});
|
|
@@ -204,6 +221,7 @@ registerDirective("foreach", {
|
|
|
204
221
|
const firstChild = wrapper.firstElementChild;
|
|
205
222
|
if (firstChild) {
|
|
206
223
|
firstChild.classList.add(animEnter);
|
|
224
|
+
firstChild.addEventListener("animationend", () => firstChild.classList.remove(animEnter), { once: true });
|
|
207
225
|
// Stagger animation — delay must be on the child, not the wrapper
|
|
208
226
|
if (stagger) {
|
|
209
227
|
firstChild.style.animationDelay = (i * stagger) + "ms";
|
|
@@ -233,7 +251,7 @@ registerDirective("foreach", {
|
|
|
233
251
|
}
|
|
234
252
|
}
|
|
235
253
|
|
|
236
|
-
ctx
|
|
254
|
+
_watchExpr(fromPath, ctx, update);
|
|
237
255
|
update();
|
|
238
256
|
},
|
|
239
257
|
});
|
package/src/directives/refs.js
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
_refs,
|
|
7
7
|
_stores,
|
|
8
8
|
_notifyStoreWatchers,
|
|
9
|
+
_onDispose,
|
|
9
10
|
} from "../globals.js";
|
|
10
11
|
import { createContext } from "../context.js";
|
|
11
12
|
import { evaluate, _execStatement, _interpolate } from "../evaluate.js";
|
|
@@ -17,6 +18,9 @@ registerDirective("ref", {
|
|
|
17
18
|
priority: 5,
|
|
18
19
|
init(el, name, refName) {
|
|
19
20
|
_refs[refName] = el;
|
|
21
|
+
_onDispose(() => {
|
|
22
|
+
if (_refs[refName] === el) delete _refs[refName];
|
|
23
|
+
});
|
|
20
24
|
},
|
|
21
25
|
});
|
|
22
26
|
|
package/src/directives/state.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// DIRECTIVES: state, store, computed, watch
|
|
3
3
|
// ═══════════════════════════════════════════════════════════════════════
|
|
4
4
|
|
|
5
|
-
import { _stores, _log } from "../globals.js";
|
|
5
|
+
import { _stores, _log, _watchExpr } from "../globals.js";
|
|
6
6
|
import { createContext } from "../context.js";
|
|
7
7
|
import { evaluate, _execStatement } from "../evaluate.js";
|
|
8
8
|
import { findContext } from "../dom.js";
|
|
@@ -78,7 +78,7 @@ registerDirective("computed", {
|
|
|
78
78
|
const val = evaluate(expr, ctx);
|
|
79
79
|
ctx.$set(computedName, val);
|
|
80
80
|
}
|
|
81
|
-
ctx
|
|
81
|
+
_watchExpr(expr, ctx, update);
|
|
82
82
|
update();
|
|
83
83
|
},
|
|
84
84
|
});
|
|
@@ -89,7 +89,7 @@ registerDirective("watch", {
|
|
|
89
89
|
const ctx = findContext(el);
|
|
90
90
|
const onChange = el.getAttribute("on:change");
|
|
91
91
|
let lastVal = evaluate(watchExpr, ctx);
|
|
92
|
-
ctx
|
|
92
|
+
_watchExpr(watchExpr, ctx, () => {
|
|
93
93
|
const newVal = evaluate(watchExpr, ctx);
|
|
94
94
|
if (newVal !== lastVal) {
|
|
95
95
|
const oldVal = lastVal;
|
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
// DIRECTIVES: class-*, style-*
|
|
3
3
|
// ═══════════════════════════════════════════════════════════════════════
|
|
4
4
|
|
|
5
|
+
import { _watchExpr } from "../globals.js";
|
|
5
6
|
import { evaluate } from "../evaluate.js";
|
|
6
7
|
import { findContext } from "../dom.js";
|
|
7
8
|
import { registerDirective } from "../registry.js";
|
|
9
|
+
import { _watchI18n } from "../i18n.js";
|
|
8
10
|
|
|
9
11
|
registerDirective("class-*", {
|
|
10
12
|
priority: 20,
|
|
@@ -13,16 +15,18 @@ registerDirective("class-*", {
|
|
|
13
15
|
const ctx = findContext(el);
|
|
14
16
|
|
|
15
17
|
// class-map="{ active: x, bold: y }"
|
|
18
|
+
// Supports space-separated keys: class-map="{ 'bg-sky-500 text-white': x }"
|
|
16
19
|
if (suffix === "map") {
|
|
17
20
|
function update() {
|
|
18
21
|
const obj = evaluate(expr, ctx);
|
|
19
22
|
if (obj && typeof obj === "object") {
|
|
20
23
|
for (const [cls, cond] of Object.entries(obj)) {
|
|
21
|
-
|
|
24
|
+
const parts = cls.split(/\s+/).filter(Boolean);
|
|
25
|
+
parts.forEach((c) => el.classList.toggle(c, !!cond));
|
|
22
26
|
}
|
|
23
27
|
}
|
|
24
28
|
}
|
|
25
|
-
ctx
|
|
29
|
+
_watchExpr(expr, ctx, update);
|
|
26
30
|
update();
|
|
27
31
|
return;
|
|
28
32
|
}
|
|
@@ -41,7 +45,7 @@ registerDirective("class-*", {
|
|
|
41
45
|
prevClasses = next;
|
|
42
46
|
}
|
|
43
47
|
}
|
|
44
|
-
ctx
|
|
48
|
+
_watchExpr(expr, ctx, update);
|
|
45
49
|
update();
|
|
46
50
|
return;
|
|
47
51
|
}
|
|
@@ -50,7 +54,8 @@ registerDirective("class-*", {
|
|
|
50
54
|
function update() {
|
|
51
55
|
el.classList.toggle(suffix, !!evaluate(expr, ctx));
|
|
52
56
|
}
|
|
53
|
-
ctx
|
|
57
|
+
_watchExpr(expr, ctx, update);
|
|
58
|
+
if (expr.includes("$i18n") || expr.includes("NoJS.locale")) _watchI18n(update);
|
|
54
59
|
update();
|
|
55
60
|
},
|
|
56
61
|
});
|
|
@@ -71,7 +76,7 @@ registerDirective("style-*", {
|
|
|
71
76
|
}
|
|
72
77
|
}
|
|
73
78
|
}
|
|
74
|
-
ctx
|
|
79
|
+
_watchExpr(expr, ctx, update);
|
|
75
80
|
update();
|
|
76
81
|
return;
|
|
77
82
|
}
|
|
@@ -82,7 +87,7 @@ registerDirective("style-*", {
|
|
|
82
87
|
const val = evaluate(expr, ctx);
|
|
83
88
|
el.style[cssProp] = val != null ? String(val) : "";
|
|
84
89
|
}
|
|
85
|
-
ctx
|
|
90
|
+
_watchExpr(expr, ctx, update);
|
|
86
91
|
update();
|
|
87
92
|
},
|
|
88
93
|
});
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// HELPER: _validateField
|
|
4
4
|
// ═══════════════════════════════════════════════════════════════════════
|
|
5
5
|
|
|
6
|
-
import { _validators } from "../globals.js";
|
|
6
|
+
import { _validators, _onDispose } from "../globals.js";
|
|
7
7
|
import { createContext } from "../context.js";
|
|
8
8
|
import { findContext, _cloneTemplate } from "../dom.js";
|
|
9
9
|
import { registerDirective, processTree } from "../registry.js";
|
|
@@ -223,10 +223,12 @@ registerDirective("error-boundary", {
|
|
|
223
223
|
});
|
|
224
224
|
|
|
225
225
|
// Listen for window-level errors (resource load failures, etc.)
|
|
226
|
-
|
|
226
|
+
const errorHandler = (e) => {
|
|
227
227
|
if (el.contains(e.target) || el === e.target) {
|
|
228
228
|
showFallback(e.message || "An error occurred");
|
|
229
229
|
}
|
|
230
|
-
}
|
|
230
|
+
};
|
|
231
|
+
window.addEventListener("error", errorHandler);
|
|
232
|
+
_onDispose(() => window.removeEventListener("error", errorHandler));
|
|
231
233
|
},
|
|
232
234
|
});
|
package/src/evaluate.js
CHANGED
|
@@ -265,22 +265,28 @@ export function _execStatement(expr, ctx, extraVars = {}) {
|
|
|
265
265
|
// For each key in any ancestor context, find the owning context at runtime
|
|
266
266
|
// and call $set on it — so mutations inside `each` loops correctly
|
|
267
267
|
// propagate back to parent state (e.g. cart updated from a loop's on:click).
|
|
268
|
+
// Only write back values that actually changed locally, to avoid
|
|
269
|
+
// overwriting proxy mutations made by called functions.
|
|
268
270
|
const chainKeys = new Set();
|
|
269
271
|
let _wCtx = ctx;
|
|
270
272
|
while (_wCtx && _wCtx.__isProxy) {
|
|
271
273
|
for (const k of Object.keys(_wCtx.__raw)) chainKeys.add(k);
|
|
272
274
|
_wCtx = _wCtx.$parent;
|
|
273
275
|
}
|
|
276
|
+
const origObj = {};
|
|
277
|
+
for (const k of chainKeys) {
|
|
278
|
+
if (!k.startsWith("$") && k in vals) origObj[k] = vals[k];
|
|
279
|
+
}
|
|
274
280
|
const setters = [...chainKeys]
|
|
275
281
|
.filter((k) => !k.startsWith("$"))
|
|
276
282
|
.map(
|
|
277
283
|
(k) =>
|
|
278
|
-
`{let _c=__ctx;while(_c&&_c.__isProxy){if('${k}'in _c.__raw){
|
|
284
|
+
`{let _c=__ctx;while(_c&&_c.__isProxy){if('${k}'in _c.__raw){if(typeof ${k}!=='undefined'){if(${k}!==__orig['${k}'])_c.$set('${k}',${k});else if(typeof ${k}==='object'&&${k}!==null)_c.$notify();}break;}_c=_c.$parent;}}`,
|
|
279
285
|
)
|
|
280
286
|
.join("\n");
|
|
281
287
|
|
|
282
|
-
const fn = new Function("__ctx", ...keyArr, `${expr};\n${setters}`);
|
|
283
|
-
fn(ctx, ...valArr);
|
|
288
|
+
const fn = new Function("__ctx", "__orig", ...keyArr, `${expr};\n${setters}`);
|
|
289
|
+
fn(ctx, origObj, ...valArr);
|
|
284
290
|
|
|
285
291
|
// Notify global store watchers when expression touches $store
|
|
286
292
|
if (typeof expr === "string" && expr.includes("$store")) {
|
package/src/globals.js
CHANGED
|
@@ -12,8 +12,8 @@ export const _config = {
|
|
|
12
12
|
csrf: null,
|
|
13
13
|
cache: { strategy: "none", ttl: 300000 },
|
|
14
14
|
templates: { cache: true },
|
|
15
|
-
router: { mode: "history", base: "/", scrollBehavior: "top" },
|
|
16
|
-
i18n: { defaultLocale: "en", fallbackLocale: "en", detectBrowser: false },
|
|
15
|
+
router: { mode: "history", base: "/", scrollBehavior: "top", templates: "pages", ext: ".tpl" },
|
|
16
|
+
i18n: { defaultLocale: "en", fallbackLocale: "en", detectBrowser: false, loadPath: null, ns: [], cache: true, persist: false },
|
|
17
17
|
debug: false,
|
|
18
18
|
devtools: false,
|
|
19
19
|
csp: null,
|
|
@@ -30,6 +30,15 @@ export const _cache = new Map();
|
|
|
30
30
|
export const _refs = {};
|
|
31
31
|
export let _routerInstance = null;
|
|
32
32
|
|
|
33
|
+
// ─── Lifecycle: tracks the element being processed by processElement ────────
|
|
34
|
+
// Used by ctx.$watch and _onDispose to transparently tag watchers/disposers
|
|
35
|
+
// with the owning DOM element — no changes needed in directive files.
|
|
36
|
+
export let _currentEl = null;
|
|
37
|
+
|
|
38
|
+
export function _setCurrentEl(el) {
|
|
39
|
+
_currentEl = el;
|
|
40
|
+
}
|
|
41
|
+
|
|
33
42
|
export function setRouterInstance(r) {
|
|
34
43
|
_routerInstance = r;
|
|
35
44
|
}
|
|
@@ -43,7 +52,13 @@ export function _warn(...args) {
|
|
|
43
52
|
}
|
|
44
53
|
|
|
45
54
|
export function _notifyStoreWatchers() {
|
|
46
|
-
for (const fn of _storeWatchers)
|
|
55
|
+
for (const fn of _storeWatchers) {
|
|
56
|
+
if (fn._el && !fn._el.isConnected) {
|
|
57
|
+
_storeWatchers.delete(fn);
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
fn();
|
|
61
|
+
}
|
|
47
62
|
}
|
|
48
63
|
|
|
49
64
|
export function _watchExpr(expr, ctx, fn) {
|
|
@@ -53,6 +68,15 @@ export function _watchExpr(expr, ctx, fn) {
|
|
|
53
68
|
}
|
|
54
69
|
}
|
|
55
70
|
|
|
71
|
+
// Register a dispose callback on the element currently being processed.
|
|
72
|
+
// Called from directives to clean up intervals, observers, window listeners.
|
|
73
|
+
export function _onDispose(fn) {
|
|
74
|
+
if (_currentEl) {
|
|
75
|
+
_currentEl.__disposers = _currentEl.__disposers || [];
|
|
76
|
+
_currentEl.__disposers.push(fn);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
56
80
|
export function _emitEvent(name, data) {
|
|
57
81
|
(_eventBus[name] || []).forEach((fn) => fn(data));
|
|
58
82
|
}
|
package/src/i18n.js
CHANGED
|
@@ -2,15 +2,84 @@
|
|
|
2
2
|
// i18n SYSTEM
|
|
3
3
|
// ═══════════════════════════════════════════════════════════════════════
|
|
4
4
|
|
|
5
|
-
import { _config } from "./globals.js";
|
|
5
|
+
import { _config, _warn } from "./globals.js";
|
|
6
6
|
|
|
7
7
|
const _i18nListeners = new Set();
|
|
8
|
+
export { _i18nListeners };
|
|
8
9
|
|
|
9
10
|
export function _watchI18n(fn) {
|
|
10
11
|
_i18nListeners.add(fn);
|
|
11
12
|
return () => _i18nListeners.delete(fn);
|
|
12
13
|
}
|
|
13
14
|
|
|
15
|
+
// ─── Notify all i18n listeners (shared by setter + directive) ────────
|
|
16
|
+
export function _notifyI18n() {
|
|
17
|
+
for (const fn of _i18nListeners) {
|
|
18
|
+
if (fn._el && !fn._el.isConnected) { _i18nListeners.delete(fn); continue; }
|
|
19
|
+
fn();
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// ─── Deep merge (recursive, returns new object) ─────────────────────
|
|
24
|
+
export function _deepMerge(target, source) {
|
|
25
|
+
const out = { ...target };
|
|
26
|
+
for (const key of Object.keys(source)) {
|
|
27
|
+
if (
|
|
28
|
+
source[key] && typeof source[key] === "object" && !Array.isArray(source[key]) &&
|
|
29
|
+
target[key] && typeof target[key] === "object" && !Array.isArray(target[key])
|
|
30
|
+
) {
|
|
31
|
+
out[key] = _deepMerge(target[key], source[key]);
|
|
32
|
+
} else {
|
|
33
|
+
out[key] = source[key];
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return out;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ─── Locale file cache: Map<string, object> key = "en" or "en:dashboard"
|
|
40
|
+
export const _i18nCache = new Map();
|
|
41
|
+
export const _loadedNs = new Set();
|
|
42
|
+
|
|
43
|
+
// ─── Fetch a single JSON file and merge into _i18n.locales[locale] ──
|
|
44
|
+
export async function _loadLocale(locale, ns) {
|
|
45
|
+
const cacheKey = ns ? `${locale}:${ns}` : locale;
|
|
46
|
+
if (_config.i18n.cache && _i18nCache.has(cacheKey)) return;
|
|
47
|
+
|
|
48
|
+
let url = _config.i18n.loadPath.replace("{locale}", locale);
|
|
49
|
+
if (ns) url = url.replace("{ns}", ns);
|
|
50
|
+
else if (url.includes("{ns}")) return; // no namespace to substitute
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
const res = await fetch(url);
|
|
55
|
+
if (!res.ok) { _warn(`i18n: failed to load ${url} (${res.status})`); return; }
|
|
56
|
+
const data = await res.json();
|
|
57
|
+
_i18n.locales[locale] = _deepMerge(_i18n.locales[locale] || {}, data);
|
|
58
|
+
if (_config.i18n.cache) _i18nCache.set(cacheKey, data);
|
|
59
|
+
} catch (e) {
|
|
60
|
+
_warn(`i18n: error loading ${url}`, e);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ─── Load all configured data for a locale (flat or all namespaces) ──
|
|
65
|
+
export async function _loadI18nForLocale(locale) {
|
|
66
|
+
if (!_config.i18n.loadPath) return;
|
|
67
|
+
const ns = _config.i18n.ns;
|
|
68
|
+
if (!ns.length || !_config.i18n.loadPath.includes("{ns}")) {
|
|
69
|
+
await _loadLocale(locale, null);
|
|
70
|
+
} else {
|
|
71
|
+
await Promise.all(ns.map((n) => _loadLocale(locale, n)));
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ─── Load a single namespace for current + fallback locales ──────────
|
|
76
|
+
export async function _loadI18nNamespace(ns) {
|
|
77
|
+
if (!_config.i18n.loadPath) return;
|
|
78
|
+
_loadedNs.add(ns);
|
|
79
|
+
const locales = new Set([_i18n.locale, _config.i18n.fallbackLocale]);
|
|
80
|
+
await Promise.all([...locales].map((l) => _loadLocale(l, ns)));
|
|
81
|
+
}
|
|
82
|
+
|
|
14
83
|
export const _i18n = {
|
|
15
84
|
_locale: "en",
|
|
16
85
|
locales: {},
|
|
@@ -20,7 +89,16 @@ export const _i18n = {
|
|
|
20
89
|
set locale(v) {
|
|
21
90
|
if (this._locale !== v) {
|
|
22
91
|
this._locale = v;
|
|
23
|
-
|
|
92
|
+
if (_config.i18n.persist && typeof localStorage !== "undefined") {
|
|
93
|
+
try { localStorage.setItem("nojs-locale", v); } catch (_) {}
|
|
94
|
+
}
|
|
95
|
+
if (_config.i18n.loadPath) {
|
|
96
|
+
// Load configured ns + any route-loaded ns for the new locale
|
|
97
|
+
const allNs = new Set([..._config.i18n.ns, ..._loadedNs]);
|
|
98
|
+
Promise.all([...allNs].map((n) => _loadLocale(v, n))).then(() => _notifyI18n());
|
|
99
|
+
} else {
|
|
100
|
+
_notifyI18n();
|
|
101
|
+
}
|
|
24
102
|
}
|
|
25
103
|
},
|
|
26
104
|
t(key, params = {}) {
|
package/src/index.js
CHANGED
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
setRouterInstance,
|
|
17
17
|
_log,
|
|
18
18
|
} from "./globals.js";
|
|
19
|
-
import { _i18n } from "./i18n.js";
|
|
19
|
+
import { _i18n, _loadI18nForLocale } from "./i18n.js";
|
|
20
20
|
import { createContext } from "./context.js";
|
|
21
21
|
import { evaluate, resolve } from "./evaluate.js";
|
|
22
22
|
import { findContext, _loadRemoteTemplates, _loadRemoteTemplatesPhase1, _loadRemoteTemplatesPhase2, _processTemplateIncludes } from "./dom.js";
|
|
@@ -37,6 +37,7 @@ import "./directives/events.js";
|
|
|
37
37
|
import "./directives/refs.js";
|
|
38
38
|
import "./directives/validation.js";
|
|
39
39
|
import "./directives/i18n.js";
|
|
40
|
+
import "./directives/dnd.js";
|
|
40
41
|
|
|
41
42
|
// ═══════════════════════════════════════════════════════════════════════
|
|
42
43
|
// PUBLIC API
|
|
@@ -60,6 +61,13 @@ const NoJS = {
|
|
|
60
61
|
_config.baseApiUrl = v;
|
|
61
62
|
},
|
|
62
63
|
|
|
64
|
+
get locale() {
|
|
65
|
+
return _i18n.locale;
|
|
66
|
+
},
|
|
67
|
+
set locale(v) {
|
|
68
|
+
_i18n.locale = v;
|
|
69
|
+
},
|
|
70
|
+
|
|
63
71
|
config(opts = {}) {
|
|
64
72
|
// Save nested objects before shallow assign overwrites them
|
|
65
73
|
const prevHeaders = { ..._config.headers };
|
|
@@ -85,6 +93,12 @@ const NoJS = {
|
|
|
85
93
|
root = root || document.body;
|
|
86
94
|
_log("Initializing...");
|
|
87
95
|
|
|
96
|
+
// Load external locale files (blocking — translations must be available for first paint)
|
|
97
|
+
if (_config.i18n.loadPath) {
|
|
98
|
+
const locales = new Set([_i18n.locale, _config.i18n.fallbackLocale]);
|
|
99
|
+
await Promise.all([...locales].map((l) => _loadI18nForLocale(l)));
|
|
100
|
+
}
|
|
101
|
+
|
|
88
102
|
// Inline template includes (e.g. skeletons) — synchronous, before any fetch
|
|
89
103
|
_processTemplateIncludes(root);
|
|
90
104
|
|
|
@@ -141,13 +155,32 @@ const NoJS = {
|
|
|
141
155
|
|
|
142
156
|
// i18n
|
|
143
157
|
i18n(opts) {
|
|
158
|
+
// Set config options BEFORE locale (setter checks loadPath)
|
|
159
|
+
if (opts.loadPath != null) _config.i18n.loadPath = opts.loadPath;
|
|
160
|
+
if (opts.ns) _config.i18n.ns = opts.ns;
|
|
161
|
+
if (opts.cache != null) _config.i18n.cache = opts.cache;
|
|
162
|
+
if (opts.persist != null) _config.i18n.persist = opts.persist;
|
|
144
163
|
if (opts.locales) _i18n.locales = opts.locales;
|
|
145
|
-
if (opts.defaultLocale) _i18n.locale = opts.defaultLocale;
|
|
146
164
|
if (opts.fallbackLocale) _config.i18n.fallbackLocale = opts.fallbackLocale;
|
|
165
|
+
|
|
166
|
+
// Set defaultLocale WITHOUT the setter (avoids overwriting localStorage)
|
|
167
|
+
if (opts.defaultLocale) _i18n._locale = opts.defaultLocale;
|
|
168
|
+
|
|
169
|
+
// Restore persisted locale (highest priority)
|
|
170
|
+
if (_config.i18n.persist && typeof localStorage !== "undefined") {
|
|
171
|
+
try {
|
|
172
|
+
const saved = localStorage.getItem("nojs-locale");
|
|
173
|
+
if (saved && _i18n.locales[saved]) { _i18n._locale = saved; return; }
|
|
174
|
+
} catch (_) {}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Detect browser language (second priority)
|
|
147
178
|
if (opts.detectBrowser) {
|
|
148
179
|
const browserLang =
|
|
149
180
|
typeof navigator !== "undefined" ? navigator.language : "en";
|
|
150
|
-
|
|
181
|
+
const prefix = browserLang.split("-")[0];
|
|
182
|
+
if (_i18n.locales[browserLang]) _i18n._locale = browserLang;
|
|
183
|
+
else if (_i18n.locales[prefix]) _i18n._locale = prefix;
|
|
151
184
|
}
|
|
152
185
|
},
|
|
153
186
|
|
|
@@ -183,7 +216,7 @@ const NoJS = {
|
|
|
183
216
|
resolve,
|
|
184
217
|
|
|
185
218
|
// Version
|
|
186
|
-
version: "1.0
|
|
219
|
+
version: "1.2.0",
|
|
187
220
|
};
|
|
188
221
|
|
|
189
222
|
export default NoJS;
|