@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.
- package/dist/cms.js +5280 -0
- package/dist/cms.min.js +1 -0
- package/dist/min-cms.js +1 -0
- package/package.json +27 -0
- package/src/00-bootstrap.js +393 -0
- package/src/09-overlay-shared.js +162 -0
- package/src/10-overlay.js +213 -0
- package/src/15-dom-props.js +211 -0
- package/src/16-renderer-shared.js +308 -0
- package/src/17-renderer-children.js +134 -0
- package/src/18-renderer-args.js +73 -0
- package/src/20-renderer.js +575 -0
- package/src/30-rod-core.js +270 -0
- package/src/31-rod-model.js +157 -0
- package/src/32-lifecycle-helpers.js +84 -0
- package/src/32-lifecycle.js +131 -0
- package/src/33-debug-perf.js +176 -0
- package/src/34-rod-devtools.js +43 -0
- package/src/39-store-shared.js +60 -0
- package/src/40-store-core.js +203 -0
- package/src/41-store-extensions.js +194 -0
- package/src/42-plugins.js +163 -0
- package/src/42a-auth-shared.js +148 -0
- package/src/43-auth.js +171 -0
- package/src/43a-http-shared.js +234 -0
- package/src/44-http.js +157 -0
- package/src/45-route-hooks.js +55 -0
- package/src/49-router-shared.js +99 -0
- package/src/49a-ui-meta-shared.js +116 -0
- package/src/50-ui-meta.js +189 -0
- package/src/51-can.js +20 -0
- package/src/52-router.js +399 -0
- package/src/53-footer.js +142 -0
- package/src/README.md +58 -0
- package/src/modules.json +31 -0
package/src/52-router.js
ADDED
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
// -- ROUTER --
|
|
2
|
+
JSswift.router = (() => {
|
|
3
|
+
let mode = "history"; // "history" | "hash" | "auto"
|
|
4
|
+
let routes = [];
|
|
5
|
+
let outlet = null;
|
|
6
|
+
let unmountCurrent = null;
|
|
7
|
+
let base = "";
|
|
8
|
+
let beforeHook = null;
|
|
9
|
+
|
|
10
|
+
// --- Router DevTools state ---
|
|
11
|
+
let _currentCtx = null;
|
|
12
|
+
let _history = [];
|
|
13
|
+
let _tracing = false;
|
|
14
|
+
const {
|
|
15
|
+
normalizePath,
|
|
16
|
+
stripBase: stripBasePath,
|
|
17
|
+
compilePattern,
|
|
18
|
+
parseQuery,
|
|
19
|
+
matchRoute,
|
|
20
|
+
pushHistoryEntry
|
|
21
|
+
} = JSswift._routerShared;
|
|
22
|
+
|
|
23
|
+
const meta = {
|
|
24
|
+
setOutlet: { description: "imposta il contenitore del router" },
|
|
25
|
+
setBase: { description: "imposta il base path del router" },
|
|
26
|
+
add: { description: "per aggiungere la route" },
|
|
27
|
+
notFound: { description: "imposta la view 404" },
|
|
28
|
+
beforeEach: { description: "hook prima di ogni navigazione" },
|
|
29
|
+
start: { description: "avvia il router" },
|
|
30
|
+
navigate: { description: "naviga verso una route" },
|
|
31
|
+
subscribe: { description: "sottoscrive agli aggiornamenti di route" },
|
|
32
|
+
current: { description: "ritorna l'URL corrente" },
|
|
33
|
+
isActive: { description: "verifica se una route è attiva" },
|
|
34
|
+
status: { description: "stringa di stato router" },
|
|
35
|
+
inspect: { description: "stampa info diagnostiche" },
|
|
36
|
+
trace: { description: "abilita/disabilita tracing" },
|
|
37
|
+
history: { description: "ritorna la history interna" },
|
|
38
|
+
setURLOnly: { description: "aggiorna solo l'URL del browser senza render/meta" }
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
function setURLOnly(urlLike, { replace = false } = {}) {
|
|
42
|
+
const url = typeof urlLike === "string" ? new URL(urlLike, window.location.origin) : urlLike;
|
|
43
|
+
const fullPath = url.pathname + url.search + url.hash;
|
|
44
|
+
|
|
45
|
+
if (mode === "hash" || mode === "auto") {
|
|
46
|
+
if (history.pushState) {
|
|
47
|
+
const h = "#" + fullPath;
|
|
48
|
+
if (replace) history.replaceState({}, "", h);
|
|
49
|
+
else history.pushState({}, "", h);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const h = "#" + fullPath;
|
|
53
|
+
if (replace) window.location.replace(h);
|
|
54
|
+
else window.location.hash = h;
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (replace) history.replaceState({}, "", base + fullPath);
|
|
59
|
+
else history.pushState({}, "", base + fullPath);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
function getCurrentURL() {
|
|
64
|
+
if (mode === "hash") {
|
|
65
|
+
const h = window.location.hash || "#/";
|
|
66
|
+
const url = h.startsWith("#") ? h.slice(1) : h;
|
|
67
|
+
return new URL(url, window.location.origin);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// auto
|
|
71
|
+
if (mode === "auto") {
|
|
72
|
+
if (window.location.hash.startsWith("#/")) {
|
|
73
|
+
const url = window.location.hash.slice(1);
|
|
74
|
+
return new URL(url, window.location.origin);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return new URL(window.location.href);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function updateURL(url, replace = false) {
|
|
82
|
+
if (mode === "hash" || (mode === "auto" && !history.pushState)) {
|
|
83
|
+
const h = "#" + (url.pathname + url.search + url.hash);
|
|
84
|
+
if (replace) {
|
|
85
|
+
window.location.replace(h);
|
|
86
|
+
} else {
|
|
87
|
+
window.location.hash = h;
|
|
88
|
+
}
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (replace) history.replaceState({}, "", base + url.pathname + url.search + url.hash);
|
|
93
|
+
else history.pushState({}, "", base + url.pathname + url.search + url.hash);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function setMode(m) {
|
|
97
|
+
mode = m || "history";
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function stripBase(path) {
|
|
101
|
+
return stripBasePath(path, base);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function isActive(path) {
|
|
105
|
+
if (!path) return false;
|
|
106
|
+
const currentPath = _currentCtx ? _currentCtx.path : stripBase(getCurrentURL().pathname);
|
|
107
|
+
let pathname = path;
|
|
108
|
+
|
|
109
|
+
if (typeof pathname === "string" && (pathname.includes("?") || pathname.includes("#") || pathname.includes("://"))) {
|
|
110
|
+
pathname = new URL(pathname, window.location.origin).pathname;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const target = stripBase(pathname);
|
|
114
|
+
if (!target.includes(":")) return target === currentPath;
|
|
115
|
+
const compiled = compilePattern(target);
|
|
116
|
+
return compiled.regex.test(currentPath);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async function render(urlLike, { replace = false } = {}) {
|
|
120
|
+
if (!outlet) {
|
|
121
|
+
console.warn("[router] outlet non impostato. Usa JSswift.router.setOutlet('#app').");
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const url = typeof urlLike === "string" ? new URL(urlLike, window.location.origin) : urlLike;
|
|
126
|
+
if (_tracing) {
|
|
127
|
+
console.log("[router.navigate]", url.toString());
|
|
128
|
+
}
|
|
129
|
+
const fullPath = url.pathname + url.search + url.hash;
|
|
130
|
+
|
|
131
|
+
// aggiorna address bar
|
|
132
|
+
updateURL(url, replace);
|
|
133
|
+
|
|
134
|
+
const pathname = stripBase(url.pathname);
|
|
135
|
+
const m = matchRoute(pathname, routes);
|
|
136
|
+
|
|
137
|
+
const ctx = {
|
|
138
|
+
path: pathname,
|
|
139
|
+
fullPath,
|
|
140
|
+
params: m ? m.params : {},
|
|
141
|
+
query: parseQuery(url.search),
|
|
142
|
+
hash: url.hash || "",
|
|
143
|
+
navigate, // per navigare dentro view
|
|
144
|
+
outlet
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
// beforeEach (redirect/block)
|
|
148
|
+
if (beforeHook) {
|
|
149
|
+
const res = await beforeHook(ctx);
|
|
150
|
+
if (res === false) return; // blocca
|
|
151
|
+
if (typeof res === "string") { // redirect
|
|
152
|
+
navigate(res, { replace: true });
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// unmount view precedente
|
|
158
|
+
if (typeof unmountCurrent === "function") {
|
|
159
|
+
try { unmountCurrent(); } catch (e) { console.error("[router] unmount error:", e); }
|
|
160
|
+
unmountCurrent = null;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// 404
|
|
164
|
+
if (!m) {
|
|
165
|
+
const view404 = routes._notFound;
|
|
166
|
+
if (view404) {
|
|
167
|
+
unmountCurrent = JSswift.mount(outlet, () => view404(ctx), { clear: true });
|
|
168
|
+
} else {
|
|
169
|
+
unmountCurrent = JSswift.mount(outlet, _.div("404"), { clear: true });
|
|
170
|
+
}
|
|
171
|
+
notifyRoute(ctx);
|
|
172
|
+
_currentCtx = ctx;
|
|
173
|
+
pushHistoryEntry(_history, ctx);
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// mount view matchata
|
|
178
|
+
// mount view matchata (supporta nested)
|
|
179
|
+
const view = m.route.view;
|
|
180
|
+
const parent = m.parent; // se esiste, questa route è child
|
|
181
|
+
let childMounted = null;
|
|
182
|
+
|
|
183
|
+
ctx.renderChild = (target) => {
|
|
184
|
+
if (!ctx._child) return null;
|
|
185
|
+
|
|
186
|
+
const childOutlet = typeof target === "string" ? ctx.outlet.querySelector(target) : target;
|
|
187
|
+
if (!childOutlet) {
|
|
188
|
+
console.warn("[router] child outlet non trovato:", target);
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// monta child (clear true)
|
|
193
|
+
childMounted = JSswift.mount(childOutlet, () => ctx._child.view(ctx._child.ctx), { clear: true });
|
|
194
|
+
return childMounted;
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
if (parent) {
|
|
198
|
+
// nested: render layout (parent.view) e prepara child
|
|
199
|
+
const childRoute = m.route;
|
|
200
|
+
|
|
201
|
+
// ctx child separato (stessi query/hash, params merged)
|
|
202
|
+
const childCtx = {
|
|
203
|
+
...ctx,
|
|
204
|
+
params: { ...ctx.params }, // include già i params del child match (per come matcha)
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
ctx._child = {
|
|
208
|
+
view: childRoute.view,
|
|
209
|
+
ctx: childCtx
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
// mount layout sul root outlet
|
|
213
|
+
unmountCurrent = JSswift.mount(outlet, () => parent.view(ctx), { clear: true });
|
|
214
|
+
} else {
|
|
215
|
+
// non nested
|
|
216
|
+
ctx._child = null;
|
|
217
|
+
unmountCurrent = JSswift.mount(outlet, () => view(ctx), { clear: true });
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
notifyRoute(ctx);
|
|
221
|
+
// devtools state update
|
|
222
|
+
_currentCtx = ctx;
|
|
223
|
+
pushHistoryEntry(_history, ctx);
|
|
224
|
+
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function navigate(to, opts = {}) {
|
|
228
|
+
const url = new URL(to, window.location.origin);
|
|
229
|
+
render(url, opts);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function start() {
|
|
233
|
+
// back/forward
|
|
234
|
+
window.addEventListener("popstate", () => {
|
|
235
|
+
render(getCurrentURL(), { replace: true });
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
window.addEventListener("hashchange", () => {
|
|
239
|
+
if (mode === "hash" || mode === "auto") {
|
|
240
|
+
render(getCurrentURL(), { replace: true });
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
// click interception su <a>
|
|
247
|
+
document.addEventListener("click", (e) => {
|
|
248
|
+
const a = e.target?.closest?.("a");
|
|
249
|
+
if (!a) return;
|
|
250
|
+
|
|
251
|
+
// rispetta:
|
|
252
|
+
// - new tab / middle click / ctrl/cmd
|
|
253
|
+
if (e.defaultPrevented) return;
|
|
254
|
+
if (e.button !== 0) return;
|
|
255
|
+
if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
|
|
256
|
+
|
|
257
|
+
// rispetta download/target
|
|
258
|
+
if (a.hasAttribute("download")) return;
|
|
259
|
+
if (a.target && a.target !== "_self") return;
|
|
260
|
+
|
|
261
|
+
const href = a.getAttribute("href");
|
|
262
|
+
if (!href || href.startsWith("mailto:") || href.startsWith("tel:")) return;
|
|
263
|
+
|
|
264
|
+
// esterno
|
|
265
|
+
const url = new URL(href, window.location.origin);
|
|
266
|
+
if (url.origin !== window.location.origin) return;
|
|
267
|
+
|
|
268
|
+
// hash-only (stessa pagina)
|
|
269
|
+
// se vuoi gestirlo, puoi farlo qui; per ora: lasciamo default se non cambia path
|
|
270
|
+
const current = new URL(window.location.href);
|
|
271
|
+
const samePath = (url.pathname + url.search) === (current.pathname + current.search);
|
|
272
|
+
if (samePath && url.hash) return;
|
|
273
|
+
|
|
274
|
+
// prevent e navigate
|
|
275
|
+
e.preventDefault();
|
|
276
|
+
navigate(url.pathname + url.search + url.hash);
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
// render iniziale
|
|
280
|
+
render(getCurrentURL(), { replace: true });
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function setOutlet(target) {
|
|
284
|
+
outlet = typeof target === "string" ? JSswift.dom.q(target) : target;
|
|
285
|
+
return outlet;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function setBase(b) {
|
|
289
|
+
base = b ? normalizePath(b) : "";
|
|
290
|
+
// base dovrebbe essere "" oppure "/qualcosa"
|
|
291
|
+
if (base === "/") base = "";
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function add(path, view, children = null) {
|
|
295
|
+
const compiled = compilePattern(path);
|
|
296
|
+
const record = { path: compiled.pattern, view, _compiled: compiled, children: [] };
|
|
297
|
+
|
|
298
|
+
if (Array.isArray(children)) {
|
|
299
|
+
// children: array di [subPath, subView] o [subPath, subView, subChildren]
|
|
300
|
+
for (const c of children) {
|
|
301
|
+
const [subPath, subView, subChildren] = c;
|
|
302
|
+
// child path relativo al parent
|
|
303
|
+
const full = normalizePath(compiled.pattern + "/" + (subPath || "").replace(/^\//, ""));
|
|
304
|
+
record.children.push({
|
|
305
|
+
path: full,
|
|
306
|
+
view: subView,
|
|
307
|
+
_compiled: compilePattern(full),
|
|
308
|
+
children: Array.isArray(subChildren) ? subChildren : []
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
routes.push(record);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
function notFound(view) {
|
|
318
|
+
routes._notFound = view;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function beforeEach(fn) {
|
|
322
|
+
beforeHook = fn;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// --- Router reactive state ---
|
|
326
|
+
let _routeListeners = new Set();
|
|
327
|
+
|
|
328
|
+
function notifyRoute(ctx) {
|
|
329
|
+
for (const fn of _routeListeners) {
|
|
330
|
+
try { fn(ctx); } catch (e) { console.error("[router] listener error:", e); }
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
// ===============================
|
|
336
|
+
// Router DevTools
|
|
337
|
+
// ===============================
|
|
338
|
+
function routeStatus() {
|
|
339
|
+
if (!_currentCtx) return "ROUTER ⏳ not ready";
|
|
340
|
+
const q = Object.keys(_currentCtx.query || {}).length ? "?" : "";
|
|
341
|
+
return `ROUTER ${_currentCtx.path}${q} ${_currentCtx.hash || ""
|
|
342
|
+
}`;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function routeInspect(label = "router") {
|
|
346
|
+
if (!_currentCtx) {
|
|
347
|
+
console.warn("[router.inspect] nessuna route attiva");
|
|
348
|
+
return null;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const info = {
|
|
352
|
+
label,
|
|
353
|
+
mode: typeof mode !== "undefined" ? mode : "unknown",
|
|
354
|
+
base,
|
|
355
|
+
path: _currentCtx.path,
|
|
356
|
+
params: _currentCtx.params,
|
|
357
|
+
query: _currentCtx.query,
|
|
358
|
+
hash: _currentCtx.hash,
|
|
359
|
+
outlet,
|
|
360
|
+
timestamp: new Date().toISOString()
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
console.groupCollapsed(`[JSswift.router.inspect] ${label}`);
|
|
364
|
+
console.log(info);
|
|
365
|
+
console.log("history:", _history.slice());
|
|
366
|
+
console.groupEnd();
|
|
367
|
+
|
|
368
|
+
return info;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function routeTrace(on = true) {
|
|
372
|
+
_tracing = !!on;
|
|
373
|
+
console.log("[JSswift.router.trace]", _tracing ? "ON" : "OFF");
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function routeHistory() {
|
|
377
|
+
return _history.slice();
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
return {
|
|
382
|
+
meta,
|
|
383
|
+
setOutlet, setBase, add, setURLOnly, notFound, beforeEach, start, navigate, isActive, subscribe(fn) {
|
|
384
|
+
_routeListeners.add(fn);
|
|
385
|
+
return () => _routeListeners.delete(fn);
|
|
386
|
+
},
|
|
387
|
+
current() {
|
|
388
|
+
return getCurrentURL ? getCurrentURL() : new URL(window.location.href);
|
|
389
|
+
},
|
|
390
|
+
status: routeStatus,
|
|
391
|
+
inspect: routeInspect,
|
|
392
|
+
trace: routeTrace,
|
|
393
|
+
history: routeHistory
|
|
394
|
+
};
|
|
395
|
+
})();
|
|
396
|
+
|
|
397
|
+
if (JSswift.config?.debug) {
|
|
398
|
+
window.$router = JSswift.router;
|
|
399
|
+
}
|
package/src/53-footer.js
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
// alias per compatibilità
|
|
2
|
+
JSswift.signal = JSswift.reactive.signal;
|
|
3
|
+
JSswift.effect = JSswift.reactive.effect;
|
|
4
|
+
JSswift.computed = JSswift.reactive.computed;
|
|
5
|
+
JSswift.untracked = JSswift.reactive.untracked;
|
|
6
|
+
JSswift.batch = JSswift.reactive.batch;
|
|
7
|
+
|
|
8
|
+
function normalizeThemeName(theme) {
|
|
9
|
+
if (theme == null) return null;
|
|
10
|
+
const value = String(theme).trim();
|
|
11
|
+
return value || null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function normalizeThemeList(input) {
|
|
15
|
+
const values = Array.isArray(input)
|
|
16
|
+
? input
|
|
17
|
+
: typeof input === "string"
|
|
18
|
+
? input.split(",")
|
|
19
|
+
: [];
|
|
20
|
+
const out = [];
|
|
21
|
+
const seen = new Set();
|
|
22
|
+
for (const item of values) {
|
|
23
|
+
const value = normalizeThemeName(item);
|
|
24
|
+
if (!value || seen.has(value)) continue;
|
|
25
|
+
seen.add(value);
|
|
26
|
+
out.push(value);
|
|
27
|
+
}
|
|
28
|
+
return out;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function getThemeRoot() {
|
|
32
|
+
if (typeof document !== "undefined" && document.documentElement) {
|
|
33
|
+
return document.documentElement;
|
|
34
|
+
}
|
|
35
|
+
return JSswift.dom?.q ? JSswift.dom.q("html") : null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function getThemeStorage() {
|
|
39
|
+
try {
|
|
40
|
+
return typeof globalThis !== "undefined" ? globalThis.localStorage || null : null;
|
|
41
|
+
} catch (_error) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function getThemeStorageKey() {
|
|
47
|
+
return normalizeThemeName(JSswift.theme?.storageKey) || "jsswift:theme";
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function readSavedTheme() {
|
|
51
|
+
const storage = getThemeStorage();
|
|
52
|
+
if (!storage) return null;
|
|
53
|
+
try {
|
|
54
|
+
return normalizeThemeName(storage.getItem(getThemeStorageKey()));
|
|
55
|
+
} catch (_error) {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function writeSavedTheme(theme) {
|
|
61
|
+
const storage = getThemeStorage();
|
|
62
|
+
if (!storage) return;
|
|
63
|
+
try {
|
|
64
|
+
if (theme == null) {
|
|
65
|
+
storage.removeItem(getThemeStorageKey());
|
|
66
|
+
} else {
|
|
67
|
+
storage.setItem(getThemeStorageKey(), theme);
|
|
68
|
+
}
|
|
69
|
+
} catch (_error) { }
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function resolveThemeList(themes) {
|
|
73
|
+
const html = getThemeRoot();
|
|
74
|
+
const candidates = [
|
|
75
|
+
themes,
|
|
76
|
+
JSswift.theme?.themes,
|
|
77
|
+
JSswift.config?.themes,
|
|
78
|
+
typeof globalThis !== "undefined" ? globalThis.JSswift_setting?.themes : null,
|
|
79
|
+
typeof globalThis !== "undefined" ? globalThis.JSswift_setting?.themeList : null,
|
|
80
|
+
html?.getAttribute?.("data-themes"),
|
|
81
|
+
];
|
|
82
|
+
|
|
83
|
+
for (const candidate of candidates) {
|
|
84
|
+
const list = normalizeThemeList(candidate);
|
|
85
|
+
if (list.length) return list;
|
|
86
|
+
}
|
|
87
|
+
return [];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
JSswift.theme = JSswift.theme || {};
|
|
91
|
+
JSswift.theme.storageKey = getThemeStorageKey();
|
|
92
|
+
if (!normalizeThemeList(JSswift.theme.themes).length) {
|
|
93
|
+
JSswift.theme.themes = resolveThemeList();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
JSswift.setTheme = function (theme, opts = {}) {
|
|
97
|
+
const value = normalizeThemeName(theme);
|
|
98
|
+
const html = getThemeRoot();
|
|
99
|
+
if (html) {
|
|
100
|
+
if (value == null) html.removeAttribute("data-theme");
|
|
101
|
+
else html.setAttribute("data-theme", value);
|
|
102
|
+
}
|
|
103
|
+
if (opts.persist !== false) writeSavedTheme(value);
|
|
104
|
+
return html;
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
JSswift.getTheme = function (opts = {}) {
|
|
108
|
+
const html = getThemeRoot();
|
|
109
|
+
const current = normalizeThemeName(html?.getAttribute?.("data-theme"));
|
|
110
|
+
if (current) return current;
|
|
111
|
+
|
|
112
|
+
if (opts.storage === false) return null;
|
|
113
|
+
|
|
114
|
+
const saved = readSavedTheme();
|
|
115
|
+
if (saved && html && opts.sync !== false) {
|
|
116
|
+
html.setAttribute("data-theme", saved);
|
|
117
|
+
}
|
|
118
|
+
return saved;
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
JSswift.toggleTheme = function (themes, opts = {}) {
|
|
122
|
+
const list = resolveThemeList(themes);
|
|
123
|
+
const activeList = list.length
|
|
124
|
+
? list
|
|
125
|
+
: normalizeThemeList([JSswift.getTheme({ sync: false }), "light", "dark"]);
|
|
126
|
+
const current = JSswift.getTheme({ sync: false });
|
|
127
|
+
const fallback = normalizeThemeName(opts.fallback) || activeList[0] || "light";
|
|
128
|
+
const currentIndex = current ? activeList.indexOf(current) : -1;
|
|
129
|
+
const nextTheme = currentIndex >= 0
|
|
130
|
+
? activeList[(currentIndex + 1) % activeList.length]
|
|
131
|
+
: fallback;
|
|
132
|
+
|
|
133
|
+
JSswift.theme.themes = activeList.slice();
|
|
134
|
+
JSswift.setTheme(nextTheme, opts);
|
|
135
|
+
return nextTheme;
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const bootTheme = readSavedTheme();
|
|
139
|
+
if (bootTheme) {
|
|
140
|
+
JSswift.setTheme(bootTheme, { persist: false });
|
|
141
|
+
}
|
|
142
|
+
})();
|
package/src/README.md
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
## CMS Source Modules
|
|
2
|
+
|
|
3
|
+
This folder contains the internal source modules of the JSswift core.
|
|
4
|
+
|
|
5
|
+
Manifest:
|
|
6
|
+
|
|
7
|
+
- `modules.json`
|
|
8
|
+
|
|
9
|
+
Current build order:
|
|
10
|
+
|
|
11
|
+
- `00-bootstrap.js`
|
|
12
|
+
- `09-overlay-shared.js`
|
|
13
|
+
- `10-overlay.js`
|
|
14
|
+
- `15-dom-props.js`
|
|
15
|
+
- `16-renderer-shared.js`
|
|
16
|
+
- `17-renderer-children.js`
|
|
17
|
+
- `18-renderer-args.js`
|
|
18
|
+
- `20-renderer.js`
|
|
19
|
+
- `30-rod-core.js`
|
|
20
|
+
- `31-rod-model.js`
|
|
21
|
+
- `32-lifecycle-helpers.js`
|
|
22
|
+
- `32-lifecycle.js`
|
|
23
|
+
- `33-debug-perf.js`
|
|
24
|
+
- `34-rod-devtools.js`
|
|
25
|
+
- `39-store-shared.js`
|
|
26
|
+
- `40-store-core.js`
|
|
27
|
+
- `41-store-extensions.js`
|
|
28
|
+
- `42-plugins.js`
|
|
29
|
+
- `42a-auth-shared.js`
|
|
30
|
+
- `43-auth.js`
|
|
31
|
+
- `43a-http-shared.js`
|
|
32
|
+
- `44-http.js`
|
|
33
|
+
- `45-route-hooks.js`
|
|
34
|
+
- `49-router-shared.js`
|
|
35
|
+
- `49a-ui-meta-shared.js`
|
|
36
|
+
- `50-ui-meta.js`
|
|
37
|
+
- `51-can.js`
|
|
38
|
+
- `52-router.js`
|
|
39
|
+
- `53-footer.js`
|
|
40
|
+
|
|
41
|
+
Build command:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
npm run build:cms
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Generated outputs:
|
|
48
|
+
|
|
49
|
+
- `packages/core/dist/cms.js`
|
|
50
|
+
- `packages/core/dist/min-cms.js`
|
|
51
|
+
- compatibility mirror in `pages/_jsswift-fe/js/`
|
|
52
|
+
|
|
53
|
+
Rules:
|
|
54
|
+
|
|
55
|
+
- edit files in `packages/core/src/`
|
|
56
|
+
- rebuild the core package
|
|
57
|
+
- run `npm test`
|
|
58
|
+
- update `docs/reference/core.md` if the technical contract changes
|
package/src/modules.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
[
|
|
2
|
+
"00-bootstrap.js",
|
|
3
|
+
"09-overlay-shared.js",
|
|
4
|
+
"10-overlay.js",
|
|
5
|
+
"15-dom-props.js",
|
|
6
|
+
"16-renderer-shared.js",
|
|
7
|
+
"17-renderer-children.js",
|
|
8
|
+
"18-renderer-args.js",
|
|
9
|
+
"20-renderer.js",
|
|
10
|
+
"30-rod-core.js",
|
|
11
|
+
"31-rod-model.js",
|
|
12
|
+
"32-lifecycle-helpers.js",
|
|
13
|
+
"32-lifecycle.js",
|
|
14
|
+
"33-debug-perf.js",
|
|
15
|
+
"34-rod-devtools.js",
|
|
16
|
+
"39-store-shared.js",
|
|
17
|
+
"40-store-core.js",
|
|
18
|
+
"41-store-extensions.js",
|
|
19
|
+
"42-plugins.js",
|
|
20
|
+
"42a-auth-shared.js",
|
|
21
|
+
"43-auth.js",
|
|
22
|
+
"43a-http-shared.js",
|
|
23
|
+
"44-http.js",
|
|
24
|
+
"45-route-hooks.js",
|
|
25
|
+
"49-router-shared.js",
|
|
26
|
+
"49a-ui-meta-shared.js",
|
|
27
|
+
"50-ui-meta.js",
|
|
28
|
+
"51-can.js",
|
|
29
|
+
"52-router.js",
|
|
30
|
+
"53-footer.js"
|
|
31
|
+
]
|