bun-plugin-stx 0.2.413 → 0.2.414
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/{chunk-anqecs1e.js → chunk-18kmm7vg.js} +10 -10
- package/dist/chunk-jmbnee6y.js +286 -0
- package/dist/{chunk-93ete56h.js → chunk-qcdy1k88.js} +1 -1
- package/dist/client.js +3 -5
- package/dist/index.js +16 -17
- package/dist/preload.js +1 -2
- package/dist/serve.js +100 -104
- package/package.json +3 -3
- package/dist/chunk-2mx7fq49.js +0 -4
- package/dist/chunk-g7br5ehx.js +0 -4292
package/dist/chunk-g7br5ehx.js
DELETED
|
@@ -1,4292 +0,0 @@
|
|
|
1
|
-
// @bun
|
|
2
|
-
import {
|
|
3
|
-
deriveLayoutGroup,
|
|
4
|
-
extractLayoutMetadata
|
|
5
|
-
} from "./chunk-93ete56h.js";
|
|
6
|
-
import"./chunk-2mx7fq49.js";
|
|
7
|
-
|
|
8
|
-
// ../router/src/file-router.ts
|
|
9
|
-
import fs3 from "fs";
|
|
10
|
-
import path4 from "path";
|
|
11
|
-
|
|
12
|
-
// ../router/src/codegen.ts
|
|
13
|
-
import fs from "fs";
|
|
14
|
-
import path from "path";
|
|
15
|
-
|
|
16
|
-
// ../router/src/page-routes.ts
|
|
17
|
-
var NON_PAGE_DIRS = ["components", "layouts", "partials", "emails"];
|
|
18
|
-
function isNonPageRoutePath(relativePath) {
|
|
19
|
-
const segments = relativePath.split(/[\\/]/).filter(Boolean);
|
|
20
|
-
return segments.slice(0, -1).some((segment) => NON_PAGE_DIRS.includes(segment));
|
|
21
|
-
}
|
|
22
|
-
function isNonPageRoutePattern(pattern) {
|
|
23
|
-
return isNonPageRoutePath(pattern.replace(/^\//, ""));
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
// ../router/src/codegen.ts
|
|
27
|
-
function generateRouteTypes(routes, outputDir) {
|
|
28
|
-
const pageRoutes = routes.filter((r) => !isNonPageRoutePattern(r.pattern));
|
|
29
|
-
const lines = ["// Auto-generated by STX - do not edit"];
|
|
30
|
-
if (pageRoutes.length === 0) {
|
|
31
|
-
lines.push("// No page routes found \u2014 route paths stay typed as string.", "export {}", "");
|
|
32
|
-
} else {
|
|
33
|
-
lines.push(`declare module '@stacksjs/stx' {`, " interface KnownRoutes {");
|
|
34
|
-
for (const route of pageRoutes) {
|
|
35
|
-
const paramEntries = [];
|
|
36
|
-
for (const p of route.params) {
|
|
37
|
-
const optional = route.pattern.includes(`:${p}?`) ? "?" : "";
|
|
38
|
-
paramEntries.push(`${p}${optional}: string`);
|
|
39
|
-
}
|
|
40
|
-
const paramsStr = paramEntries.join("; ");
|
|
41
|
-
lines.push(` '${route.pattern}': { ${paramsStr} }`);
|
|
42
|
-
}
|
|
43
|
-
lines.push(" }", "}", "", "export {}", "");
|
|
44
|
-
}
|
|
45
|
-
const contents = lines.join(`
|
|
46
|
-
`);
|
|
47
|
-
const target = path.join(outputDir, "route-types.d.ts");
|
|
48
|
-
try {
|
|
49
|
-
if (fs.existsSync(target) && fs.readFileSync(target, "utf8") === contents)
|
|
50
|
-
return;
|
|
51
|
-
fs.mkdirSync(outputDir, { recursive: true });
|
|
52
|
-
fs.writeFileSync(target, contents);
|
|
53
|
-
} catch {}
|
|
54
|
-
}
|
|
55
|
-
function generateRouteManifest(routes, outputDir, pagesDir) {
|
|
56
|
-
const cwd = process.cwd();
|
|
57
|
-
const relativePages = path.relative(cwd, pagesDir);
|
|
58
|
-
const pageRoutes = routes.filter((r) => !isNonPageRoutePattern(r.pattern));
|
|
59
|
-
const lines = [
|
|
60
|
-
"// Auto-generated by stx \u2014 do not edit",
|
|
61
|
-
`// Source: ${relativePages || "pages"}/`,
|
|
62
|
-
"",
|
|
63
|
-
"export const routes = ["
|
|
64
|
-
];
|
|
65
|
-
for (const route of pageRoutes) {
|
|
66
|
-
const relFile = path.relative(cwd, route.filePath).replace(/\\/g, "/");
|
|
67
|
-
const params = JSON.stringify(route.params);
|
|
68
|
-
const layoutStr = route.layout ? `, layout: '${path.relative(cwd, route.layout).replace(/\\/g, "/")}'` : "";
|
|
69
|
-
lines.push(` { pattern: '${route.pattern}', filePath: '${relFile}', isDynamic: ${route.isDynamic}, params: ${params}${layoutStr} },`);
|
|
70
|
-
}
|
|
71
|
-
lines.push("] as const", "");
|
|
72
|
-
lines.push("export type RouteName = typeof routes[number]['pattern']", "");
|
|
73
|
-
fs.mkdirSync(outputDir, { recursive: true });
|
|
74
|
-
fs.writeFileSync(path.join(outputDir, "routes.ts"), lines.join(`
|
|
75
|
-
`));
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// ../router/src/matcher.ts
|
|
79
|
-
import path2 from "path";
|
|
80
|
-
function filePathToPattern(filePath, pagesDir) {
|
|
81
|
-
const relativePath = path2.relative(pagesDir, filePath);
|
|
82
|
-
let route = relativePath.replace(/\.(stx|md)$/, "");
|
|
83
|
-
route = route.split(path2.sep).join("/");
|
|
84
|
-
if (route === "index") {
|
|
85
|
-
return "/";
|
|
86
|
-
}
|
|
87
|
-
if (route.endsWith("/index")) {
|
|
88
|
-
route = route.slice(0, -6);
|
|
89
|
-
}
|
|
90
|
-
route = route.replace(/\[\[([^\]]+)\]\]/g, ":$1?");
|
|
91
|
-
route = route.replace(/\[([^\]]+)\]/g, ":$1");
|
|
92
|
-
route = route.replace(/:\.\.\.([^/]+)/g, ":$1*");
|
|
93
|
-
return `/${route}`;
|
|
94
|
-
}
|
|
95
|
-
function patternToRegex(pattern) {
|
|
96
|
-
const params = [];
|
|
97
|
-
let regexStr = "";
|
|
98
|
-
let i = 0;
|
|
99
|
-
while (i < pattern.length) {
|
|
100
|
-
const rest = pattern.slice(i);
|
|
101
|
-
const optional = /^\/:([^/?*]+)\?/.exec(rest);
|
|
102
|
-
if (optional) {
|
|
103
|
-
params.push(optional[1]);
|
|
104
|
-
regexStr += "(?:/([^/]+))?";
|
|
105
|
-
i += optional[0].length;
|
|
106
|
-
continue;
|
|
107
|
-
}
|
|
108
|
-
const catchAll = /^:([^/?*]+)\*/.exec(rest);
|
|
109
|
-
if (catchAll) {
|
|
110
|
-
params.push(catchAll[1]);
|
|
111
|
-
regexStr += "(.+)";
|
|
112
|
-
i += catchAll[0].length;
|
|
113
|
-
continue;
|
|
114
|
-
}
|
|
115
|
-
const required = /^:([^/?*]+)/.exec(rest);
|
|
116
|
-
if (required) {
|
|
117
|
-
params.push(required[1]);
|
|
118
|
-
regexStr += "([^/]+)";
|
|
119
|
-
i += required[0].length;
|
|
120
|
-
continue;
|
|
121
|
-
}
|
|
122
|
-
regexStr += pattern[i].replace(/[.+^${}()|[\]\\*?]/g, "\\$&");
|
|
123
|
-
i += 1;
|
|
124
|
-
}
|
|
125
|
-
return { regex: new RegExp(`^${regexStr}$`), params };
|
|
126
|
-
}
|
|
127
|
-
function matchRoute(pathname, routes) {
|
|
128
|
-
const normalizedPath = pathname === "" ? "/" : pathname;
|
|
129
|
-
for (const route of routes) {
|
|
130
|
-
const match = normalizedPath.match(route.regex);
|
|
131
|
-
if (match) {
|
|
132
|
-
const params = {};
|
|
133
|
-
route.params.forEach((name, index) => {
|
|
134
|
-
params[name] = match[index + 1];
|
|
135
|
-
});
|
|
136
|
-
return { route, params };
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
return null;
|
|
140
|
-
}
|
|
141
|
-
function bracketPathToRegex(routePath) {
|
|
142
|
-
const pattern = routePath.replace(/\[\[([^\]]+)\]\]/g, ":$1?").replace(/\[([^\]]+)\]/g, ":$1").replace(/:\.\.\.([^/]+)/g, ":$1*");
|
|
143
|
-
return patternToRegex(pattern);
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
// ../router/src/nested-layouts.ts
|
|
147
|
-
import fs2 from "fs";
|
|
148
|
-
import path3 from "path";
|
|
149
|
-
function resolveLayoutChain(routeFilePath, pagesDir) {
|
|
150
|
-
const layouts = [];
|
|
151
|
-
let dir = path3.dirname(routeFilePath);
|
|
152
|
-
while (dir.startsWith(pagesDir) || dir === pagesDir) {
|
|
153
|
-
const layoutFile = path3.join(dir, "_layout.stx");
|
|
154
|
-
if (fs2.existsSync(layoutFile)) {
|
|
155
|
-
layouts.unshift(layoutFile);
|
|
156
|
-
}
|
|
157
|
-
if (dir === pagesDir)
|
|
158
|
-
break;
|
|
159
|
-
dir = path3.dirname(dir);
|
|
160
|
-
}
|
|
161
|
-
return layouts;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
// ../router/src/file-router.ts
|
|
165
|
-
function scanDirectory(dir, extensions) {
|
|
166
|
-
const files = [];
|
|
167
|
-
if (!fs3.existsSync(dir)) {
|
|
168
|
-
return files;
|
|
169
|
-
}
|
|
170
|
-
const entries = fs3.readdirSync(dir, { withFileTypes: true });
|
|
171
|
-
for (const entry of entries) {
|
|
172
|
-
const fullPath = path4.join(dir, entry.name);
|
|
173
|
-
if (entry.isDirectory()) {
|
|
174
|
-
if (entry.name.startsWith(".")) {
|
|
175
|
-
continue;
|
|
176
|
-
}
|
|
177
|
-
if (NON_PAGE_DIRS.includes(entry.name)) {
|
|
178
|
-
continue;
|
|
179
|
-
}
|
|
180
|
-
files.push(...scanDirectory(fullPath, extensions));
|
|
181
|
-
} else if (entry.isFile()) {
|
|
182
|
-
const ext = path4.extname(entry.name);
|
|
183
|
-
if (extensions.includes(ext)) {
|
|
184
|
-
if (entry.name.startsWith("_") && entry.name !== "_layout.stx") {
|
|
185
|
-
continue;
|
|
186
|
-
}
|
|
187
|
-
if (entry.name === "_layout.stx") {
|
|
188
|
-
continue;
|
|
189
|
-
}
|
|
190
|
-
files.push(fullPath);
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
return files;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
class Router {
|
|
198
|
-
routes;
|
|
199
|
-
pagesDir;
|
|
200
|
-
pagesDirs;
|
|
201
|
-
config;
|
|
202
|
-
constructor(baseDir, config = {}) {
|
|
203
|
-
this.config = config;
|
|
204
|
-
const rawDirs = config.pagesDirs && config.pagesDirs.length > 0 ? config.pagesDirs : [config.pagesDir || "pages"];
|
|
205
|
-
this.pagesDirs = rawDirs.map((d) => path4.resolve(baseDir, d));
|
|
206
|
-
this.pagesDir = this.pagesDirs[0];
|
|
207
|
-
const extensions = config.extensions || [".stx"];
|
|
208
|
-
const canonical = (entry) => {
|
|
209
|
-
const resolved = path4.resolve(baseDir, entry);
|
|
210
|
-
try {
|
|
211
|
-
return fs3.realpathSync(resolved);
|
|
212
|
-
} catch {
|
|
213
|
-
return resolved;
|
|
214
|
-
}
|
|
215
|
-
};
|
|
216
|
-
const excluded = (config.exclude ?? []).filter((entry) => typeof entry === "string" && entry.trim() !== "").map(canonical);
|
|
217
|
-
const isExcluded = (file) => {
|
|
218
|
-
if (excluded.length === 0)
|
|
219
|
-
return false;
|
|
220
|
-
const real = canonical(file);
|
|
221
|
-
return excluded.some((root) => real === root || real.startsWith(`${root}${path4.sep}`));
|
|
222
|
-
};
|
|
223
|
-
const seenPatterns = new Set;
|
|
224
|
-
this.routes = [];
|
|
225
|
-
for (const dir of this.pagesDirs) {
|
|
226
|
-
const files = scanDirectory(dir, extensions).filter((file) => !isExcluded(file));
|
|
227
|
-
const localPatterns = new Map;
|
|
228
|
-
for (const filePath of files) {
|
|
229
|
-
const pattern = filePathToPattern(filePath, dir);
|
|
230
|
-
const identity = config.pagesDirs ? pattern.replace(/:[\w]+/g, ":param") : pattern;
|
|
231
|
-
if (config.pagesDirs && localPatterns.has(identity))
|
|
232
|
-
throw new Error(`Conflicting layer pages for ${pattern}: ${localPatterns.get(identity)} and ${filePath}`);
|
|
233
|
-
localPatterns.set(identity, filePath);
|
|
234
|
-
if (seenPatterns.has(identity))
|
|
235
|
-
continue;
|
|
236
|
-
seenPatterns.add(identity);
|
|
237
|
-
const { regex, params } = patternToRegex(pattern);
|
|
238
|
-
const layouts = config.layouts !== false ? resolveLayoutChain(filePath, dir) : [];
|
|
239
|
-
this.routes.push({
|
|
240
|
-
pattern,
|
|
241
|
-
regex,
|
|
242
|
-
params,
|
|
243
|
-
filePath,
|
|
244
|
-
isDynamic: params.length > 0,
|
|
245
|
-
layout: layouts.length > 0 ? layouts[layouts.length - 1] : undefined
|
|
246
|
-
});
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
this.routes.sort((a, b) => {
|
|
250
|
-
if (!a.isDynamic && b.isDynamic)
|
|
251
|
-
return -1;
|
|
252
|
-
if (a.isDynamic && !b.isDynamic)
|
|
253
|
-
return 1;
|
|
254
|
-
const aSegments = a.pattern.split("/").length;
|
|
255
|
-
const bSegments = b.pattern.split("/").length;
|
|
256
|
-
if (aSegments !== bSegments)
|
|
257
|
-
return bSegments - aSegments;
|
|
258
|
-
return a.pattern.localeCompare(b.pattern);
|
|
259
|
-
});
|
|
260
|
-
if (config.emit !== false) {
|
|
261
|
-
const stxDir = path4.resolve(baseDir, config.stateDir || process.env.STX_DIR?.trim() || ".stx");
|
|
262
|
-
generateRouteTypes(this.routes, stxDir);
|
|
263
|
-
generateRouteManifest(this.routes, stxDir, this.pagesDir);
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
match(pathname) {
|
|
267
|
-
const normalizedPath = pathname === "" ? "/" : pathname;
|
|
268
|
-
for (const route of this.routes) {
|
|
269
|
-
const m = normalizedPath.match(route.regex);
|
|
270
|
-
if (m) {
|
|
271
|
-
const params = {};
|
|
272
|
-
route.params.forEach((name, index) => {
|
|
273
|
-
params[name] = m[index + 1];
|
|
274
|
-
});
|
|
275
|
-
return { route, params };
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
return null;
|
|
279
|
-
}
|
|
280
|
-
resolve(name, params) {
|
|
281
|
-
const route = this.routes.find((r) => r.pattern === name || r.filePath.includes(name));
|
|
282
|
-
if (!route)
|
|
283
|
-
return "#not-found";
|
|
284
|
-
let resolved = route.pattern;
|
|
285
|
-
if (params) {
|
|
286
|
-
for (const [key, value] of Object.entries(params)) {
|
|
287
|
-
resolved = resolved.replace(`:${key}`, encodeURIComponent(value));
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
return resolved;
|
|
291
|
-
}
|
|
292
|
-
addRoute(route) {
|
|
293
|
-
this.routes.push(route);
|
|
294
|
-
this.routes.sort((a, b) => {
|
|
295
|
-
if (!a.isDynamic && b.isDynamic)
|
|
296
|
-
return -1;
|
|
297
|
-
if (a.isDynamic && !b.isDynamic)
|
|
298
|
-
return 1;
|
|
299
|
-
return a.pattern.localeCompare(b.pattern);
|
|
300
|
-
});
|
|
301
|
-
}
|
|
302
|
-
getLayout(route) {
|
|
303
|
-
return route.layout ?? null;
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
function createRouter(baseDir, config) {
|
|
307
|
-
return new Router(baseDir, config);
|
|
308
|
-
}
|
|
309
|
-
function findErrorPage(pagesDir, statusCode) {
|
|
310
|
-
const candidates = [`${statusCode}.stx`, "error.stx"];
|
|
311
|
-
for (const candidate of candidates) {
|
|
312
|
-
const filePath = path4.join(pagesDir, candidate);
|
|
313
|
-
if (fs3.existsSync(filePath))
|
|
314
|
-
return filePath;
|
|
315
|
-
}
|
|
316
|
-
return null;
|
|
317
|
-
}
|
|
318
|
-
function formatRoutes(routes, baseDir) {
|
|
319
|
-
return routes.map((route) => {
|
|
320
|
-
const relativePath = path4.relative(baseDir, route.filePath);
|
|
321
|
-
const dynamicLabel = route.isDynamic ? " (dynamic)" : "";
|
|
322
|
-
return `${route.pattern} \u2192 ${relativePath}${dynamicLabel}`;
|
|
323
|
-
});
|
|
324
|
-
}
|
|
325
|
-
// ../router/src/named-routes.ts
|
|
326
|
-
var routes = {};
|
|
327
|
-
var appUrl = "";
|
|
328
|
-
function setAppUrl(url) {
|
|
329
|
-
appUrl = url.replace(/\/$/, "");
|
|
330
|
-
}
|
|
331
|
-
function defineRoute(name, routePath, params = {}) {
|
|
332
|
-
routes[name] = { path: routePath, name, params };
|
|
333
|
-
}
|
|
334
|
-
function defineRoutes(routeDefinitions) {
|
|
335
|
-
for (const [name, definition] of Object.entries(routeDefinitions)) {
|
|
336
|
-
if (typeof definition === "string") {
|
|
337
|
-
routes[name] = { path: definition, name };
|
|
338
|
-
} else {
|
|
339
|
-
routes[name] = { ...definition, name };
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
function route(name, params = {}, absolute = false) {
|
|
344
|
-
const r = routes[name];
|
|
345
|
-
if (!r) {
|
|
346
|
-
console.warn(`Route [${name}] not defined.`);
|
|
347
|
-
return "#undefined-route";
|
|
348
|
-
}
|
|
349
|
-
const mergedParams = { ...r.params, ...params };
|
|
350
|
-
let routePath = r.path;
|
|
351
|
-
routePath = routePath.replace(/:(\w+)/g, (_, paramName) => {
|
|
352
|
-
const value = mergedParams[paramName];
|
|
353
|
-
if (value === undefined) {
|
|
354
|
-
console.warn(`Missing parameter [${paramName}] for route [${name}].`);
|
|
355
|
-
return `:${paramName}`;
|
|
356
|
-
}
|
|
357
|
-
delete mergedParams[paramName];
|
|
358
|
-
return encodeURIComponent(String(value));
|
|
359
|
-
});
|
|
360
|
-
const remainingParams = Object.entries(mergedParams);
|
|
361
|
-
if (remainingParams.length > 0) {
|
|
362
|
-
const queryString = remainingParams.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`).join("&");
|
|
363
|
-
routePath += (routePath.includes("?") ? "&" : "?") + queryString;
|
|
364
|
-
}
|
|
365
|
-
return absolute && appUrl ? `${appUrl}${routePath}` : routePath;
|
|
366
|
-
}
|
|
367
|
-
function resetRoutes() {
|
|
368
|
-
for (const key of Object.keys(routes)) {
|
|
369
|
-
delete routes[key];
|
|
370
|
-
}
|
|
371
|
-
appUrl = "";
|
|
372
|
-
}
|
|
373
|
-
// ../router/src/middleware.ts
|
|
374
|
-
import fs4 from "fs";
|
|
375
|
-
import path5 from "path";
|
|
376
|
-
function navigateTo(navPath, options = {}) {
|
|
377
|
-
return {
|
|
378
|
-
type: "redirect",
|
|
379
|
-
path: navPath,
|
|
380
|
-
options: {
|
|
381
|
-
replace: options.replace ?? false,
|
|
382
|
-
external: options.external ?? false,
|
|
383
|
-
redirectCode: options.redirectCode ?? 302
|
|
384
|
-
}
|
|
385
|
-
};
|
|
386
|
-
}
|
|
387
|
-
function abortNavigation(error) {
|
|
388
|
-
const normalizedError = typeof error === "string" ? { statusCode: 500, message: error } : error;
|
|
389
|
-
return {
|
|
390
|
-
type: "abort",
|
|
391
|
-
error: normalizedError
|
|
392
|
-
};
|
|
393
|
-
}
|
|
394
|
-
function defineMiddleware(handler, options = {}) {
|
|
395
|
-
return {
|
|
396
|
-
handler,
|
|
397
|
-
mode: options.mode ?? "universal"
|
|
398
|
-
};
|
|
399
|
-
}
|
|
400
|
-
function parseCookies(cookieHeader) {
|
|
401
|
-
const cookies = {};
|
|
402
|
-
if (!cookieHeader)
|
|
403
|
-
return cookies;
|
|
404
|
-
const pairs = cookieHeader.split(";");
|
|
405
|
-
for (const pair of pairs) {
|
|
406
|
-
const [name, ...rest] = pair.split("=");
|
|
407
|
-
if (name) {
|
|
408
|
-
const key = name.trim();
|
|
409
|
-
const value = rest.join("=").trim();
|
|
410
|
-
if (key) {
|
|
411
|
-
cookies[key] = decodeURIComponent(value);
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
return cookies;
|
|
416
|
-
}
|
|
417
|
-
function serializeCookie(name, value, options = {}) {
|
|
418
|
-
let cookie = `${encodeURIComponent(name)}=${encodeURIComponent(value)}`;
|
|
419
|
-
if (options.expires) {
|
|
420
|
-
const expires = typeof options.expires === "number" ? new Date(Date.now() + options.expires * 1000) : options.expires;
|
|
421
|
-
cookie += `; Expires=${expires.toUTCString()}`;
|
|
422
|
-
}
|
|
423
|
-
if (options.path)
|
|
424
|
-
cookie += `; Path=${options.path}`;
|
|
425
|
-
if (options.domain)
|
|
426
|
-
cookie += `; Domain=${options.domain}`;
|
|
427
|
-
if (options.secure)
|
|
428
|
-
cookie += "; Secure";
|
|
429
|
-
if (options.httpOnly)
|
|
430
|
-
cookie += "; HttpOnly";
|
|
431
|
-
if (options.sameSite)
|
|
432
|
-
cookie += `; SameSite=${options.sameSite}`;
|
|
433
|
-
return cookie;
|
|
434
|
-
}
|
|
435
|
-
function createServerCookieManager(request, responseHeaders) {
|
|
436
|
-
const cookieHeader = request.headers.get("cookie") || "";
|
|
437
|
-
const cookies = parseCookies(cookieHeader);
|
|
438
|
-
return {
|
|
439
|
-
get(name) {
|
|
440
|
-
return cookies[name];
|
|
441
|
-
},
|
|
442
|
-
set(name, value, options = {}) {
|
|
443
|
-
cookies[name] = value;
|
|
444
|
-
responseHeaders.append("Set-Cookie", serializeCookie(name, value, options));
|
|
445
|
-
},
|
|
446
|
-
delete(name) {
|
|
447
|
-
delete cookies[name];
|
|
448
|
-
responseHeaders.append("Set-Cookie", serializeCookie(name, "", { expires: new Date(0), path: "/" }));
|
|
449
|
-
},
|
|
450
|
-
getAll() {
|
|
451
|
-
return { ...cookies };
|
|
452
|
-
}
|
|
453
|
-
};
|
|
454
|
-
}
|
|
455
|
-
function createClientCookieManager() {
|
|
456
|
-
return {
|
|
457
|
-
get(name) {
|
|
458
|
-
if (typeof document === "undefined")
|
|
459
|
-
return;
|
|
460
|
-
const cookies = parseCookies(document.cookie);
|
|
461
|
-
return cookies[name];
|
|
462
|
-
},
|
|
463
|
-
set(name, value, options = {}) {
|
|
464
|
-
if (typeof document === "undefined")
|
|
465
|
-
return;
|
|
466
|
-
document.cookie = serializeCookie(name, value, options);
|
|
467
|
-
},
|
|
468
|
-
delete(name) {
|
|
469
|
-
if (typeof document === "undefined")
|
|
470
|
-
return;
|
|
471
|
-
document.cookie = serializeCookie(name, "", { expires: new Date(0), path: "/" });
|
|
472
|
-
},
|
|
473
|
-
getAll() {
|
|
474
|
-
if (typeof document === "undefined")
|
|
475
|
-
return {};
|
|
476
|
-
return parseCookies(document.cookie);
|
|
477
|
-
}
|
|
478
|
-
};
|
|
479
|
-
}
|
|
480
|
-
function createStorageManager() {
|
|
481
|
-
const available = typeof window !== "undefined" && typeof window.localStorage !== "undefined";
|
|
482
|
-
return {
|
|
483
|
-
get(key) {
|
|
484
|
-
if (!available)
|
|
485
|
-
return null;
|
|
486
|
-
try {
|
|
487
|
-
return window.localStorage.getItem(key);
|
|
488
|
-
} catch {
|
|
489
|
-
return null;
|
|
490
|
-
}
|
|
491
|
-
},
|
|
492
|
-
set(key, value) {
|
|
493
|
-
if (!available)
|
|
494
|
-
return;
|
|
495
|
-
try {
|
|
496
|
-
window.localStorage.setItem(key, value);
|
|
497
|
-
} catch {}
|
|
498
|
-
},
|
|
499
|
-
remove(key) {
|
|
500
|
-
if (!available)
|
|
501
|
-
return;
|
|
502
|
-
try {
|
|
503
|
-
window.localStorage.removeItem(key);
|
|
504
|
-
} catch {}
|
|
505
|
-
},
|
|
506
|
-
isAvailable() {
|
|
507
|
-
return available;
|
|
508
|
-
}
|
|
509
|
-
};
|
|
510
|
-
}
|
|
511
|
-
var middlewareRegistry = new Map;
|
|
512
|
-
function registerMiddleware(name, middleware) {
|
|
513
|
-
middlewareRegistry.set(name, middleware);
|
|
514
|
-
}
|
|
515
|
-
function getMiddleware(name) {
|
|
516
|
-
return middlewareRegistry.get(name);
|
|
517
|
-
}
|
|
518
|
-
function hasMiddleware(name) {
|
|
519
|
-
return middlewareRegistry.has(name);
|
|
520
|
-
}
|
|
521
|
-
function clearMiddleware() {
|
|
522
|
-
middlewareRegistry.clear();
|
|
523
|
-
}
|
|
524
|
-
function getMiddlewareNames() {
|
|
525
|
-
return Array.from(middlewareRegistry.keys());
|
|
526
|
-
}
|
|
527
|
-
async function loadMiddlewareFromDirectory(baseDir, middlewareDir = "middleware") {
|
|
528
|
-
const fullPath = path5.resolve(baseDir, middlewareDir);
|
|
529
|
-
if (!fs4.existsSync(fullPath)) {
|
|
530
|
-
return;
|
|
531
|
-
}
|
|
532
|
-
const files = fs4.readdirSync(fullPath);
|
|
533
|
-
for (const file of files) {
|
|
534
|
-
if (!file.endsWith(".ts") && !file.endsWith(".js"))
|
|
535
|
-
continue;
|
|
536
|
-
if (file.includes(".test.") || file.includes(".spec."))
|
|
537
|
-
continue;
|
|
538
|
-
const name = file.replace(/\.(ts|js)$/, "");
|
|
539
|
-
const filePath = path5.join(fullPath, file);
|
|
540
|
-
try {
|
|
541
|
-
const mod = await import(filePath);
|
|
542
|
-
const mw = mod.default;
|
|
543
|
-
if (mw && typeof mw.handler === "function") {
|
|
544
|
-
registerMiddleware(name, mw);
|
|
545
|
-
} else if (typeof mw === "function") {
|
|
546
|
-
registerMiddleware(name, { handler: mw, mode: "universal" });
|
|
547
|
-
}
|
|
548
|
-
} catch (error) {
|
|
549
|
-
console.warn(`Failed to load middleware '${name}' from ${filePath}:`, error);
|
|
550
|
-
}
|
|
551
|
-
}
|
|
552
|
-
}
|
|
553
|
-
function parseQuery(search) {
|
|
554
|
-
const query = {};
|
|
555
|
-
if (!search)
|
|
556
|
-
return query;
|
|
557
|
-
const params = new URLSearchParams(search);
|
|
558
|
-
for (const [key, value] of params) {
|
|
559
|
-
query[key] = value;
|
|
560
|
-
}
|
|
561
|
-
return query;
|
|
562
|
-
}
|
|
563
|
-
function createRouteLocation(pathname, params, meta, search = "") {
|
|
564
|
-
return {
|
|
565
|
-
path: pathname,
|
|
566
|
-
params,
|
|
567
|
-
query: parseQuery(search),
|
|
568
|
-
fullPath: search ? `${pathname}?${search}` : pathname,
|
|
569
|
-
meta
|
|
570
|
-
};
|
|
571
|
-
}
|
|
572
|
-
function createMiddlewareContext(to, from, request, options) {
|
|
573
|
-
const isServer = options?.isServer ?? typeof window === "undefined";
|
|
574
|
-
const isClient = !isServer;
|
|
575
|
-
const responseHeaders = new Headers;
|
|
576
|
-
return {
|
|
577
|
-
to,
|
|
578
|
-
from,
|
|
579
|
-
isClient,
|
|
580
|
-
isServer,
|
|
581
|
-
cookies: isServer && request ? createServerCookieManager(request, responseHeaders) : createClientCookieManager(),
|
|
582
|
-
storage: createStorageManager(),
|
|
583
|
-
state: {},
|
|
584
|
-
request,
|
|
585
|
-
responseHeaders
|
|
586
|
-
};
|
|
587
|
-
}
|
|
588
|
-
async function runMiddleware(middlewareNames, context) {
|
|
589
|
-
const names = Array.isArray(middlewareNames) ? middlewareNames : [middlewareNames];
|
|
590
|
-
const isServer = context.isServer;
|
|
591
|
-
for (const name of names) {
|
|
592
|
-
const middleware = getMiddleware(name);
|
|
593
|
-
if (!middleware) {
|
|
594
|
-
console.error(`Route middleware '${name}' is not registered \u2014 failing the guard closed.`);
|
|
595
|
-
return {
|
|
596
|
-
passed: false,
|
|
597
|
-
abort: {
|
|
598
|
-
type: "abort",
|
|
599
|
-
error: {
|
|
600
|
-
statusCode: 500,
|
|
601
|
-
message: `Route middleware '${name}' is not registered`
|
|
602
|
-
}
|
|
603
|
-
},
|
|
604
|
-
state: context.state,
|
|
605
|
-
responseHeaders: context.responseHeaders
|
|
606
|
-
};
|
|
607
|
-
}
|
|
608
|
-
if (middleware.mode === "server" && !isServer)
|
|
609
|
-
continue;
|
|
610
|
-
try {
|
|
611
|
-
const result = await middleware.handler(context);
|
|
612
|
-
if (result) {
|
|
613
|
-
if (result.type === "redirect") {
|
|
614
|
-
return {
|
|
615
|
-
passed: false,
|
|
616
|
-
redirect: result,
|
|
617
|
-
state: context.state,
|
|
618
|
-
responseHeaders: context.responseHeaders
|
|
619
|
-
};
|
|
620
|
-
}
|
|
621
|
-
if (result.type === "abort") {
|
|
622
|
-
return {
|
|
623
|
-
passed: false,
|
|
624
|
-
abort: result,
|
|
625
|
-
state: context.state,
|
|
626
|
-
responseHeaders: context.responseHeaders
|
|
627
|
-
};
|
|
628
|
-
}
|
|
629
|
-
}
|
|
630
|
-
} catch (error) {
|
|
631
|
-
console.error(`Error in middleware '${name}':`, error);
|
|
632
|
-
return {
|
|
633
|
-
passed: false,
|
|
634
|
-
abort: {
|
|
635
|
-
type: "abort",
|
|
636
|
-
error: {
|
|
637
|
-
statusCode: 500,
|
|
638
|
-
message: error instanceof Error ? error.message : "Middleware error"
|
|
639
|
-
}
|
|
640
|
-
},
|
|
641
|
-
state: context.state,
|
|
642
|
-
responseHeaders: context.responseHeaders
|
|
643
|
-
};
|
|
644
|
-
}
|
|
645
|
-
}
|
|
646
|
-
return {
|
|
647
|
-
passed: true,
|
|
648
|
-
state: context.state,
|
|
649
|
-
responseHeaders: context.responseHeaders
|
|
650
|
-
};
|
|
651
|
-
}
|
|
652
|
-
// ../router/src/client.ts
|
|
653
|
-
var cachedRouterScript;
|
|
654
|
-
var cachedRouterScriptDev;
|
|
655
|
-
function stripRouterLogs(source) {
|
|
656
|
-
const out = [];
|
|
657
|
-
const needle = "log(";
|
|
658
|
-
let i = 0;
|
|
659
|
-
while (i < source.length) {
|
|
660
|
-
const hit = source.indexOf(needle, i);
|
|
661
|
-
if (hit === -1) {
|
|
662
|
-
out.push(source.slice(i));
|
|
663
|
-
break;
|
|
664
|
-
}
|
|
665
|
-
const before = hit > 0 ? source[hit - 1] : " ";
|
|
666
|
-
const isDeclaration = /\bfunction\s+$/.test(source.slice(Math.max(0, hit - 16), hit));
|
|
667
|
-
if (/[\w$.]/.test(before) || isDeclaration) {
|
|
668
|
-
out.push(source.slice(i, hit + needle.length));
|
|
669
|
-
i = hit + needle.length;
|
|
670
|
-
continue;
|
|
671
|
-
}
|
|
672
|
-
out.push(source.slice(i, hit));
|
|
673
|
-
let depth = 1;
|
|
674
|
-
let j = hit + needle.length;
|
|
675
|
-
while (j < source.length && depth > 0) {
|
|
676
|
-
const character = source[j];
|
|
677
|
-
if (character === '"' || character === "'" || character === "`") {
|
|
678
|
-
const quote = character;
|
|
679
|
-
j++;
|
|
680
|
-
while (j < source.length && source[j] !== quote) {
|
|
681
|
-
if (source[j] === "\\")
|
|
682
|
-
j++;
|
|
683
|
-
j++;
|
|
684
|
-
}
|
|
685
|
-
j++;
|
|
686
|
-
continue;
|
|
687
|
-
}
|
|
688
|
-
if (character === "(")
|
|
689
|
-
depth++;
|
|
690
|
-
else if (character === ")")
|
|
691
|
-
depth--;
|
|
692
|
-
j++;
|
|
693
|
-
}
|
|
694
|
-
out.push("0");
|
|
695
|
-
i = j;
|
|
696
|
-
}
|
|
697
|
-
return out.join("");
|
|
698
|
-
}
|
|
699
|
-
function minifyRouterScript(source, mangle = false) {
|
|
700
|
-
try {
|
|
701
|
-
const transpiler = new Bun.Transpiler(mangle ? { loader: "js", minify: { whitespace: true, identifiers: true } } : { loader: "js", minifyWhitespace: true });
|
|
702
|
-
return transpiler.transformSync(source).replace(/\}(var |let |const |function )/g, "};$1").trim();
|
|
703
|
-
} catch {
|
|
704
|
-
return source;
|
|
705
|
-
}
|
|
706
|
-
}
|
|
707
|
-
function routerSource() {
|
|
708
|
-
return `
|
|
709
|
-
;(function(){
|
|
710
|
-
'use strict';
|
|
711
|
-
var ROUTER_REV=7;
|
|
712
|
-
if(window.__stxRouter&&window.__stxRouter.__rev===ROUTER_REV)return;
|
|
713
|
-
|
|
714
|
-
// \u2500\u2500 Configuration \u2500\u2500
|
|
715
|
-
var defaults={container:'main',loadingClass:'stx-navigating',viewTransitions:true,cache:true,scrollToTop:true,prefetch:true,progress:true,progressColor:'#78dce8',progressHeight:'2px',interceptAllLinks:false,prefetchCacheMax:50,routeFocus:true,announceRoute:true,interceptForms:false,cssLoadTimeout:1500,scrollRestoration:true,screens:'auto',screenLimit:8,navDuration:350,swipeBack:true,swipeEdge:20,prefetchVisible:true,prefetchVisibleMax:12,revalidate:true,revalidateAfter:3000};
|
|
716
|
-
var o=Object.assign({},defaults,window.__stxRouterConfig||{},window.STX_ROUTER_OPTIONS||{});
|
|
717
|
-
var containerSel=o.container;
|
|
718
|
-
var debug=!!o.debug;
|
|
719
|
-
function log(){if(debug&&typeof console!=='undefined'&&console.log)console.log.apply(console,arguments)}
|
|
720
|
-
|
|
721
|
-
// Shorthands for the document-rooted queries this script makes ~33 times.
|
|
722
|
-
// Every page carries these bytes, and the budget in
|
|
723
|
-
// scripts/performance-budgets.ts is what noticed.
|
|
724
|
-
var LAYOUT_META='meta[name="stx-layout"]';
|
|
725
|
-
var LAYOUT_GROUP_META='meta[name="stx-layout-group"]';
|
|
726
|
-
var BUILD_META='meta[name="stx-build"]';
|
|
727
|
-
function qs(sel){return document.querySelector(sel)}
|
|
728
|
-
function qsa(sel){return document.querySelectorAll(sel)}
|
|
729
|
-
function ce(tag){return document.createElement(tag)}
|
|
730
|
-
function dhead(){return document.head}
|
|
731
|
-
|
|
732
|
-
// \u2500\u2500 Scroll position \u2500\u2500
|
|
733
|
-
// Forward navigation goes to the top, or to the hash. Back and forward return
|
|
734
|
-
// to where the entry was left -- the browser cannot do that for us, because
|
|
735
|
-
// by the time it would restore, the content of the entry has not been fetched
|
|
736
|
-
// yet, so it clamps to whatever height the outgoing page happened to have.
|
|
737
|
-
// history.scrollRestoration therefore goes to manual and the positions are
|
|
738
|
-
// kept here, keyed by a token written into each history entry (two entries
|
|
739
|
-
// for the same URL keep their own positions, which is the point).
|
|
740
|
-
//
|
|
741
|
-
// Manual restoration also takes the browser's reload behaviour away, so the
|
|
742
|
-
// positions are mirrored into sessionStorage: a reload, or a return to the
|
|
743
|
-
// tab, finds the entry's token already in history.state and restores from
|
|
744
|
-
// there. sessionStorage is per tab and dies with it, same as the history.
|
|
745
|
-
var SCROLL_TOKEN='__stxScroll';
|
|
746
|
-
var SCROLL_STORE='stx:scroll:';
|
|
747
|
-
// On every entry the router pushes, never on the one the app opened with
|
|
748
|
-
// (which carries a scroll token too): going back from a pushed entry stays
|
|
749
|
-
// in the app, from the first it leaves it (or, in a phone app, does nothing).
|
|
750
|
-
var PUSHED_MARK='__stxPushed';
|
|
751
|
-
// How far above the entry the app opened with this one sits, and which tab
|
|
752
|
-
// it belongs to. A tab switch rewinds to depth 0 before it writes the next
|
|
753
|
-
// tab's entries, which is what keeps Back from ever crossing tabs.
|
|
754
|
-
var DEPTH='__stxDepth';
|
|
755
|
-
var TAB='__stxTab';
|
|
756
|
-
var histDepth=(history.state&&history.state[DEPTH])|0;
|
|
757
|
-
var scrollSeq=0;
|
|
758
|
-
var pendingScroll=null;
|
|
759
|
-
var restoreScroll=!!o.scrollRestoration;
|
|
760
|
-
function newScrollToken(){return 's'+(++scrollSeq)+'-'+Date.now()}
|
|
761
|
-
var scrollToken=(history.state&&history.state[SCROLL_TOKEN])||newScrollToken();
|
|
762
|
-
// sessionStorage is the only store: it is synchronous, it is per tab, it dies
|
|
763
|
-
// with the tab exactly as the history does, and it is the one that survives a
|
|
764
|
-
// reload. A private window that refuses it degrades to scrolling to the top.
|
|
765
|
-
function readScroll(token){
|
|
766
|
-
try{
|
|
767
|
-
var parts=(window.sessionStorage.getItem(SCROLL_STORE+token)||'').split(',');
|
|
768
|
-
return parts.length===2?[parseFloat(parts[0])||0,parseFloat(parts[1])||0]:null;
|
|
769
|
-
}catch(e){return null}
|
|
770
|
-
}
|
|
771
|
-
// Inner scrollers, marked data-stx-scroll: a screen whose list scrolls
|
|
772
|
-
// inside its own box rather than the window lost its place on Back exactly
|
|
773
|
-
// the way the window used to. Keyed by the attribute's value, or by order
|
|
774
|
-
// for an unnamed one.
|
|
775
|
-
function scrollerKey(el,i){return el.getAttribute('data-stx-scroll')||('#'+i)}
|
|
776
|
-
function innerScroll(root){
|
|
777
|
-
var m=null;
|
|
778
|
-
if(root&&root.querySelectorAll)root.querySelectorAll('[data-stx-scroll]').forEach(function(el,i){
|
|
779
|
-
if(el.scrollTop||el.scrollLeft){m=m||{};m[scrollerKey(el,i)]=[el.scrollLeft||0,el.scrollTop||0]}
|
|
780
|
-
});
|
|
781
|
-
return m;
|
|
782
|
-
}
|
|
783
|
-
function applyInner(root,m){
|
|
784
|
-
if(!m||!root||!root.querySelectorAll)return;
|
|
785
|
-
root.querySelectorAll('[data-stx-scroll]').forEach(function(el,i){
|
|
786
|
-
var at=m[scrollerKey(el,i)];
|
|
787
|
-
if(at){el.scrollLeft=at[0];el.scrollTop=at[1]}
|
|
788
|
-
});
|
|
789
|
-
}
|
|
790
|
-
// Called while the outgoing entry is still the current one, so the position
|
|
791
|
-
// read here belongs to the token being written.
|
|
792
|
-
function rememberScroll(){
|
|
793
|
-
if(active&&active.el&&!active.el.hasAttribute(HIDDEN))saveScroll(active);
|
|
794
|
-
if(!restoreScroll)return;
|
|
795
|
-
try{
|
|
796
|
-
window.sessionStorage.setItem(SCROLL_STORE+scrollToken,(window.pageXOffset||window.scrollX||0)+','+(window.pageYOffset||window.scrollY||0));
|
|
797
|
-
// Taken as the swap began when there is one: by the time the history
|
|
798
|
-
// is written the outgoing page's scrollers have been replaced.
|
|
799
|
-
var inner=innerSnap!==undefined?innerSnap:innerScroll(activeRoot());
|
|
800
|
-
innerSnap=undefined;
|
|
801
|
-
if(inner)window.sessionStorage.setItem(SCROLL_STORE+scrollToken+':i',JSON.stringify(inner));
|
|
802
|
-
else window.sessionStorage.removeItem(SCROLL_STORE+scrollToken+':i');
|
|
803
|
-
}catch(e){}
|
|
804
|
-
}
|
|
805
|
-
var pendingInner=null;
|
|
806
|
-
var innerSnap;
|
|
807
|
-
function readInner(token){
|
|
808
|
-
try{return JSON.parse(window.sessionStorage.getItem(SCROLL_STORE+token+':i')||'null')}catch(e){return null}
|
|
809
|
-
}
|
|
810
|
-
// One place decides where a swap leaves the viewport, so the fragment path
|
|
811
|
-
// and the whole-document path cannot drift apart.
|
|
812
|
-
function applyScroll(hash){
|
|
813
|
-
if(pendingScroll){
|
|
814
|
-
var at=pendingScroll;
|
|
815
|
-
pendingScroll=null;
|
|
816
|
-
window.scrollTo({left:at[0],top:at[1],behavior:'instant'});
|
|
817
|
-
// Again once the page has bound: a scroller's content is often drawn
|
|
818
|
-
// by a :for that has not run yet, and a box with nothing in it cannot
|
|
819
|
-
// be scrolled anywhere.
|
|
820
|
-
var inner=pendingInner;pendingInner=null;
|
|
821
|
-
if(inner){applyInner(activeRoot(),inner);setTimeout(function(){applyInner(activeRoot(),inner)},60)}
|
|
822
|
-
return;
|
|
823
|
-
}
|
|
824
|
-
pendingInner=null;
|
|
825
|
-
if(o.scrollToTop&&!hash){window.scrollTo({top:0,behavior:'instant'});return}
|
|
826
|
-
if(hash){var el=findIn(hash);if(el)el.scrollIntoView({behavior:'smooth'})}
|
|
827
|
-
}
|
|
828
|
-
// Keeps whatever another script put in the entry; only adds the token.
|
|
829
|
-
function stampScrollToken(){
|
|
830
|
-
var state={};
|
|
831
|
-
var current=history.state;
|
|
832
|
-
if(current&&typeof current==='object')for(var key in current)state[key]=current[key];
|
|
833
|
-
state[SCROLL_TOKEN]=scrollToken;
|
|
834
|
-
state[DEPTH]=histDepth;
|
|
835
|
-
try{history.replaceState(state,'',location.href)}catch(e){}
|
|
836
|
-
}
|
|
837
|
-
if(restoreScroll){
|
|
838
|
-
try{history.scrollRestoration='manual'}catch(e){}
|
|
839
|
-
// A token already in the entry means this document is a reload or a
|
|
840
|
-
// restore of an entry visited before, so its position is worth asking for.
|
|
841
|
-
var known=history.state&&history.state[SCROLL_TOKEN];
|
|
842
|
-
if(known){
|
|
843
|
-
var saved=readScroll(scrollToken);
|
|
844
|
-
if(saved&&(saved[0]||saved[1])){
|
|
845
|
-
var restoreAt=function(){window.scrollTo({left:saved[0],top:saved[1],behavior:'instant'})};
|
|
846
|
-
if(document.readyState==='complete')restoreAt();
|
|
847
|
-
else window.addEventListener('load',restoreAt,{once:true});
|
|
848
|
-
}
|
|
849
|
-
}
|
|
850
|
-
else{
|
|
851
|
-
stampScrollToken();
|
|
852
|
-
}
|
|
853
|
-
// A reload or a link out of the app never calls writeHistory, so this is
|
|
854
|
-
// the only chance to record where the page was left. pagehide fires for
|
|
855
|
-
// both, and unlike beforeunload it does not keep the page out of the
|
|
856
|
-
// back/forward cache.
|
|
857
|
-
window.addEventListener('pagehide',rememberScroll);
|
|
858
|
-
}
|
|
859
|
-
|
|
860
|
-
// Replace, never merge: absent payloads must forget the outgoing page's data.
|
|
861
|
-
// Run at the committed swap, not during prefetch or a superseded navigation.
|
|
862
|
-
function hydrateServerData(html){
|
|
863
|
-
window.__STX_RUNTIME_CONFIG__={};
|
|
864
|
-
var configMatch=html.match(/<script\\b[^>]*\\bdata-stx-runtime-config\\b[^>]*>([\\s\\S]*?)<\\/script>/i);
|
|
865
|
-
if(configMatch){try{var configValues=JSON.parse(configMatch[1]);if(configValues&&typeof configValues==='object'&&!Array.isArray(configValues))window.__STX_RUNTIME_CONFIG__=configValues}catch(e){}}
|
|
866
|
-
window.__STX_DATA__={};
|
|
867
|
-
var match=html.match(/<script\\b[^>]*\\bdata-stx-server-data\\b[^>]*>([\\s\\S]*?)<\\/script>/i);
|
|
868
|
-
if(match){try{var values=JSON.parse(match[1]);if(values&&typeof values==='object'&&!Array.isArray(values))window.__STX_DATA__=values}catch(e){}}
|
|
869
|
-
}
|
|
870
|
-
|
|
871
|
-
// \u2500\u2500 Build skew (stacksjs/stx#1772) \u2500\u2500
|
|
872
|
-
// The build that rendered THIS document, and therefore the build the runtime
|
|
873
|
-
// executing right now came from. Under bun --watch a save restarts the server,
|
|
874
|
-
// so the next navigation can fetch a fragment produced by a newer build; when
|
|
875
|
-
// the scoped-script or binding format drifted between the two, the old runtime
|
|
876
|
-
// cannot hydrate the new fragment \u2014 literal moustaches, dead bindings, stale
|
|
877
|
-
// canvas. Sporadic, and never on a clean boot.
|
|
878
|
-
//
|
|
879
|
-
// Conservative on purpose: act only when BOTH ids are known. A missing id is
|
|
880
|
-
// "no information", never "mismatch", so statically hosted output (no headers)
|
|
881
|
-
// and older servers behave exactly as before.
|
|
882
|
-
var buildMeta=qs(BUILD_META);
|
|
883
|
-
var loadedBuild=buildMeta?(buildMeta.getAttribute('content')||''):'';
|
|
884
|
-
window.__stxBuild=loadedBuild;
|
|
885
|
-
function isBuildSkew(incoming){
|
|
886
|
-
return !!(loadedBuild&&incoming&&incoming!==loadedBuild);
|
|
887
|
-
}
|
|
888
|
-
// A navigation the user asked for loads its destination as a document: no
|
|
889
|
-
// fragment of the newer build can be hydrated by this runtime, and a tap is
|
|
890
|
-
// the one moment a page load is expected anyway. Anything else that notices
|
|
891
|
-
// the newer build (a background revalidation, the offline worker) must not
|
|
892
|
-
// reload a screen the user is in the middle of using, so the reload waits
|
|
893
|
-
// until the page is hidden, and a cold start picks the build up regardless.
|
|
894
|
-
var skewDeferred=false;
|
|
895
|
-
function reloadForSkew(url,incoming,background){
|
|
896
|
-
log('[router] build skew: page is',loadedBuild,'server is',incoming,background?'\u2014 reload when hidden':'\u2014 full navigation');
|
|
897
|
-
if(!background){location.href=url;return}
|
|
898
|
-
if(skewDeferred)return;
|
|
899
|
-
skewDeferred=true;
|
|
900
|
-
var reloadHidden=function(){
|
|
901
|
-
if(document.visibilityState!=='hidden')return;
|
|
902
|
-
document.removeEventListener('visibilitychange',reloadHidden);
|
|
903
|
-
location.reload();
|
|
904
|
-
};
|
|
905
|
-
document.addEventListener('visibilitychange',reloadHidden);
|
|
906
|
-
}
|
|
907
|
-
// A link marked data-stx-transition="none" swaps without the cross-fade. A
|
|
908
|
-
// tab bar is the case: iOS switches tabs instantly, and the fade read as a
|
|
909
|
-
// quarter-second lag on every tap in a phone app. Set by the click that
|
|
910
|
-
// started the navigation, so programmatic and history navigations keep it.
|
|
911
|
-
var instantNext=false;
|
|
912
|
-
var instantNav=false;
|
|
913
|
-
function runViewTransition(callback){
|
|
914
|
-
if(instantNav||!o.viewTransitions||!document.startViewTransition)return false;
|
|
915
|
-
try{
|
|
916
|
-
var transition=document.startViewTransition(callback);
|
|
917
|
-
var onAbort=function(err){log('[router] view transition aborted:',err&&err.message?err.message:err)};
|
|
918
|
-
if(transition&&transition.ready&&transition.ready.catch)transition.ready.catch(onAbort);
|
|
919
|
-
if(transition&&transition.finished&&transition.finished.catch)transition.finished.catch(onAbort);
|
|
920
|
-
if(transition&&transition.updateCallbackDone&&transition.updateCallbackDone.catch)transition.updateCallbackDone.catch(onAbort);
|
|
921
|
-
return true;
|
|
922
|
-
}catch(err){
|
|
923
|
-
log('[router] view transition unavailable:',err&&err.message?err.message:err);
|
|
924
|
-
return false;
|
|
925
|
-
}
|
|
926
|
-
}
|
|
927
|
-
|
|
928
|
-
// \u2500\u2500 Instant navigation: one frame, already filled \u2500\u2500
|
|
929
|
-
// A tab swap used to paint the new screen before its scripts had run: a page
|
|
930
|
-
// script with an import is a module, and a module runs after the task that
|
|
931
|
-
// inserted it, so for a few frames the screen showed its bare markup and then
|
|
932
|
-
// filled in, on every visit. Each inserted page script now reports when it
|
|
933
|
-
// has run, and an instant navigation holds the old frame (a View Transition
|
|
934
|
-
// with no animation) until they all have, capped so it can never hang.
|
|
935
|
-
var scriptsPending=0;
|
|
936
|
-
var hydrateWaiters=[];
|
|
937
|
-
window.__stxScriptRan=function(){
|
|
938
|
-
if(scriptsPending>0)scriptsPending--;
|
|
939
|
-
if(!scriptsPending){var w=hydrateWaiters;hydrateWaiters=[];w.forEach(function(f){f()})}
|
|
940
|
-
};
|
|
941
|
-
function markRan(code){scriptsPending++;return code+String.fromCharCode(10)+';window.__stxScriptRan&&window.__stxScriptRan();'}
|
|
942
|
-
function whenHydrated(capMs){
|
|
943
|
-
return new Promise(function(resolve){
|
|
944
|
-
var done=false;
|
|
945
|
-
function finish(){if(done)return;done=true;setTimeout(resolve,0)}
|
|
946
|
-
if(!scriptsPending){done=true;resolve();return}
|
|
947
|
-
hydrateWaiters.push(finish);
|
|
948
|
-
setTimeout(finish,capMs);
|
|
949
|
-
});
|
|
950
|
-
}
|
|
951
|
-
// Whether a fragment's scripts all run inside the swap's own task: none is
|
|
952
|
-
// a module, and every file it names is already loaded. Then an instant
|
|
953
|
-
// navigation needs no held frame at all: swapped, run and bound before the
|
|
954
|
-
// browser next paints, it shows the new screen whole in the first frame.
|
|
955
|
-
function swapRunsInOneTask(html,base){
|
|
956
|
-
var sync=true;
|
|
957
|
-
html.replace(new RegExp('<scr'+'ipt\\\\b([^>]*)>([\\\\s\\\\S]*?)<\\\\/scr'+'ipt>','gi'),function(m,attrs,code){
|
|
958
|
-
if(!sync)return m;
|
|
959
|
-
var typeMatch=attrs.match(/\\btype\\s*=\\s*(?:"([^"]*)"|'([^']*)'|([^\\s>]+))/i);
|
|
960
|
-
var scriptType=((typeMatch&&(typeMatch[1]||typeMatch[2]||typeMatch[3]))||'').trim().toLowerCase();
|
|
961
|
-
if(scriptType==='module'){sync=false;return m}
|
|
962
|
-
if(scriptType&&scriptType!=='text/javascript'&&scriptType!=='application/javascript')return m;
|
|
963
|
-
var srcMatch=attrs.match(/\\bsrc\\s*=\\s*(?:"([^"]*)"|'([^']*)'|([^\\s>]+))/i);
|
|
964
|
-
var src=srcMatch&&(srcMatch[1]||srcMatch[2]||srcMatch[3]);
|
|
965
|
-
if(src){
|
|
966
|
-
if(!isSharedClientScriptAttrs(attrs)){var key=externalScriptKey(src,base);if(key&&!loadedExternalScripts[key])sync=false}
|
|
967
|
-
return m;
|
|
968
|
-
}
|
|
969
|
-
if(code&&hasStaticImport(code))sync=false;
|
|
970
|
-
return m;
|
|
971
|
-
});
|
|
972
|
-
return sync;
|
|
973
|
-
}
|
|
974
|
-
function runInOneTask(complete){
|
|
975
|
-
// The runtime binds on stx:load after a debounce; bind now, in this task.
|
|
976
|
-
function flush(){if(window.stx&&typeof window.stx._flushLoad==='function')window.stx._flushLoad()}
|
|
977
|
-
window.addEventListener('stx:load',flush,{once:true});
|
|
978
|
-
try{complete()}finally{window.removeEventListener('stx:load',flush)}
|
|
979
|
-
}
|
|
980
|
-
function runInstantSwap(complete){
|
|
981
|
-
if(!document.startViewTransition)return false;
|
|
982
|
-
var root=document.documentElement;
|
|
983
|
-
root.classList.add('stx-instant');
|
|
984
|
-
var clear=function(){root.classList.remove('stx-instant')};
|
|
985
|
-
try{
|
|
986
|
-
var transition=document.startViewTransition(function(){
|
|
987
|
-
scriptsPending=0;
|
|
988
|
-
// The runtime binds the page on stx:load after a short debounce; run
|
|
989
|
-
// it now, so the frame this reveals is bound rather than showing its
|
|
990
|
-
// {{ }} for a frame before they fill in.
|
|
991
|
-
var loaded=new Promise(function(resolve){window.addEventListener('stx:load',function(){if(window.stx&&typeof window.stx._flushLoad==='function')window.stx._flushLoad();resolve()},{once:true});setTimeout(resolve,300)});
|
|
992
|
-
complete();
|
|
993
|
-
return loaded.then(function(){return whenHydrated(300)});
|
|
994
|
-
});
|
|
995
|
-
if(transition&&transition.finished&&transition.finished.then)transition.finished.then(clear,clear);else clear();
|
|
996
|
-
if(transition&&transition.ready&&transition.ready.catch)transition.ready.catch(function(){});
|
|
997
|
-
return true;
|
|
998
|
-
}catch(err){
|
|
999
|
-
clear();
|
|
1000
|
-
log('[router] instant transition unavailable:',err&&err.message?err.message:err);
|
|
1001
|
-
return false;
|
|
1002
|
-
}
|
|
1003
|
-
}
|
|
1004
|
-
|
|
1005
|
-
// \u2500\u2500 Progress bar (0\u2192100% at top of viewport) \u2500\u2500
|
|
1006
|
-
// Native loading indicator baked into the router. Starts when navigation
|
|
1007
|
-
// begins, trickles up to ~90% during fetch, snaps to 100% + fades on
|
|
1008
|
-
// completion. Opt-out via window.__stxRouterConfig={progress:false}.
|
|
1009
|
-
var progEl=null;
|
|
1010
|
-
var progVal=0;
|
|
1011
|
-
var progTimer=null;
|
|
1012
|
-
function setProgress(v){
|
|
1013
|
-
progVal=v<0?0:(v>100?100:v);
|
|
1014
|
-
if(!progEl)return;
|
|
1015
|
-
progEl.style.opacity=progVal>0?'1':'0';
|
|
1016
|
-
progEl.style.transform='scaleX('+(progVal/100)+')';
|
|
1017
|
-
}
|
|
1018
|
-
// Shown only for a navigation that is actually waiting. A page served from
|
|
1019
|
-
// the prefetch cache swaps in a few milliseconds, and drawing then fading a
|
|
1020
|
-
// bar for it made every instant navigation look like a load.
|
|
1021
|
-
var progDelay=null;
|
|
1022
|
-
function startProgress(){
|
|
1023
|
-
if(!o.progress||!progEl)return;
|
|
1024
|
-
if(progTimer){clearInterval(progTimer);progTimer=null}
|
|
1025
|
-
if(progDelay)clearTimeout(progDelay);
|
|
1026
|
-
progDelay=setTimeout(function(){
|
|
1027
|
-
progDelay=null;
|
|
1028
|
-
setProgress(8);
|
|
1029
|
-
progTimer=setInterval(function(){
|
|
1030
|
-
if(progVal>=90)return;
|
|
1031
|
-
// Asymptotic trickle \u2014 fast at first, slower near 90%
|
|
1032
|
-
var inc=Math.max(0.4,(90-progVal)*0.08);
|
|
1033
|
-
setProgress(progVal+inc);
|
|
1034
|
-
},160);
|
|
1035
|
-
},150);
|
|
1036
|
-
}
|
|
1037
|
-
function finishProgress(){
|
|
1038
|
-
if(!o.progress||!progEl)return;
|
|
1039
|
-
if(progDelay){clearTimeout(progDelay);progDelay=null;return}
|
|
1040
|
-
if(progTimer){clearInterval(progTimer);progTimer=null}
|
|
1041
|
-
setProgress(100);
|
|
1042
|
-
// Fade out, then reset. Delays chosen so the "full" state is briefly
|
|
1043
|
-
// visible before the bar disappears.
|
|
1044
|
-
setTimeout(function(){
|
|
1045
|
-
if(!progEl)return;
|
|
1046
|
-
progEl.style.opacity='0';
|
|
1047
|
-
setTimeout(function(){progVal=0;if(progEl)progEl.style.transform='scaleX(0)'},260);
|
|
1048
|
-
},180);
|
|
1049
|
-
}
|
|
1050
|
-
|
|
1051
|
-
var cache={};
|
|
1052
|
-
// When each entry was fetched, so a cache hit knows whether it is worth
|
|
1053
|
-
// checking behind the swap (revalidate).
|
|
1054
|
-
var cacheAt={};
|
|
1055
|
-
var prefetching={};
|
|
1056
|
-
var awaitingKey='';
|
|
1057
|
-
var isNavigating=false;
|
|
1058
|
-
|
|
1059
|
-
// LRU bookkeeping for cache/layoutCache/layoutGroupCache. Pre-fix
|
|
1060
|
-
// (stacksjs/stx#1719) these were plain objects with no eviction, so
|
|
1061
|
-
// hover-prefetch on a sidebar of 200 internal links retained ~10MB
|
|
1062
|
-
// of HTML strings for the page's lifetime. cacheOrder is a most-
|
|
1063
|
-
// recently-used-last list of keys; setCache pushes new entries and
|
|
1064
|
-
// evicts the oldest when length exceeds o.prefetchCacheMax (default
|
|
1065
|
-
// 50). touchCache promotes a key on cache-hit. We use a plain array
|
|
1066
|
-
// rather than a Map to keep the diff localized and the runtime
|
|
1067
|
-
// hand-minified shape consistent.
|
|
1068
|
-
var cacheOrder=[];
|
|
1069
|
-
function setCache(key,html,layout,group,title,cattrs){
|
|
1070
|
-
if(!(key in cache))cacheOrder.push(key);
|
|
1071
|
-
cache[key]=html;
|
|
1072
|
-
layoutCache[key]=layout;
|
|
1073
|
-
layoutGroupCache[key]=group;
|
|
1074
|
-
titleCache[key]=title||'';
|
|
1075
|
-
attrsCache[key]=cattrs||'';
|
|
1076
|
-
cacheAt[key]=Date.now();
|
|
1077
|
-
while(cacheOrder.length>o.prefetchCacheMax){
|
|
1078
|
-
var oldest=cacheOrder.shift();
|
|
1079
|
-
delete cache[oldest];
|
|
1080
|
-
delete layoutCache[oldest];
|
|
1081
|
-
delete layoutGroupCache[oldest];
|
|
1082
|
-
delete titleCache[oldest];
|
|
1083
|
-
delete attrsCache[oldest];
|
|
1084
|
-
delete cacheAt[oldest];
|
|
1085
|
-
}
|
|
1086
|
-
}
|
|
1087
|
-
// Read a prefetch response into the shape setCache stores, or null when this
|
|
1088
|
-
// answer must not be cached at all.
|
|
1089
|
-
//
|
|
1090
|
-
// Both prefetch paths used to assemble this by hand, and both left out
|
|
1091
|
-
// X-STX-Container-Attrs (#1947): setCache takes six arguments and they
|
|
1092
|
-
// passed five, which JavaScript accepts without a word. The entry was
|
|
1093
|
-
// cached with no container attributes, so a link that had been hovered
|
|
1094
|
-
// before it was clicked applied '' on the cache hit, and setContainerAttrs
|
|
1095
|
-
// stripped the class the previous navigation had put on the routed
|
|
1096
|
-
// container. A dashboard lost its max-width, but only on links that
|
|
1097
|
-
// happened to be hovered first, and a hard reload always fixed it \u2014 which
|
|
1098
|
-
// made it read as a CSS problem.
|
|
1099
|
-
//
|
|
1100
|
-
// Hand-assembling it twice is what let them drift, so the two guards below
|
|
1101
|
-
// live here rather than at the call sites \u2014 the hover path had one of them
|
|
1102
|
-
// and the public router.prefetch() had neither:
|
|
1103
|
-
//
|
|
1104
|
-
// - a redirected body belongs to the URL it came FROM. A guarded route
|
|
1105
|
-
// prefetched while logged out answers with /login's markup, and storing
|
|
1106
|
-
// that under the guarded key poisons the cache: the later real
|
|
1107
|
-
// navigation is a hit, so it swaps the login page in without ever
|
|
1108
|
-
// reaching the network, where navigate's own redirect check would have
|
|
1109
|
-
// caught it (#1849).
|
|
1110
|
-
// - a fragment from a DIFFERENT build must not be handed to this page's
|
|
1111
|
-
// runtime (#1772). navigate answers skew by reloading; a prefetch must
|
|
1112
|
-
// not, because the user only hovered a link. Declining to cache is the
|
|
1113
|
-
// whole fix \u2014 the click then goes to the network, where navigate does
|
|
1114
|
-
// the reload properly.
|
|
1115
|
-
function readPrefetchResponse(r,wantsFragment){
|
|
1116
|
-
if(r.redirected||r.headers.get('X-STX-Location'))return Promise.resolve(null);
|
|
1117
|
-
if(isBuildSkew(r.headers.get('X-STX-Build')||''))return Promise.resolve(null);
|
|
1118
|
-
var isFrag=wantsFragment&&r.headers.get('X-STX-Fragment')==='true';
|
|
1119
|
-
var layout=r.headers.get('X-STX-Layout')||'';
|
|
1120
|
-
var group=r.headers.get('X-STX-Layout-Group')||'';
|
|
1121
|
-
var title=r.headers.get('X-STX-Title')||'';
|
|
1122
|
-
var cattrs=r.headers.get('X-STX-Container-Attrs')||'';
|
|
1123
|
-
var runtime=r.headers.get('X-STX-Runtime')||'';
|
|
1124
|
-
return r.text().then(function(html){return{html:isFrag?fragmentMarker(runtime)+html:html,layout:layout,layoutGroup:group,title:title,containerAttrs:cattrs}});
|
|
1125
|
-
}
|
|
1126
|
-
// Apply a page title captured from the fragment response's X-STX-Title
|
|
1127
|
-
// header (URI-encoded) so SPA swaps keep document.title in sync with the
|
|
1128
|
-
// page \u2014 full-document swaps set the title from the <title> tag directly.
|
|
1129
|
-
function applyTitle(t){if(t){try{document.title=decodeURIComponent(t)}catch(e){document.title=t}}}
|
|
1130
|
-
// \u2500\u2500 Container attributes \u2500\u2500
|
|
1131
|
-
// A fragment carries only the container's INNER content, so any layout that
|
|
1132
|
-
// lives on the destination's <main> itself (e.g. a centered auth page whose
|
|
1133
|
-
// <main> carries flex + min-h-[100dvh] + items-center) would be
|
|
1134
|
-
// dropped when injected into the persistent container. The server sends the
|
|
1135
|
-
// destination container's attributes in X-STX-Container-Attrs (URI-encoded);
|
|
1136
|
-
// we apply them here and track what we applied so navigating onward to a page
|
|
1137
|
-
// with a bare <main> clears them again. Attributes the router never applied
|
|
1138
|
-
// (the app's own id/data-stx-content hooks) are left untouched.
|
|
1139
|
-
var pendingContainerAttrs='';
|
|
1140
|
-
function parseContainerAttrs(attrStr){
|
|
1141
|
-
var map={};
|
|
1142
|
-
if(!attrStr)return map;
|
|
1143
|
-
var probe=ce('div');
|
|
1144
|
-
probe.innerHTML='<i '+attrStr+'></i>';
|
|
1145
|
-
var src=probe.firstChild;
|
|
1146
|
-
// getAttributeNames first: it is the same standard DOM answer and it is
|
|
1147
|
-
// what the test DOM implements. Reading .attributes alone made this
|
|
1148
|
-
// silently return {} there \u2014 so the whole container-attrs path, header to
|
|
1149
|
-
// element, had no test coverage at all while #1947 sat in it.
|
|
1150
|
-
if(src&&src.getAttributeNames){
|
|
1151
|
-
src.getAttributeNames().forEach(function(n){map[n]=src.getAttribute(n)});
|
|
1152
|
-
}
|
|
1153
|
-
else if(src&&src.attributes){
|
|
1154
|
-
Array.prototype.forEach.call(src.attributes,function(a){map[a.name]=a.value});
|
|
1155
|
-
}
|
|
1156
|
-
return map;
|
|
1157
|
-
}
|
|
1158
|
-
function setContainerAttrs(el,map){
|
|
1159
|
-
if(!el)return;
|
|
1160
|
-
var prev=(el.getAttribute('data-stx-cattrs')||'').split(',');
|
|
1161
|
-
prev.forEach(function(n){if(n&&!(n in map))el.removeAttribute(n)});
|
|
1162
|
-
var names=[];
|
|
1163
|
-
for(var k in map){
|
|
1164
|
-
if(Object.prototype.hasOwnProperty.call(map,k)&&k!=='data-stx-cattrs'){
|
|
1165
|
-
el.setAttribute(k,map[k]);names.push(k);
|
|
1166
|
-
}
|
|
1167
|
-
}
|
|
1168
|
-
if(names.length)el.setAttribute('data-stx-cattrs',names.join(','));
|
|
1169
|
-
else el.removeAttribute('data-stx-cattrs');
|
|
1170
|
-
}
|
|
1171
|
-
function applyContainerAttrs(el,encoded){
|
|
1172
|
-
var attrStr='';
|
|
1173
|
-
if(encoded){try{attrStr=decodeURIComponent(encoded)}catch(e){attrStr=encoded}}
|
|
1174
|
-
setContainerAttrs(el,parseContainerAttrs(attrStr));
|
|
1175
|
-
}
|
|
1176
|
-
function touchCache(key){
|
|
1177
|
-
var i=cacheOrder.indexOf(key);
|
|
1178
|
-
if(i>=0){cacheOrder.splice(i,1);cacheOrder.push(key)}
|
|
1179
|
-
}
|
|
1180
|
-
function evictCache(key){
|
|
1181
|
-
var i=cacheOrder.indexOf(key);
|
|
1182
|
-
if(i>=0)cacheOrder.splice(i,1);
|
|
1183
|
-
delete cache[key];
|
|
1184
|
-
delete layoutCache[key];
|
|
1185
|
-
delete layoutGroupCache[key];
|
|
1186
|
-
delete titleCache[key];
|
|
1187
|
-
delete attrsCache[key];
|
|
1188
|
-
delete cacheAt[key];
|
|
1189
|
-
}
|
|
1190
|
-
|
|
1191
|
-
// Extract top-level CSS blocks (rules AND @media blocks with nested braces).
|
|
1192
|
-
// The old regex ([^{}]+{[^{}]*}) silently dropped @media blocks because
|
|
1193
|
-
// it couldn't handle nested braces \u2014 causing responsive classes (lg:, md:)
|
|
1194
|
-
// to vanish after SPA navigation.
|
|
1195
|
-
function extractCssBlocks(css){
|
|
1196
|
-
var blocks=[];
|
|
1197
|
-
var i=0;
|
|
1198
|
-
var len=css.length;
|
|
1199
|
-
while(i<len){
|
|
1200
|
-
// Skip whitespace
|
|
1201
|
-
while(i<len&&(css[i]===' '||css[i]==='\\n'||css[i]==='\\r'||css[i]==='\\t'))i++;
|
|
1202
|
-
if(i>=len)break;
|
|
1203
|
-
// Find the start of a block (first {)
|
|
1204
|
-
var blockStart=i;
|
|
1205
|
-
var bracePos=css.indexOf('{',i);
|
|
1206
|
-
if(bracePos===-1)break;
|
|
1207
|
-
// Walk forward tracking brace depth to find the matching close
|
|
1208
|
-
var depth=0;
|
|
1209
|
-
var j=bracePos;
|
|
1210
|
-
while(j<len){
|
|
1211
|
-
if(css[j]==='{')depth++;
|
|
1212
|
-
else if(css[j]==='}'){depth--;if(depth===0){j++;break}}
|
|
1213
|
-
j++;
|
|
1214
|
-
}
|
|
1215
|
-
if(depth!==0)break;
|
|
1216
|
-
blocks.push(css.slice(blockStart,j).trim());
|
|
1217
|
-
i=j;
|
|
1218
|
-
}
|
|
1219
|
-
return blocks;
|
|
1220
|
-
}
|
|
1221
|
-
|
|
1222
|
-
function mergeCss(existing,incoming){
|
|
1223
|
-
var blocks=extractCssBlocks(incoming);
|
|
1224
|
-
var newBlocks=[];
|
|
1225
|
-
for(var bi=0;bi<blocks.length;bi++){
|
|
1226
|
-
if(existing.indexOf(blocks[bi])===-1)newBlocks.push(blocks[bi]);
|
|
1227
|
-
}
|
|
1228
|
-
if(newBlocks.length>0)return existing+'\\n'+newBlocks.join('\\n');
|
|
1229
|
-
return null;
|
|
1230
|
-
}
|
|
1231
|
-
|
|
1232
|
-
function getContainer(){
|
|
1233
|
-
return qs(containerSel)||qs('[data-stx-content]')||qs('main');
|
|
1234
|
-
}
|
|
1235
|
-
|
|
1236
|
-
// \u2500\u2500 Navigation \u2500\u2500
|
|
1237
|
-
// Layout group detection \u2014 layouts in the same group share a <main> container.
|
|
1238
|
-
// Only truly different layout groups trigger a full page reload.
|
|
1239
|
-
var layoutCache={};
|
|
1240
|
-
var layoutGroupCache={};
|
|
1241
|
-
var titleCache={};
|
|
1242
|
-
var attrsCache={};
|
|
1243
|
-
// Track executed script hashes to prevent redeclaration errors on navigation.
|
|
1244
|
-
// Layout-level scripts (theme, nav setup) execute on initial page load and
|
|
1245
|
-
// should NOT re-execute when navigating to another page with the same layout.
|
|
1246
|
-
var executedScriptHashes={};
|
|
1247
|
-
|
|
1248
|
-
// \u2500\u2500 Re-execution contract (stacksjs/stx#1773) \u2500\u2500
|
|
1249
|
-
// Whether a generated script may run again after an SPA swap used to be
|
|
1250
|
-
// SNIFFED from its source: does it start with '(' or ';', or mention
|
|
1251
|
-
// window.stx.mount. That is a property of the emitted shape, not of intent,
|
|
1252
|
-
// so every change to how scripts are wrapped was one edit away from silently
|
|
1253
|
-
// reclassifying them \u2014 and the symptom of getting it wrong is a component
|
|
1254
|
-
// that renders completely dead (literal moustaches, stuck :show) only on a
|
|
1255
|
-
// REVISIT, because nav-away already disposed its scope.
|
|
1256
|
-
//
|
|
1257
|
-
// Emitters now declare it: data-stx-run="always" on self-contained scope
|
|
1258
|
-
// IIFEs and mount() wrappers, which own their scope and whose registration
|
|
1259
|
-
// _cleanupContainer removed on the way out. Anything unstamped falls back to
|
|
1260
|
-
// the old sniff, so a page rendered by an older server behaves exactly as
|
|
1261
|
-
// before.
|
|
1262
|
-
//
|
|
1263
|
-
// The sniff itself now asks what the script REGISTERS rather than how it is
|
|
1264
|
-
// spelled (#1828). cleanupContainer deletes window.stx._scopes entries,
|
|
1265
|
-
// element disposers and destroy hooks on the way out, so the scripts that
|
|
1266
|
-
// must run again are exactly the ones that put a scope back. That is
|
|
1267
|
-
// detectable; a leading character is not.
|
|
1268
|
-
//
|
|
1269
|
-
// Deliberately NOT "strip leading comments, then test the first character",
|
|
1270
|
-
// which is the obvious repair and is wrong in the expensive direction. Three
|
|
1271
|
-
// emitters open with a comment and MUST NOT re-run: the animation scripts
|
|
1272
|
-
// register matchMedia and DOMContentLoaded listeners plus an
|
|
1273
|
-
// IntersectionObserver, and the STX lifecycle runtime owns a Map of live
|
|
1274
|
-
// instances. Comment-stripping would flip all three to re-running on every
|
|
1275
|
-
// navigation, leaking a listener and an observer each time and resetting the
|
|
1276
|
-
// instance registry \u2014 trading a silent missing component for a silent leak.
|
|
1277
|
-
var REGISTERS_SCOPE=/window\\.stx\\.mount|_scopes\\s*\\[|\\.initScope\\s*\\(|__stxComponentFactories\\s*\\[/;
|
|
1278
|
-
// The page's module registry (#1957): the bundle every component that
|
|
1279
|
-
// imports a local module reads from, so on navigation it has to run before
|
|
1280
|
-
// any of them. Marked data-stx-modules; the text test also finds it where
|
|
1281
|
-
// only a script's text survives. Its wrapper names __stxModuleBundles, which
|
|
1282
|
-
// no component script does -- they read __stxModules.
|
|
1283
|
-
var REGISTRY_TEXT=/__stxModuleBundles/;
|
|
1284
|
-
function isModuleRegistryScript(s){
|
|
1285
|
-
return !!s&&((s.hasAttribute&&s.hasAttribute('data-stx-modules'))||REGISTRY_TEXT.test(s.textContent||''));
|
|
1286
|
-
}
|
|
1287
|
-
// The page's composables (resources/functions and friends), published as
|
|
1288
|
-
// globals its templates call. Each page ships the ones it reaches, so the
|
|
1289
|
-
// next page can need one the first never loaded: it runs right after the
|
|
1290
|
-
// registry, before anything that calls into it. Guarded per bundle, so a
|
|
1291
|
-
// repeat is a no-op.
|
|
1292
|
-
var COMPOSABLES_TEXT=/__stxComposableBundles/;
|
|
1293
|
-
function isComposablesScript(s){
|
|
1294
|
-
return !!s&&((s.hasAttribute&&s.hasAttribute('data-stx-composables'))||COMPOSABLES_TEXT.test(s.textContent||''));
|
|
1295
|
-
}
|
|
1296
|
-
/*
|
|
1297
|
-
* What the block wrap below would otherwise hide (stacksjs/stx#2041).
|
|
1298
|
-
*
|
|
1299
|
-
* A re-executed script is wrapped in a bare block so that top-level const
|
|
1300
|
-
* and let cannot collide across navigations. That wrap is invisible for a
|
|
1301
|
-
* plain function declaration -- Annex B.3.3 still hoists its binding to the
|
|
1302
|
-
* enclosing scope in sloppy mode -- and NOT invisible for anything else.
|
|
1303
|
-
* async function, function*, class, const and let are all block-scoped, so
|
|
1304
|
-
* the same source published a global when the browser parsed the script and
|
|
1305
|
-
* published nothing when the router re-ran it.
|
|
1306
|
-
*
|
|
1307
|
-
* The effect is a divergence between development and a built app, because
|
|
1308
|
-
* only the built one reaches a route as a fragment: a page served whole runs
|
|
1309
|
-
* its script natively. An inline onclick that names such a declaration then
|
|
1310
|
-
* worked in dev and threw ReferenceError in the packaged app, where there is
|
|
1311
|
-
* no console, so it read as a button that does nothing.
|
|
1312
|
-
*
|
|
1313
|
-
* So: re-publish what the browser would have published. Only values that are
|
|
1314
|
-
* functions, which is what an inline handler calls -- assigning every
|
|
1315
|
-
* top-level const would otherwise write over window.name and friends, which
|
|
1316
|
-
* a lexical global shadows rather than overwrites. Each name is guarded on
|
|
1317
|
-
* its own so an unmatched one cannot take the rest down, and typeof is safe
|
|
1318
|
-
* on a name that was never declared.
|
|
1319
|
-
*
|
|
1320
|
-
* Column-anchored: a declaration nested inside a function is indented, and
|
|
1321
|
-
* is not in scope at the end of the block anyway.
|
|
1322
|
-
*/
|
|
1323
|
-
var TLD=/^(?:export\\s+)?(?:async\\s+)?(?:function\\s*\\*?|class|const|let|var)\\s+([$\\w]+)/gm;
|
|
1324
|
-
function republishTopLevel(src){
|
|
1325
|
-
var n=[],m,o='',i;
|
|
1326
|
-
TLD.lastIndex=0;
|
|
1327
|
-
while((m=TLD.exec(src))!==null)if(n.indexOf(m[1])<0)n.push(m[1]);
|
|
1328
|
-
for(i=0;i<n.length;i++)o+='try{typeof '+n[i]+'=="function"&&(window.'+n[i]+'='+n[i]+')}catch(e){}';
|
|
1329
|
-
return o?'\\n;'+o:'';
|
|
1330
|
-
}
|
|
1331
|
-
function runsAlways(declared, code){
|
|
1332
|
-
if(declared==='always')return true;
|
|
1333
|
-
if(declared==='once')return false;
|
|
1334
|
-
if(REGISTERS_SCOPE.test(code))return true;
|
|
1335
|
-
var head=code.trimStart();
|
|
1336
|
-
return head.charAt(0)==='('||head.charAt(0)===';';
|
|
1337
|
-
}
|
|
1338
|
-
// A component scope script names the root it binds in data-stx-owner (#1958).
|
|
1339
|
-
// On navigation it runs only for a root that just ARRIVED: one swapped in
|
|
1340
|
-
// with the new content, which is not bound yet. A layout component that
|
|
1341
|
-
// stayed on screen is skipped, in both shapes that reach here:
|
|
1342
|
-
// - its root is gone: it keeps its first render's id while the fragment
|
|
1343
|
-
// carries the next page's id for it (ids are page-keyed on purpose), so
|
|
1344
|
-
// the script has no markup of its own;
|
|
1345
|
-
// - its root is present and already bound: the same page file on both
|
|
1346
|
-
// sides of the navigation gives the same id.
|
|
1347
|
-
// Either way running it built a second instance of a component whose markup
|
|
1348
|
-
// stays bound to the first -- setup and its side effects repeated on every
|
|
1349
|
-
// navigation. Scripts without an owner are unaffected.
|
|
1350
|
-
function ownerStays(owner){
|
|
1351
|
-
if(!owner)return false;
|
|
1352
|
-
var root=qs('[data-stx-scope="'+owner+'"]');
|
|
1353
|
-
return !root||typeof root.__stx_disposers==='function';
|
|
1354
|
-
}
|
|
1355
|
-
function hashScript(code){
|
|
1356
|
-
var h=0;for(var i=0;i<code.length;i++){h=((h<<5)-h)+code.charCodeAt(i);h|=0}
|
|
1357
|
-
return h;
|
|
1358
|
-
}
|
|
1359
|
-
function hasStaticImport(code){
|
|
1360
|
-
return /(?:^|\\n)[ \\t]*import(?:[ \\t]+|[{*'"])/.test(code);
|
|
1361
|
-
}
|
|
1362
|
-
function generatedSetupName(code){
|
|
1363
|
-
var match=code.match(/function (__stx_setup_[A-Za-z0-9_]+)\\s*\\(/);
|
|
1364
|
-
return match?match[1]:'';
|
|
1365
|
-
}
|
|
1366
|
-
// Does this fetched DOCUMENT carry the signals runtime?
|
|
1367
|
-
//
|
|
1368
|
-
// Unlike a fragment, a document holds the answer directly, so there is
|
|
1369
|
-
// nothing to infer from markers or ask the server for: look for the script.
|
|
1370
|
-
// Both emitted shapes carry data-stx-runtime \u2014 inline, and the serve-mode
|
|
1371
|
-
// src="/_stx/runtime.<hash>.js" \u2014 with the content sniff behind it for a
|
|
1372
|
-
// document rendered by a server old enough not to stamp the attribute.
|
|
1373
|
-
function documentShipsRuntime(doc,html){
|
|
1374
|
-
if(doc&&doc.querySelector&&doc.querySelector('script[data-stx-runtime]'))return true;
|
|
1375
|
-
return html.indexOf("'use strict';var cloakStyle")!==-1
|
|
1376
|
-
||(html.indexOf('_cleanupContainer')!==-1&&html.indexOf('signals runtime loading')!==-1);
|
|
1377
|
-
}
|
|
1378
|
-
function isSignalsRuntimeScript(script,code){
|
|
1379
|
-
return !!(script&&script.hasAttribute&&script.hasAttribute('data-stx-runtime'))
|
|
1380
|
-
||(code.indexOf('_cleanupContainer')!==-1&&code.indexOf('signals runtime loading')!==-1)
|
|
1381
|
-
||code.indexOf("'use strict';var cloakStyle")!==-1;
|
|
1382
|
-
}
|
|
1383
|
-
// The runtime and the router themselves, as external script tags.
|
|
1384
|
-
//
|
|
1385
|
-
// Serve mode links both by content hash, so after a deploy the incoming
|
|
1386
|
-
// page's copy has a different URL from the one this document loaded. Every
|
|
1387
|
-
// path below that loads a script[src] it has not seen yet would then run a
|
|
1388
|
-
// second router and a second runtime over the live ones \u2014 two click
|
|
1389
|
-
// handlers on every link, a fresh window.stx under scopes created by the
|
|
1390
|
-
// old one. A new version is picked up by the build-skew reload instead; it
|
|
1391
|
-
// is never loaded into a running page. The attrs form is for the fragment
|
|
1392
|
-
// path, which sees a tag as text.
|
|
1393
|
-
function isSharedClientScript(el){
|
|
1394
|
-
return !!(el&&el.hasAttribute&&(el.hasAttribute('data-stx-runtime')||el.hasAttribute('data-stx-router')));
|
|
1395
|
-
}
|
|
1396
|
-
function isSharedClientScriptAttrs(attrs){
|
|
1397
|
-
return /(?:^|\\s)data-stx-(?:runtime|router)(?:[\\s=]|$)/i.test(attrs||'');
|
|
1398
|
-
}
|
|
1399
|
-
// A fragment never carries the signals runtime: the server strips the runtime
|
|
1400
|
-
// IIFE out of it, and every re-execution path here filters it out again. So a
|
|
1401
|
-
// page that never needed a runtime cannot hydrate a fragment that does \u2014 the
|
|
1402
|
-
// setup function would be defined but never invoked, scope IIFEs throw while
|
|
1403
|
-
// destructuring window.stx, and every x-cloak element stays display:none
|
|
1404
|
-
// forever against the cloak style shipped on EVERY page. Hand those off to a
|
|
1405
|
-
// real navigation, which loads the destination's own runtime (#1809).
|
|
1406
|
-
//
|
|
1407
|
-
// The SERVER decides this now, and says so in X-STX-Runtime, which travels
|
|
1408
|
-
// into the fragment marker as rt=1 / rt=0 so it survives the prefetch cache.
|
|
1409
|
-
// It is answering a question it knows the answer to exactly \u2014 did I put a
|
|
1410
|
-
// runtime in this page \u2014 where the router could only guess from markup.
|
|
1411
|
-
//
|
|
1412
|
-
// Guessing missed a whole page shape (#1827). Every marker below originates in
|
|
1413
|
-
// a client SCRIPT, so a page with reactive syntax and no script anywhere \u2014 a
|
|
1414
|
-
// <script server>-only login form built from :if / x-model / :disabled \u2014 emits
|
|
1415
|
-
// none of them, while the server ships it the runtime. Whatever root marker it
|
|
1416
|
-
// does get lands on <body>, which a fragment excludes by construction. So
|
|
1417
|
-
// <p :if="error" x-cloak>Invalid credentials</p>
|
|
1418
|
-
// swapped into a runtime-less page keeps the x-cloak the server stamped on it,
|
|
1419
|
-
// and only the runtime ever removes that: server-rendered text, in the DOM, at
|
|
1420
|
-
// display:none forever.
|
|
1421
|
-
//
|
|
1422
|
-
// The same held for x-data on the routed container, whose scope attribute is
|
|
1423
|
-
// hoisted out of the fragment body into X-STX-Container-Attrs. Asking the
|
|
1424
|
-
// server covers both without either being enumerated.
|
|
1425
|
-
//
|
|
1426
|
-
// The sniff stays as the fallback for a server too old to declare it. Its
|
|
1427
|
-
// discriminators matter. x-cloak and data-stx-scoped are NOT usable: each
|
|
1428
|
-
// fragment inlines the head styles, cloak rule included, and the appearance
|
|
1429
|
-
// bootstrap emits a self-contained data-stx-scoped script on pages with no
|
|
1430
|
-
// reactivity at all. A bare window.stx is not usable either \u2014 the server
|
|
1431
|
-
// prepends a _latestSetup=null clear script to every fragment. Any of those
|
|
1432
|
-
// would make this always true and disable SPA navigation everywhere.
|
|
1433
|
-
function fragmentNeedsRuntime(frag,declared){
|
|
1434
|
-
if(declared==='1')return true;
|
|
1435
|
-
if(declared==='0')return false;
|
|
1436
|
-
if(frag.indexOf('__stx_setup_')!==-1
|
|
1437
|
-
||frag.indexOf('data-stx-scope=')!==-1
|
|
1438
|
-
||frag.indexOf('data-stx-reactive')!==-1
|
|
1439
|
-
||/=\\s*window\\.stx\\s*;/.test(frag))return true;
|
|
1440
|
-
var content=stripInertRegions(frag);
|
|
1441
|
-
return BARE_DIRECTIVE.test(content)||content.indexOf('{{')!==-1;
|
|
1442
|
-
}
|
|
1443
|
-
// Documentation is both the largest population of runtime-less pages and the
|
|
1444
|
-
// likeliest to contain directive syntax as literal CONTENT, so the fallback
|
|
1445
|
-
// reads past the regions where that content lives. A page of stx examples must
|
|
1446
|
-
// not full-reload on every hop through the docs \u2014 that is the regression
|
|
1447
|
-
// #1809 existed to remove.
|
|
1448
|
-
function stripInertRegions(frag){
|
|
1449
|
-
return frag
|
|
1450
|
-
.replace(new RegExp('<scr'+'ipt\\\\b[\\\\s\\\\S]*?<\\\\/scr'+'ipt>','gi'),'')
|
|
1451
|
-
.replace(/<style\\b[\\s\\S]*?<\\/style>/gi,'')
|
|
1452
|
-
.replace(/<pre\\b[\\s\\S]*?<\\/pre>/gi,'')
|
|
1453
|
-
.replace(/<code\\b[\\s\\S]*?<\\/code>/gi,'')
|
|
1454
|
-
.replace(/<!--[\\s\\S]*?-->/g,'');
|
|
1455
|
-
}
|
|
1456
|
-
// Anchored on the whitespace before an attribute name and the = that follows,
|
|
1457
|
-
// so it reads attribute POSITIONS rather than characters. Bare ':' and '@' are
|
|
1458
|
-
// everywhere in ordinary content \u2014 'xlink:href', 'background: red', '10:00',
|
|
1459
|
-
// an email address \u2014 and none of those sit in that position.
|
|
1460
|
-
//
|
|
1461
|
-
// The x- arm is deliberately open rather than a list of known directives:
|
|
1462
|
-
// x-attr is a GENERIC binding prefix, so x-href / x-src / x-value / x-alt bind
|
|
1463
|
-
// exactly like x-text does. Measured over the 339 rendered docs pages, the
|
|
1464
|
-
// open form costs nothing over an allowlist (3 matches either way, all three
|
|
1465
|
-
// of them pages the CURRENT four-marker sniff already matches because they
|
|
1466
|
-
// document its marker names) while covering directives an allowlist forgets.
|
|
1467
|
-
var BARE_DIRECTIVE=/\\s(?::[a-z][\\w-]*|@[a-z][\\w.-]*|x-[a-z][\\w-]*)\\s*=\\s*["']/i;
|
|
1468
|
-
// The fragment marker doubles as the carrier for the server's declaration.
|
|
1469
|
-
// Encoding it in the HTML rather than threading a parameter keeps it inside
|
|
1470
|
-
// the one value that already reaches swap() through every path, the prefetch
|
|
1471
|
-
// cache included.
|
|
1472
|
-
function fragmentMarker(declared){
|
|
1473
|
-
return '<!--stx-fragment'+(declared==='true'?' rt=1':declared==='false'?' rt=0':'')+'-->';
|
|
1474
|
-
}
|
|
1475
|
-
// \u2500\u2500 External scripts inside the routed container \u2500\u2500
|
|
1476
|
-
//
|
|
1477
|
-
// A page whose x-data factory lives in its own file \u2014 a
|
|
1478
|
-
// <script src="dashboard-xdata.js"> sitting inside [data-stx-content] \u2014 lost that
|
|
1479
|
-
// file on every SPA navigation. Both swap paths dropped it: the fragment
|
|
1480
|
-
// path's regex sees an empty body and returns '', and the full-document path
|
|
1481
|
-
// removed it outright. The inline scripts beside it were re-executed with
|
|
1482
|
-
// care; the external ones were deleted.
|
|
1483
|
-
//
|
|
1484
|
-
// The result was a screen that rendered on a cold load and came back empty
|
|
1485
|
-
// after a click, reporting "dashboardXData is not defined" with every binding
|
|
1486
|
-
// in the container left unevaluated \u2014 because nothing had defined it since
|
|
1487
|
-
// the file was thrown away.
|
|
1488
|
-
//
|
|
1489
|
-
// Loaded once per URL, never re-run. What these files define is a global that
|
|
1490
|
-
// outlives the navigation, so a second execution buys nothing and costs
|
|
1491
|
-
// whatever side effects the file has: a mount helper firing again on every
|
|
1492
|
-
// click through the same page is its own bug.
|
|
1493
|
-
var loadedExternalScripts={};
|
|
1494
|
-
// The base is resolved first because callers pass what they have: the
|
|
1495
|
-
// fragment path knows only the path it navigated to ('/reports/disk'), and
|
|
1496
|
-
// new URL(src, '/reports/disk') throws \u2014 a base must be absolute. Silently
|
|
1497
|
-
// returning '' there meant the file was never requested, which is the bug
|
|
1498
|
-
// this whole block exists to fix, reintroduced one level down.
|
|
1499
|
-
function externalScriptKey(src,base){
|
|
1500
|
-
try{
|
|
1501
|
-
var absoluteBase=base?new URL(base,location.href).href:location.href;
|
|
1502
|
-
return new URL(src,absoluteBase).href;
|
|
1503
|
-
}catch(e){return ''}
|
|
1504
|
-
}
|
|
1505
|
-
function rememberExternalScript(src,base){
|
|
1506
|
-
var key=externalScriptKey(src,base);
|
|
1507
|
-
if(key)loadedExternalScripts[key]=1;
|
|
1508
|
-
return key;
|
|
1509
|
-
}
|
|
1510
|
-
// Returns a promise, or null when there is nothing to wait for \u2014 an already
|
|
1511
|
-
// loaded file, or an unparseable src. Resolves rather than rejects on error:
|
|
1512
|
-
// one 404 must not stop the rest of the page hydrating, matching what the
|
|
1513
|
-
// head-script path already does by sending failures to execScripts too.
|
|
1514
|
-
function loadExternalScript(src,base){
|
|
1515
|
-
var key=externalScriptKey(src,base);
|
|
1516
|
-
if(!key||loadedExternalScripts[key])return null;
|
|
1517
|
-
loadedExternalScripts[key]=1;
|
|
1518
|
-
return new Promise(function(resolve){
|
|
1519
|
-
var el=ce('script');
|
|
1520
|
-
el.src=key;
|
|
1521
|
-
// Deliberately NOT data-stx-page: both swap paths clear those on every
|
|
1522
|
-
// navigation, which would sweep this away and \u2014 since it is never loaded
|
|
1523
|
-
// twice \u2014 leave the document without it. These are loaded once and stay,
|
|
1524
|
-
// the same as the external head scripts beside them.
|
|
1525
|
-
el.setAttribute('data-stx-external','');
|
|
1526
|
-
// In document order: a fragment's composables bundle reads the module
|
|
1527
|
-
// registry loaded beside it, and an inserted script is async by default.
|
|
1528
|
-
el.async=false;
|
|
1529
|
-
el.onload=function(){resolve()};
|
|
1530
|
-
el.onerror=function(){log('[router] container script failed:',key);resolve()};
|
|
1531
|
-
dhead().appendChild(el);
|
|
1532
|
-
});
|
|
1533
|
-
}
|
|
1534
|
-
|
|
1535
|
-
// Record scripts from the initial page load
|
|
1536
|
-
qsa('script').forEach(function(s){
|
|
1537
|
-
var text=s.textContent||'';
|
|
1538
|
-
if(s.hasAttribute('src'))rememberExternalScript(s.getAttribute('src'));
|
|
1539
|
-
else if(text.trim())executedScriptHashes[hashScript(text)]=1;
|
|
1540
|
-
});
|
|
1541
|
-
function cacheKey(url){
|
|
1542
|
-
// Base is the CURRENT document, not the origin root: a relative key like
|
|
1543
|
-
// '?status=resolved' on /dashboard must cache as /dashboard?status=resolved,
|
|
1544
|
-
// not /?status=resolved (#1777).
|
|
1545
|
-
var u=new URL(url,location.href);
|
|
1546
|
-
return u.pathname+u.search;
|
|
1547
|
-
}
|
|
1548
|
-
// Keep SPA fetches inside the active locale (/en/... when viewing English).
|
|
1549
|
-
function withCurrentLocale(href){
|
|
1550
|
-
var d=window.__stxI18n;
|
|
1551
|
-
if(d&&typeof d.localizeHref==='function')return d.localizeHref(href);
|
|
1552
|
-
return href;
|
|
1553
|
-
}
|
|
1554
|
-
function defaultLayoutGroup(layout){
|
|
1555
|
-
if(!layout)return 'app';
|
|
1556
|
-
var clean=String(layout).replace(/\\\\/g,'/');
|
|
1557
|
-
var parts=clean.split('/').filter(Boolean);
|
|
1558
|
-
var idx=parts.lastIndexOf('layouts');
|
|
1559
|
-
var part=idx>=0?parts[idx+1]:parts[parts.length-1];
|
|
1560
|
-
return part?part.replace(/\\.stx$/i,''):'app';
|
|
1561
|
-
}
|
|
1562
|
-
function getCurrentLayoutGroup(){
|
|
1563
|
-
var meta=qs(LAYOUT_GROUP_META);
|
|
1564
|
-
if(meta&&meta.getAttribute('content'))return meta.getAttribute('content');
|
|
1565
|
-
var layout=qs(LAYOUT_META);
|
|
1566
|
-
return defaultLayoutGroup(layout?layout.getAttribute('content'):'');
|
|
1567
|
-
}
|
|
1568
|
-
function getDocLayoutGroup(doc,layout){
|
|
1569
|
-
var meta=doc&&doc.querySelector?doc.querySelector(LAYOUT_GROUP_META):null;
|
|
1570
|
-
return meta&&meta.getAttribute('content')?meta.getAttribute('content'):defaultLayoutGroup(layout||'');
|
|
1571
|
-
}
|
|
1572
|
-
function checkLayoutChange(newLayout,targetUrl,newGroup){
|
|
1573
|
-
var currentLayout=qs(LAYOUT_META);
|
|
1574
|
-
var curLayoutName=currentLayout?currentLayout.getAttribute('content'):'';
|
|
1575
|
-
var curGroup=getCurrentLayoutGroup();
|
|
1576
|
-
var nextGroup=newGroup||defaultLayoutGroup(newLayout);
|
|
1577
|
-
log('[router] layout check: current='+curLayoutName+'('+curGroup+') new='+(newLayout||'')+'('+nextGroup+')');
|
|
1578
|
-
// Different layout GROUP (e.g. app \u2192 auth): full reload
|
|
1579
|
-
if(curGroup!==nextGroup){
|
|
1580
|
-
log('[router] layout group change:',curGroup,'\u2192',nextGroup,'\u2014 full reload to:',targetUrl);
|
|
1581
|
-
return true;
|
|
1582
|
-
}
|
|
1583
|
-
// Same group but different SPECIFIC layout (e.g. layouts/app \u2192 layouts/coach):
|
|
1584
|
-
// full body swap so nav, sidebar, and other layout-level elements update
|
|
1585
|
-
if(curLayoutName && newLayout && curLayoutName!==newLayout){
|
|
1586
|
-
log('[router] layout name change:',curLayoutName,'\u2192',newLayout,'\u2014 full body swap');
|
|
1587
|
-
return true;
|
|
1588
|
-
}
|
|
1589
|
-
return false;
|
|
1590
|
-
}
|
|
1591
|
-
|
|
1592
|
-
function shouldUseFragmentResponse(){
|
|
1593
|
-
return containerSel==='main'||containerSel==='[data-stx-content]';
|
|
1594
|
-
}
|
|
1595
|
-
|
|
1596
|
-
// Write the address bar. The mode travels in the same slot as the legacy
|
|
1597
|
-
// pushState boolean: false still means "do not touch history", true still
|
|
1598
|
-
// pushes, and the string 'replace' replaces. Threading a fourth argument
|
|
1599
|
-
// through swap() to each of the three history sites would have been the
|
|
1600
|
-
// alternative (#1807).
|
|
1601
|
-
//
|
|
1602
|
-
// 'tab' is a tab's first screen: it takes the place of the entry the app
|
|
1603
|
-
// opened with (selectTab has rewound to it), under a token of its own so
|
|
1604
|
-
// the tab it replaced keeps its positions.
|
|
1605
|
-
var scrollSaved=false;
|
|
1606
|
-
function writeHistory(mode,href){
|
|
1607
|
-
// The position belongs to the entry being left, and scrollToken still
|
|
1608
|
-
// names it here -- unless the screen swap already saved it, before the
|
|
1609
|
-
// outgoing screen was lifted out of the page and the window clamped.
|
|
1610
|
-
if(!scrollSaved)rememberScroll();
|
|
1611
|
-
scrollSaved=false;
|
|
1612
|
-
if(mode==='replace'){
|
|
1613
|
-
// A replaced entry is still the one it replaces: pushed by the app or
|
|
1614
|
-
// the one the app opened with.
|
|
1615
|
-
var replaced=tokenState();
|
|
1616
|
-
if(history.state&&history.state[PUSHED_MARK])replaced[PUSHED_MARK]=true;
|
|
1617
|
-
history.replaceState(replaced,'',href);
|
|
1618
|
-
return;
|
|
1619
|
-
}
|
|
1620
|
-
scrollToken=newScrollToken();
|
|
1621
|
-
if(mode==='tab'){histDepth=0;history.replaceState(tokenState(),'',href);return}
|
|
1622
|
-
histDepth++;
|
|
1623
|
-
var pushed=tokenState();
|
|
1624
|
-
pushed[PUSHED_MARK]=true;
|
|
1625
|
-
history.pushState(pushed,'',href);
|
|
1626
|
-
}
|
|
1627
|
-
function tokenState(){var state={};state[SCROLL_TOKEN]=scrollToken;state[DEPTH]=histDepth;if(curTab)state[TAB]=curTab;return state}
|
|
1628
|
-
|
|
1629
|
-
// Second arg accepts the legacy pushState boolean OR an options object
|
|
1630
|
-
// { replace, instant }. Callers inside this file still pass the boolean.
|
|
1631
|
-
// instant is what data-stx-transition="none" is to a link: no cross-fade.
|
|
1632
|
-
//
|
|
1633
|
-
// The newest navigation wins. One already on its way when another starts
|
|
1634
|
-
// (a second tap, Back while a page is still loading) is aborted and its
|
|
1635
|
-
// answer ignored: it used to be the other way round, the later one was
|
|
1636
|
-
// dropped, and a Back during a load left the address bar on the popped
|
|
1637
|
-
// entry while the screen went on to show the page being left.
|
|
1638
|
-
var navSeq=0;
|
|
1639
|
-
var navAbort=null;
|
|
1640
|
-
// What the next swap needs to know about where it is going: set by the
|
|
1641
|
-
// caller right before navigate() (popstate, a tab switch) and carried to
|
|
1642
|
-
// swap() through pendingCtx, the same way pendingContainerAttrs is.
|
|
1643
|
-
var navCtxNext=null;
|
|
1644
|
-
var pendingCtx=null;
|
|
1645
|
-
// The page belongs to another origin: the server says so with
|
|
1646
|
-
// X-STX-Location (it cannot redirect a router fetch there, because a
|
|
1647
|
-
// cross-origin redirect of a request carrying X-STX-Router fails CORS), or
|
|
1648
|
-
// a plain fetch was redirected off-origin. Either way the browser goes
|
|
1649
|
-
// there itself; a fragment from another origin is never swapped in.
|
|
1650
|
-
function leaveOrigin(r){
|
|
1651
|
-
var away=r.headers.get('X-STX-Location');
|
|
1652
|
-
if(!away&&r.redirected&&r.url&&new URL(r.url,location.href).origin!==location.origin)away=r.url;
|
|
1653
|
-
if(!away)return false;
|
|
1654
|
-
location.href=away;
|
|
1655
|
-
return true;
|
|
1656
|
-
}
|
|
1657
|
-
function routerFetch(url,headers){
|
|
1658
|
-
var init={headers:headers};
|
|
1659
|
-
if(navAbort)init.signal=navAbort.signal;
|
|
1660
|
-
return fetch(url,init);
|
|
1661
|
-
}
|
|
1662
|
-
function navigate(url,pushState,force){
|
|
1663
|
-
var ctx=navCtxNext||{};
|
|
1664
|
-
navCtxNext=null;
|
|
1665
|
-
if(pushState&&typeof pushState==='object'){if(pushState.instant)instantNext=true;pushState=pushState.replace?'replace':true}
|
|
1666
|
-
// Lang-picker passes force=true with an already-localized path (/en/...).
|
|
1667
|
-
// Re-localizing would map it back to the *current* locale and no-op.
|
|
1668
|
-
if(!force) url=withCurrentLocale(url);
|
|
1669
|
-
log('[router] navigate() called:',url,'isNavigating:',isNavigating);
|
|
1670
|
-
// Resolve against the current document URL, matching native <a> semantics.
|
|
1671
|
-
// With location.origin as the base, any relative href ('?status=resolved',
|
|
1672
|
-
// '#anchor', './sibling') resolved to the SITE ROOT \u2014 so filter tabs,
|
|
1673
|
-
// pagination and sort links silently bounced users to the landing page once
|
|
1674
|
-
// the SPA interceptor was active (#1777).
|
|
1675
|
-
var t=new URL(url,location.href);
|
|
1676
|
-
|
|
1677
|
-
if(t.origin!==location.origin){location.href=url;return Promise.resolve(false)}
|
|
1678
|
-
|
|
1679
|
-
if(t.pathname===location.pathname&&t.hash&&pushState!=='tab'){
|
|
1680
|
-
if(pushState!==false)writeHistory(pushState,t.href);
|
|
1681
|
-
var el=findIn(t.hash);
|
|
1682
|
-
if(el)el.scrollIntoView({behavior:'smooth'});
|
|
1683
|
-
return Promise.resolve(true);
|
|
1684
|
-
}
|
|
1685
|
-
|
|
1686
|
-
// Skip if user clicks a link to the page they're already on. We
|
|
1687
|
-
// deliberately allow popstate (pushState===false) through because
|
|
1688
|
-
// the browser has already updated location.href to the popped
|
|
1689
|
-
// entry \u2014 without this allowance, hitting back would early-return
|
|
1690
|
-
// and the visible page content would be left frozen on the
|
|
1691
|
-
// forward-navigation page.
|
|
1692
|
-
if(pushState!==false&&pushState!=='tab'&&t.href===location.href&&!t.hash&&!force)return Promise.resolve(false);
|
|
1693
|
-
|
|
1694
|
-
// Its prefetch is on the way: wait for that answer rather than asking the
|
|
1695
|
-
// server a second time. A later tap elsewhere wins.
|
|
1696
|
-
var awaitedKey=cacheKey(url);
|
|
1697
|
-
if(o.cache&&!force&&!cache[awaitedKey]&&prefetching[awaitedKey]&&prefetching[awaitedKey].then){
|
|
1698
|
-
awaitingKey=awaitedKey;
|
|
1699
|
-
return prefetching[awaitedKey].then(function(){
|
|
1700
|
-
if(awaitingKey!==awaitedKey)return false;
|
|
1701
|
-
awaitingKey='';
|
|
1702
|
-
navCtxNext=ctx;
|
|
1703
|
-
return navigate(url,pushState,force);
|
|
1704
|
-
});
|
|
1705
|
-
}
|
|
1706
|
-
awaitingKey='';
|
|
1707
|
-
|
|
1708
|
-
if(navAbort){try{navAbort.abort()}catch(e){}}
|
|
1709
|
-
navAbort=typeof AbortController==='function'?new AbortController():null;
|
|
1710
|
-
var id=++navSeq;
|
|
1711
|
-
ctx.id=id;
|
|
1712
|
-
if(!ctx.direction)ctx.direction=pushState==='replace'?'replace':pushState==='tab'?'tab':pushState===false?'pop':'push';
|
|
1713
|
-
isNavigating=true;
|
|
1714
|
-
instantNav=instantNext;
|
|
1715
|
-
instantNext=false;
|
|
1716
|
-
document.body.classList.add(o.loadingClass);
|
|
1717
|
-
startProgress();
|
|
1718
|
-
|
|
1719
|
-
var targetHref=t.href;
|
|
1720
|
-
var targetPath=cacheKey(url);
|
|
1721
|
-
var targetHash=t.hash;
|
|
1722
|
-
|
|
1723
|
-
function done(){
|
|
1724
|
-
if(id!==navSeq)return;
|
|
1725
|
-
isNavigating=false;navAbort=null;
|
|
1726
|
-
document.body.classList.remove(o.loadingClass);finishProgress();
|
|
1727
|
-
settleDirection();
|
|
1728
|
-
}
|
|
1729
|
-
function stale(){return id!==navSeq}
|
|
1730
|
-
// A failed fetch keeps the app: the destination shows a way to try again
|
|
1731
|
-
// in place of its content, where a document load used to be the answer
|
|
1732
|
-
// to everything -- offline that load is the offline page, or nothing.
|
|
1733
|
-
function failed(err){
|
|
1734
|
-
if(stale()||(err&&err.name==='AbortError'))return false;
|
|
1735
|
-
console.error('[router] fetch error:',err);
|
|
1736
|
-
return showNavError(url,err,pushState,targetHash,ctx);
|
|
1737
|
-
}
|
|
1738
|
-
function swapWith(html,key){pendingCtx=ctx;return swap(html,key,pushState,targetHash)}
|
|
1739
|
-
|
|
1740
|
-
if(o.cache&&cache[targetPath]&&!force){
|
|
1741
|
-
// Cache hit \u2192 promote in LRU order so this entry survives eviction
|
|
1742
|
-
// longer than other entries that haven't been touched.
|
|
1743
|
-
touchCache(targetPath);
|
|
1744
|
-
if(checkLayoutChange(layoutCache[targetPath],url,layoutGroupCache[targetPath])){
|
|
1745
|
-
// Layout changed \u2014 fetch full page and do full document swap
|
|
1746
|
-
log('[router] cache hit but layout changed \u2014 fetching full page');
|
|
1747
|
-
return routerFetch(url,{'Accept':'text/html'}).then(function(r){
|
|
1748
|
-
if(leaveOrigin(r))return null;
|
|
1749
|
-
if(!r.ok)throw httpError(r.status);
|
|
1750
|
-
return r.text();
|
|
1751
|
-
}).then(function(html){
|
|
1752
|
-
if(html===null||stale())return false;
|
|
1753
|
-
log('[router] full page fetched from cache path, len:',html.length);
|
|
1754
|
-
pendingLayoutDecl={layout:layoutCache[targetPath]||'',group:layoutGroupCache[targetPath]||''};
|
|
1755
|
-
return swapWith(html,targetPath);
|
|
1756
|
-
}).catch(failed).finally(done);
|
|
1757
|
-
}
|
|
1758
|
-
pendingContainerAttrs=attrsCache[targetPath]||'';
|
|
1759
|
-
pendingLayoutDecl=null;
|
|
1760
|
-
return Promise.resolve(swapWith(cache[targetPath],targetPath)).then(function(swapped){
|
|
1761
|
-
if(swapped===false)return false;
|
|
1762
|
-
applyTitle(titleCache[targetPath]);
|
|
1763
|
-
// Shown from the cache at once; asked again behind it, so a page that
|
|
1764
|
-
// changed since says so (stx:updated) instead of staying stale.
|
|
1765
|
-
revalidate(targetPath,url);
|
|
1766
|
-
return true;
|
|
1767
|
-
}).finally(done);
|
|
1768
|
-
}
|
|
1769
|
-
else {
|
|
1770
|
-
if(force&&cache[targetPath])evictCache(targetPath);
|
|
1771
|
-
// First fetch as fragment (SPA mode) when the configured container is
|
|
1772
|
-
// the page content. Custom app-shell containers need full documents so
|
|
1773
|
-
// the router does not inject a <main> fragment into the wrong element.
|
|
1774
|
-
var wantsFragment=shouldUseFragmentResponse();
|
|
1775
|
-
return routerFetch(url,wantsFragment?{'X-STX-Router':'true','Accept':'text/html'}:{'Accept':'text/html'}).then(function(r){
|
|
1776
|
-
if(stale())return null;
|
|
1777
|
-
if(leaveOrigin(r))return null;
|
|
1778
|
-
if(!r.ok)throw httpError(r.status);
|
|
1779
|
-
// A route guard answered with a redirect and fetch followed it
|
|
1780
|
-
// transparently, so r.ok is the DESTINATION's 200 and this markup
|
|
1781
|
-
// belongs to r.url \u2014 not to the path that was requested. Without
|
|
1782
|
-
// retargeting, an auth guard's /login body is swapped in while the
|
|
1783
|
-
// address bar, the history entry and the cache key all still read
|
|
1784
|
-
// /dashboard: the user sees a login form at a URL that claims to be
|
|
1785
|
-
// the page they asked for, and reloading bounces them again (#1849).
|
|
1786
|
-
if(r.redirected&&r.url){
|
|
1787
|
-
var rd=new URL(r.url,location.href);
|
|
1788
|
-
// A guard that sends you off-origin (a hosted IdP) cannot be
|
|
1789
|
-
// resolved by swapping a fragment \u2014 hand it to the browser.
|
|
1790
|
-
if(rd.origin!==location.origin){location.href=r.url;return null}
|
|
1791
|
-
log('[router] guard redirected:',url,'->',rd.pathname);
|
|
1792
|
-
url=rd.pathname+rd.search+rd.hash;
|
|
1793
|
-
targetPath=cacheKey(url);
|
|
1794
|
-
targetHash=rd.hash;
|
|
1795
|
-
}
|
|
1796
|
-
// Before anything is parsed or swapped: a fragment from a different
|
|
1797
|
-
// build must not be handed to this page's runtime (#1772).
|
|
1798
|
-
var incomingBuild=r.headers.get('X-STX-Build')||'';
|
|
1799
|
-
if(isBuildSkew(incomingBuild)){reloadForSkew(url,incomingBuild);return null}
|
|
1800
|
-
var isFragment=wantsFragment&&r.headers.get('X-STX-Fragment')==='true';
|
|
1801
|
-
var newLayout=r.headers.get('X-STX-Layout')||'';
|
|
1802
|
-
var newGroup=r.headers.get('X-STX-Layout-Group')||'';
|
|
1803
|
-
var newTitle=r.headers.get('X-STX-Title')||'';
|
|
1804
|
-
var newCAttrs=r.headers.get('X-STX-Container-Attrs')||'';
|
|
1805
|
-
var newRuntime=r.headers.get('X-STX-Runtime')||'';
|
|
1806
|
-
// Layout change? Fetch the FULL page (no X-STX-Router header) and do full document swap
|
|
1807
|
-
if(isFragment&&checkLayoutChange(newLayout,url,newGroup)){
|
|
1808
|
-
log('[router] layout change \u2014 fetching full page for document swap');
|
|
1809
|
-
return routerFetch(url,{'Accept':'text/html'}).then(function(fullRes){
|
|
1810
|
-
log('[router] full page fetched:',fullRes.status,'ok:',fullRes.ok);
|
|
1811
|
-
if(leaveOrigin(fullRes))return null;
|
|
1812
|
-
if(!fullRes.ok)throw httpError(fullRes.status);
|
|
1813
|
-
return fullRes.text().then(function(html){
|
|
1814
|
-
log('[router] full page html length:',html.length);
|
|
1815
|
-
return{html:html,isFragment:false,layout:newLayout,layoutGroup:newGroup,title:newTitle};
|
|
1816
|
-
});
|
|
1817
|
-
});
|
|
1818
|
-
}
|
|
1819
|
-
return r.text().then(function(html){return{html:html,isFragment:isFragment,layout:newLayout,layoutGroup:newGroup,title:newTitle,containerAttrs:newCAttrs,runtime:newRuntime}});
|
|
1820
|
-
}).then(function(result){
|
|
1821
|
-
if(!result||stale())return false;
|
|
1822
|
-
if(result.isFragment)result.html=fragmentMarker(result.runtime)+result.html;
|
|
1823
|
-
if(o.cache)setCache(targetPath,result.html,result.layout,result.layoutGroup,result.title,result.containerAttrs);
|
|
1824
|
-
pendingContainerAttrs=result.isFragment?(result.containerAttrs||''):'';
|
|
1825
|
-
pendingLayoutDecl=result.isFragment?null:{layout:result.layout||'',group:result.layoutGroup||''};
|
|
1826
|
-
return Promise.resolve(swapWith(result.html,targetPath)).then(function(swapped){
|
|
1827
|
-
if(swapped===false)return false;
|
|
1828
|
-
// Fragment swaps carry no <head>; apply the title from the header.
|
|
1829
|
-
// Full-document swaps already set document.title from the <title> tag.
|
|
1830
|
-
if(result.isFragment)applyTitle(result.title);
|
|
1831
|
-
return true;
|
|
1832
|
-
});
|
|
1833
|
-
}).catch(failed).finally(done);
|
|
1834
|
-
}
|
|
1835
|
-
}
|
|
1836
|
-
function httpError(status){var err=new Error(String(status));err.status=status;return err}
|
|
1837
|
-
|
|
1838
|
-
// \u2500\u2500 A screen that could not be loaded \u2500\u2500
|
|
1839
|
-
// Shown in the routed container (in a screen of its own when screens are
|
|
1840
|
-
// retained, so Back returns to the one before it), with the address bar at
|
|
1841
|
-
// the destination, so "Try again" and coming back online both retry it in
|
|
1842
|
-
// place. A page that wants to draw its own cancels stx:navigate-error.
|
|
1843
|
-
function escAttr(v){return String(v).replace(/&/g,'&').replace(/"/g,'"').replace(/</g,'<')}
|
|
1844
|
-
function showNavError(url,err,pushState,hash,ctx){
|
|
1845
|
-
var status=err&&err.status||0;
|
|
1846
|
-
var offline=!status&&navigator.onLine===false;
|
|
1847
|
-
var ev=new CustomEvent('stx:navigate-error',{cancelable:true,detail:{url:url,status:status,offline:offline,error:err}});
|
|
1848
|
-
window.dispatchEvent(ev);
|
|
1849
|
-
if(ev.defaultPrevented)return false;
|
|
1850
|
-
var title=offline?'You are offline':'This page could not be loaded';
|
|
1851
|
-
var text=offline?'It will load as soon as you are back online.':status?'Something went wrong on the way. Try again in a moment.':'Check your connection and try again.';
|
|
1852
|
-
var html='<!--stx-fragment rt=0--><div class="stx-retry" role="alert" data-stx-retry="'+escAttr(url)+'"><p class="stx-retry-title">'+title+'</p><p class="stx-retry-text">'+text+'</p><button type="button" class="stx-retry-button" data-stx-retry-button>Try again</button></div>';
|
|
1853
|
-
pendingContainerAttrs='';
|
|
1854
|
-
pendingLayoutDecl=null;
|
|
1855
|
-
pendingCtx=ctx;
|
|
1856
|
-
return Promise.resolve(swap(html,cacheKey(url),pushState,hash)).then(function(){return false});
|
|
1857
|
-
}
|
|
1858
|
-
function retryShown(){var root=activeRoot();var el=root&&root.querySelector?root.querySelector('[data-stx-retry]'):null;return el?el.getAttribute('data-stx-retry'):''}
|
|
1859
|
-
document.addEventListener('click',function(e){
|
|
1860
|
-
var b=e.target&&e.target.closest?e.target.closest('[data-stx-retry-button]'):null;
|
|
1861
|
-
if(!b)return;
|
|
1862
|
-
var holder=b.closest('[data-stx-retry]');
|
|
1863
|
-
if(holder)navigate(holder.getAttribute('data-stx-retry'),'replace',true);
|
|
1864
|
-
});
|
|
1865
|
-
window.addEventListener('online',function(){var u=retryShown();if(u)navigate(u,'replace',true)});
|
|
1866
|
-
|
|
1867
|
-
// A full document is only safe to partial-swap into the stx shell if it is
|
|
1868
|
-
// itself stx-rendered. Pages served by another engine mounted on the same
|
|
1869
|
-
// origin (e.g. a docs/blog generator) carry none of these markers; swapping
|
|
1870
|
-
// their <main> + styles into the stx shell corrupts the layout. Detect them
|
|
1871
|
-
// and hand off to a native full navigation instead.
|
|
1872
|
-
function isStxDocument(html){
|
|
1873
|
-
return html.indexOf('name="stx-layout"')!==-1
|
|
1874
|
-
||html.indexOf('__stxRouterConfig')!==-1
|
|
1875
|
-
||html.indexOf('data-stx-content')!==-1;
|
|
1876
|
-
}
|
|
1877
|
-
|
|
1878
|
-
// The layout and group the SERVER declared for the document about to be
|
|
1879
|
-
// swapped in. Set immediately before each swap() call by whichever path
|
|
1880
|
-
// fetched it, and consumed once.
|
|
1881
|
-
//
|
|
1882
|
-
// Threaded this way rather than as a swap() parameter, matching
|
|
1883
|
-
// pendingContainerAttrs \u2014 see the note above writeHistory about the cost of
|
|
1884
|
-
// adding an argument to swap() and its three call sites (#1807).
|
|
1885
|
-
var pendingLayoutDecl=null;
|
|
1886
|
-
// \u2500\u2500 Focus and route announcement \u2500\u2500
|
|
1887
|
-
// A fragment swap replaces content without a document load, so the browser
|
|
1888
|
-
// does none of what it normally does for a navigation. Focus stays on a link
|
|
1889
|
-
// that no longer exists, which the browser resets to <body> \u2014 a keyboard user
|
|
1890
|
-
// is silently returned to the top of the tab order \u2014 and a screen reader says
|
|
1891
|
-
// nothing at all, so there is no signal the page changed. Neither is fixable
|
|
1892
|
-
// from app code, because only the router knows a navigation happened (#1862).
|
|
1893
|
-
function routeAnnouncer(){
|
|
1894
|
-
var el=document.getElementById('stx-route-announcer');
|
|
1895
|
-
if(el)return el;
|
|
1896
|
-
el=ce('div');
|
|
1897
|
-
el.id='stx-route-announcer';
|
|
1898
|
-
el.setAttribute('aria-live','polite');
|
|
1899
|
-
el.setAttribute('role','status');
|
|
1900
|
-
el.setAttribute('aria-atomic','true');
|
|
1901
|
-
// Visually hidden but still ANNOUNCED. display:none and visibility:hidden
|
|
1902
|
-
// are both skipped by screen readers, which would make this a silent no-op
|
|
1903
|
-
// that looks correct in the DOM.
|
|
1904
|
-
el.style.cssText='position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0 0 0 0);clip-path:inset(50%);white-space:nowrap;border:0';
|
|
1905
|
-
document.body.appendChild(el);
|
|
1906
|
-
return el;
|
|
1907
|
-
}
|
|
1908
|
-
|
|
1909
|
-
function announceRoute(){
|
|
1910
|
-
if(o.announceRoute===false)return;
|
|
1911
|
-
var c=activeRoot();
|
|
1912
|
-
var h=c&&c.querySelector?c.querySelector('h1'):null;
|
|
1913
|
-
// The heading names the destination better than the title, which often
|
|
1914
|
-
// carries a site-name suffix. Falls back to the title, then gives up
|
|
1915
|
-
// rather than announcing an empty string.
|
|
1916
|
-
var label=(h&&(h.textContent||'').trim())||document.title||'';
|
|
1917
|
-
if(!label)return;
|
|
1918
|
-
var el=routeAnnouncer();
|
|
1919
|
-
// Cleared first: several screen readers ignore a live-region update whose
|
|
1920
|
-
// text is identical to what is already there, so navigating between two
|
|
1921
|
-
// pages with the same heading would announce only the first.
|
|
1922
|
-
el.textContent='';
|
|
1923
|
-
el.textContent=label;
|
|
1924
|
-
}
|
|
1925
|
-
|
|
1926
|
-
function focusAfterNavigation(hash){
|
|
1927
|
-
if(o.routeFocus===false)return;
|
|
1928
|
-
var target=null;
|
|
1929
|
-
if(hash){try{target=qs(hash)}catch(e){target=null}}
|
|
1930
|
-
if(!target)target=getContainer();
|
|
1931
|
-
if(!target||!target.focus)return;
|
|
1932
|
-
// The container is not naturally focusable. tabindex="-1" makes it
|
|
1933
|
-
// programmatically focusable without inserting it into the tab order.
|
|
1934
|
-
if(!target.hasAttribute('tabindex')){target.setAttribute('tabindex','-1');target.setAttribute('data-stx-route-focus','')}
|
|
1935
|
-
// preventScroll because scroll position is already decided just above \u2014
|
|
1936
|
-
// focusing would otherwise fight scrollToTop and the hash scrollIntoView.
|
|
1937
|
-
try{target.focus({preventScroll:true})}catch(e){try{target.focus()}catch(e2){}}
|
|
1938
|
-
}
|
|
1939
|
-
|
|
1940
|
-
// \u2500\u2500 Generated utility stylesheets \u2500\u2500
|
|
1941
|
-
// Every page ships its own content-addressed utility sheet as
|
|
1942
|
-
// <link data-css="generated">, and none of them is ever removed on a
|
|
1943
|
-
// navigation, so they pile up in <head>. Two of them can define the same
|
|
1944
|
-
// class at the same specificity, which makes their ORDER decide the cascade:
|
|
1945
|
-
// the later sheet wins. Deduping alone left that order as "first seen", so
|
|
1946
|
-
// after Records \u2192 Feed \u2192 Records the Feed sheet still sat after the Records
|
|
1947
|
-
// one and its .flex-col beat Records' own responsive sm:flex-row \u2014 the header
|
|
1948
|
-
// stacked into a column on desktop. The current page's complete sheet has to
|
|
1949
|
-
// be the last one, every navigation, not just the first time it is seen.
|
|
1950
|
-
function findGeneratedCss(abs){
|
|
1951
|
-
var found=null;
|
|
1952
|
-
qsa('head link[data-css][href]').forEach(function(link){
|
|
1953
|
-
if(new URL(link.getAttribute('href'),location.href).href!==abs)return;
|
|
1954
|
-
// Collapse duplicates, so moving one copy to the end can never leave an
|
|
1955
|
-
// older copy of the same sheet behind to fight it.
|
|
1956
|
-
if(found){link.parentNode.removeChild(link);return}
|
|
1957
|
-
found=link;
|
|
1958
|
-
});
|
|
1959
|
-
return found;
|
|
1960
|
-
}
|
|
1961
|
-
|
|
1962
|
-
// Before the swap: add each sheet the page does not have yet, as
|
|
1963
|
-
// media="print" so it downloads without restyling the page still on screen.
|
|
1964
|
-
// Returns a promise for them to finish (load OR error, capped at
|
|
1965
|
-
// cssLoadTimeout so a slow sheet never stalls navigation), or null when
|
|
1966
|
-
// nothing new is needed and the swap can go ahead in the same turn. The
|
|
1967
|
-
// promise resolves false when a sheet failed outright, and the caller then
|
|
1968
|
-
// navigates in full rather than swap in markup that has no styles.
|
|
1969
|
-
function preloadGeneratedCss(hrefs,base){
|
|
1970
|
-
var pending=[];
|
|
1971
|
-
hrefs.forEach(function(href){
|
|
1972
|
-
var abs;
|
|
1973
|
-
try{abs=new URL(href,base).href}catch(e){return}
|
|
1974
|
-
var link=findGeneratedCss(abs);
|
|
1975
|
-
if(link){
|
|
1976
|
-
// Still downloading for a navigation that never swapped \u2014 wait on it
|
|
1977
|
-
// rather than on nothing.
|
|
1978
|
-
if(link.__stxCssReady)pending.push(link.__stxCssReady);
|
|
1979
|
-
return;
|
|
1980
|
-
}
|
|
1981
|
-
link=ce('link');
|
|
1982
|
-
link.setAttribute('data-css','generated');
|
|
1983
|
-
link.setAttribute('rel','stylesheet');
|
|
1984
|
-
link.setAttribute('href',new URL(href,location.href).href===abs?href:abs);
|
|
1985
|
-
link.setAttribute('media','print');
|
|
1986
|
-
link.setAttribute('data-stx-css-pending','');
|
|
1987
|
-
link.__stxCssReady=new Promise(function(resolve){
|
|
1988
|
-
var timer=setTimeout(finish,o.cssLoadTimeout);
|
|
1989
|
-
function finish(failed){
|
|
1990
|
-
clearTimeout(timer);link.onload=null;link.onerror=null;link.__stxCssReady=null;
|
|
1991
|
-
// A sheet that failed (a 5xx, a dropped connection) is gone for good:
|
|
1992
|
-
// swapping anyway painted the page with only the utilities the
|
|
1993
|
-
// previous page happened to share, which reads as a broken layout.
|
|
1994
|
-
// Drop the dead <link> so a retry fetches it again, and report it.
|
|
1995
|
-
if(failed===true&&link.parentNode)link.parentNode.removeChild(link);
|
|
1996
|
-
resolve(failed!==true);
|
|
1997
|
-
}
|
|
1998
|
-
link.onload=function(){finish(false)};
|
|
1999
|
-
link.onerror=function(){finish(true)};
|
|
2000
|
-
});
|
|
2001
|
-
pending.push(link.__stxCssReady);
|
|
2002
|
-
dhead().appendChild(link);
|
|
2003
|
-
});
|
|
2004
|
-
return pending.length?Promise.all(pending).then(function(results){return results.every(function(ok){return ok!==false})}):null;
|
|
2005
|
-
}
|
|
2006
|
-
|
|
2007
|
-
// Swap once the destination's sheets are in, or hand the navigation to the
|
|
2008
|
-
// browser when one could not be fetched \u2014 a full load retries the sheet.
|
|
2009
|
-
function afterCss(ready,url,swapNow){
|
|
2010
|
-
if(!ready)return swapNow();
|
|
2011
|
-
return ready.then(function(ok){
|
|
2012
|
-
if(ok)return swapNow();
|
|
2013
|
-
log('[router] stylesheet failed to load \u2014 full navigation to:',url);
|
|
2014
|
-
location.href=url;
|
|
2015
|
-
return false;
|
|
2016
|
-
});
|
|
2017
|
-
}
|
|
2018
|
-
|
|
2019
|
-
// At the swap: make the destination's sheets live and move them to the end
|
|
2020
|
-
// of <head>. appendChild on a node that is already there MOVES it \u2014 no
|
|
2021
|
-
// duplicate, and the browser keeps the parsed sheet rather than refetching.
|
|
2022
|
-
// Only generated sheets are reordered; app and font stylesheets keep the
|
|
2023
|
-
// order they were declared in.
|
|
2024
|
-
function promoteGeneratedCss(hrefs,base){
|
|
2025
|
-
hrefs.forEach(function(href){
|
|
2026
|
-
var abs;
|
|
2027
|
-
try{abs=new URL(href,base).href}catch(e){return}
|
|
2028
|
-
var link=findGeneratedCss(abs);
|
|
2029
|
-
if(!link){
|
|
2030
|
-
preloadGeneratedCss([href],base);
|
|
2031
|
-
link=findGeneratedCss(abs);
|
|
2032
|
-
if(!link)return;
|
|
2033
|
-
}
|
|
2034
|
-
if(link.hasAttribute('data-stx-css-pending')){
|
|
2035
|
-
link.removeAttribute('data-stx-css-pending');
|
|
2036
|
-
link.removeAttribute('media');
|
|
2037
|
-
}
|
|
2038
|
-
dhead().appendChild(link);
|
|
2039
|
-
});
|
|
2040
|
-
}
|
|
2041
|
-
|
|
2042
|
-
// The generated-sheet hrefs a fragment carries. Scripts are skipped so a
|
|
2043
|
-
// <link> written inside a string in page code is not mistaken for one.
|
|
2044
|
-
function fragmentCssHrefs(html){
|
|
2045
|
-
var hrefs=[];
|
|
2046
|
-
html.replace(new RegExp('<scr'+'ipt\\\\b[^>]*>[\\\\s\\\\S]*?<\\\\/scr'+'ipt>','gi'),'').replace(new RegExp('<link\\\\b([^>]*)>','gi'),function(m,attrs){
|
|
2047
|
-
if(attrs.indexOf('data-css')===-1)return m;
|
|
2048
|
-
var hrefMatch=attrs.match(/\\bhref=(["'])(.*?)\\1/i);
|
|
2049
|
-
if(hrefMatch&&hrefMatch[2])hrefs.push(hrefMatch[2]);
|
|
2050
|
-
return m;
|
|
2051
|
-
});
|
|
2052
|
-
return hrefs;
|
|
2053
|
-
}
|
|
2054
|
-
|
|
2055
|
-
function swap(html,url,pushState,hash){
|
|
2056
|
-
var fragMark=/^<!--stx-fragment(?: rt=([01]))?-->/.exec(html);
|
|
2057
|
-
var isFragment=!!fragMark;
|
|
2058
|
-
var declaredRuntime=fragMark&&fragMark[1]?fragMark[1]:'';
|
|
2059
|
-
if(isFragment)html=html.slice(fragMark[0].length);
|
|
2060
|
-
if(!isFragment&&!isStxDocument(html)){log('[router] non-stx document \u2014 full navigation to:',url);location.href=url;return Promise.resolve(false)}
|
|
2061
|
-
var ctx=pendingCtx||{direction:pushState==='replace'?'replace':pushState==='tab'?'tab':pushState===false?'pop':'push'};
|
|
2062
|
-
pendingCtx=null;
|
|
2063
|
-
var currentContent=getContainer();
|
|
2064
|
-
// The routed container. currentContent is where the page is written: the
|
|
2065
|
-
// container itself, or with screens retained, the new screen inside it.
|
|
2066
|
-
var host=currentContent;
|
|
2067
|
-
log('[router] swap: isFragment='+isFragment+' container='+!!currentContent+' tag='+(currentContent&¤tContent.tagName)+' selector='+containerSel+' htmlLen='+html.length);
|
|
2068
|
-
if(!currentContent){log('[router] no container \u2014 falling back');location.href=url;return Promise.resolve(false)}
|
|
2069
|
-
|
|
2070
|
-
// Fragment mode: server returned just the page content (no document wrapper)
|
|
2071
|
-
if(isFragment){
|
|
2072
|
-
// Before _cleanupContainer, not after: handing off later would dispose
|
|
2073
|
-
// the OUTGOING page's scopes and then reload anyway. Inside doFragSwap
|
|
2074
|
-
// would be later still \u2014 innerHTML is already replaced by then.
|
|
2075
|
-
if(!window.stx&&fragmentNeedsRuntime(html,declaredRuntime)){
|
|
2076
|
-
log('[router] fragment needs the signals runtime and this page has none \u2014 full navigation to:',url);
|
|
2077
|
-
location.href=url;
|
|
2078
|
-
return Promise.resolve(false);
|
|
2079
|
-
}
|
|
2080
|
-
function doFragSwap(){
|
|
2081
|
-
hydrateServerData(html);
|
|
2082
|
-
// Extract scripts from fragment before injecting HTML
|
|
2083
|
-
var fragScripts=[];
|
|
2084
|
-
var fragScriptId=0;
|
|
2085
|
-
var fragStyles=[];
|
|
2086
|
-
var fragCssHrefs=[];
|
|
2087
|
-
var fragCss=null;
|
|
2088
|
-
var fragExternalScripts=[];
|
|
2089
|
-
var cleanFrag=html.replace(new RegExp('<scr'+'ipt\\\\b([^>]*)>([\\\\s\\\\S]*?)<\\\\/scr'+'ipt>','gi'),function(m,attrs,code){
|
|
2090
|
-
// A data block is not code, and everything below this line assumes it
|
|
2091
|
-
// is: the body gets stashed in fragScripts, re-emitted as a pending
|
|
2092
|
-
// placeholder, then wrapped in braces and executed. Wrapping a JSON
|
|
2093
|
-
// object in braces reparses it as a labelled block, so its first ':'
|
|
2094
|
-
// is a SyntaxError on every single fragment swap
|
|
2095
|
-
// (stacksjs/stx#1801). @structuredData renders at its call site,
|
|
2096
|
-
// which inside a layout is @section('content') \u2014 i.e. in the routed
|
|
2097
|
-
// container \u2014 so the natural way to write it hit this.
|
|
2098
|
-
//
|
|
2099
|
-
// Left exactly as found rather than stripped: crawlers read
|
|
2100
|
-
// structured data wherever it sits, and the browser will not execute
|
|
2101
|
-
// a non-JS type anyway. Mirrors prepareRoutedBodyScripts' notion of
|
|
2102
|
-
// executable, which this path never had.
|
|
2103
|
-
var typeMatch=attrs.match(/\\btype\\s*=\\s*(?:"([^"]*)"|'([^']*)'|([^\\s>]+))/i);
|
|
2104
|
-
var scriptType=((typeMatch&&(typeMatch[1]||typeMatch[2]||typeMatch[3]))||'').trim().toLowerCase();
|
|
2105
|
-
if(scriptType&&scriptType!=='text/javascript'&&scriptType!=='application/javascript'&&scriptType!=='module')return m;
|
|
2106
|
-
// An external script has no body, so every check below reads it as
|
|
2107
|
-
// nothing to run and the fragment loses it. Queue it instead; the
|
|
2108
|
-
// file is where the page's factory usually lives.
|
|
2109
|
-
var srcMatch=attrs.match(/\\bsrc\\s*=\\s*(?:"([^"]*)"|'([^']*)'|([^\\s>]+))/i);
|
|
2110
|
-
var fragSrc=srcMatch&&(srcMatch[1]||srcMatch[2]||srcMatch[3]);
|
|
2111
|
-
if(fragSrc){if(!isSharedClientScriptAttrs(attrs))fragExternalScripts.push(fragSrc);return ''}
|
|
2112
|
-
if(code&&code.trim()&&!isSignalsRuntimeScript({hasAttribute:function(name){return name==='data-stx-runtime'&&attrs.indexOf('data-stx-runtime')!==-1}},code)){
|
|
2113
|
-
var slot='fragment-'+(++fragScriptId);
|
|
2114
|
-
var scoped=/(?:^|\\s)data-stx-scoped(?:\\s|=|$)/i.test(attrs);
|
|
2115
|
-
var runDecl=(attrs.match(/data-stx-run\\s*=\\s*["']?(always|once)["']?/i)||[])[1];
|
|
2116
|
-
var ownerDecl=(attrs.match(/data-stx-owner\\s*=\\s*["']([^"']+)["']/i)||[])[1]||'';
|
|
2117
|
-
var registry=/(?:^|\\s)data-stx-modules(?:\\s|=|$)/i.test(attrs)||REGISTRY_TEXT.test(code);
|
|
2118
|
-
var composables=/(?:^|\\s)data-stx-composables(?:\\s|=|$)/i.test(attrs)||COMPOSABLES_TEXT.test(code);
|
|
2119
|
-
fragScripts.push({text:code,slot:slot,setupName:generatedSetupName(code),scoped:scoped,run:runDecl?runDecl.toLowerCase():'',owner:ownerDecl,registry:registry,composables:composables});
|
|
2120
|
-
// Retain scoped setup code in its inert placeholder. A placeholder
|
|
2121
|
-
// inside template.content is unreachable through document, so the
|
|
2122
|
-
// repeated component runtime must execute it for each clone.
|
|
2123
|
-
return '<scr'+'ipt type="application/stx-pending" data-stx-route-script="'+slot+'"'
|
|
2124
|
-
+(scoped?' data-stx-scoped':'')
|
|
2125
|
-
+(runDecl?' data-stx-run="'+runDecl.toLowerCase()+'"':'')
|
|
2126
|
-
+'>'+code+'<\\/scr'+'ipt>';
|
|
2127
|
-
}
|
|
2128
|
-
return '';
|
|
2129
|
-
});
|
|
2130
|
-
// A <style> inside a script is a string the script owns \u2014 a web
|
|
2131
|
-
// component's shadow-DOM CSS, typically \u2014 not a page style. Lifted into
|
|
2132
|
-
// <head> it restyles the whole document (its bare button rule shrank
|
|
2133
|
-
// every button on the page) and the component loses it. Hold scripts
|
|
2134
|
-
// aside while the fragment's own styles are collected.
|
|
2135
|
-
var heldScripts=[];
|
|
2136
|
-
cleanFrag=cleanFrag.replace(new RegExp('<scr'+'ipt\\\\b[^>]*>[\\\\s\\\\S]*?<\\\\/scr'+'ipt>','gi'),function(m){
|
|
2137
|
-
heldScripts.push(m);
|
|
2138
|
-
return '<!--stx-held-script-'+(heldScripts.length-1)+'-->';
|
|
2139
|
-
});
|
|
2140
|
-
cleanFrag=cleanFrag.replace(new RegExp('<sty'+'le\\\\b([^>]*)>([\\\\s\\\\S]*?)<\\\\/sty'+'le>','gi'),function(m,attrs,css){
|
|
2141
|
-
if(attrs.indexOf('data-css')!==-1){
|
|
2142
|
-
fragCss=css;
|
|
2143
|
-
}else{
|
|
2144
|
-
fragStyles.push({attrs:attrs,css:css});
|
|
2145
|
-
}
|
|
2146
|
-
return '';
|
|
2147
|
-
});
|
|
2148
|
-
cleanFrag=cleanFrag.replace(new RegExp('<link\\\\b([^>]*)>','gi'),function(m,attrs){
|
|
2149
|
-
if(attrs.indexOf('data-css')===-1)return m;
|
|
2150
|
-
var hrefMatch=attrs.match(/\\bhref=(["'])(.*?)\\1/i);
|
|
2151
|
-
if(hrefMatch&&hrefMatch[2])fragCssHrefs.push(hrefMatch[2]);
|
|
2152
|
-
return '';
|
|
2153
|
-
});
|
|
2154
|
-
cleanFrag=cleanFrag.replace(new RegExp('<!--stx-held-script-(\\\\d+)-->','g'),function(m,i){return heldScripts[Number(i)]});
|
|
2155
|
-
// Remove old page styles (not css \u2014 that gets merged)
|
|
2156
|
-
qsa('style[data-stx-page]').forEach(function(s){s.remove()});
|
|
2157
|
-
// Merge css CSS from fragment into existing css style
|
|
2158
|
-
if(fragCss){
|
|
2159
|
-
var curCss=qs('head style[data-css]');
|
|
2160
|
-
if(curCss){
|
|
2161
|
-
var merged=mergeCss(curCss.textContent||'',fragCss);
|
|
2162
|
-
if(merged)curCss.textContent=merged;
|
|
2163
|
-
}else{
|
|
2164
|
-
var cw=ce('style');
|
|
2165
|
-
cw.setAttribute('data-css','generated');
|
|
2166
|
-
cw.textContent=fragCss;
|
|
2167
|
-
dhead().appendChild(cw);
|
|
2168
|
-
}
|
|
2169
|
-
}
|
|
2170
|
-
// Already present ones are MOVED last, not skipped \u2014 see
|
|
2171
|
-
// promoteGeneratedCss for why their order is the whole bug.
|
|
2172
|
-
promoteGeneratedCss(fragCssHrefs,location.href);
|
|
2173
|
-
// Add new page styles
|
|
2174
|
-
fragStyles.forEach(function(s){
|
|
2175
|
-
var el=ce('style');
|
|
2176
|
-
el.textContent=s.css;
|
|
2177
|
-
el.setAttribute('data-stx-page','');
|
|
2178
|
-
dhead().appendChild(el);
|
|
2179
|
-
});
|
|
2180
|
-
// Swap content \u2014 apply the destination container's own attributes first
|
|
2181
|
-
// so the incoming markup lands in a correctly-laid-out container instead
|
|
2182
|
-
// of flashing (or sticking) unstyled.
|
|
2183
|
-
if(scr)scr.cattrs=pendingContainerAttrs;
|
|
2184
|
-
applyContainerAttrs(host,pendingContainerAttrs);
|
|
2185
|
-
currentContent.innerHTML=cleanFrag;
|
|
2186
|
-
// Remove old page scripts
|
|
2187
|
-
qsa('script[data-stx-page]').forEach(function(s){s.remove()});
|
|
2188
|
-
if(pushState!==false)writeHistory(pushState,url+(hash||''));
|
|
2189
|
-
if(scr)screenCommit(scr,url+(hash||''));
|
|
2190
|
-
refreshCurrentLinks();
|
|
2191
|
-
applyScroll(hash);
|
|
2192
|
-
window.dispatchEvent(new CustomEvent('stx:navigate',{detail:{url:url,direction:ctx.direction}}));
|
|
2193
|
-
// Before page scripts run, so a page that focuses its own control on
|
|
2194
|
-
// mount still wins \u2014 its script executes after this.
|
|
2195
|
-
focusAfterNavigation(hash);
|
|
2196
|
-
// Deferred a tick because the title is applied by the caller after this
|
|
2197
|
-
// swap resolves, and the announcement should read the new one.
|
|
2198
|
-
setTimeout(announceRoute,0);
|
|
2199
|
-
// Execute page scripts FIRST \u2014 they define setup functions and set _latestSetup
|
|
2200
|
-
log('[router] frag scripts:', fragScripts.length);
|
|
2201
|
-
qsa('script[data-stx-page]').forEach(function(s){s.remove()});
|
|
2202
|
-
// The page's module registry first (#1957), setups last. A fragment
|
|
2203
|
-
// lists its scripts in document order, and the registry is emitted
|
|
2204
|
-
// after the content it serves: a component that imports a package ran
|
|
2205
|
-
// ahead of it and threw "is not registered on this page". The full
|
|
2206
|
-
// document path orders them the same way (registryFirst, below).
|
|
2207
|
-
function fragOrder(entry){return entry.registry?0:entry.composables?1:entry.setupName?3:2}
|
|
2208
|
-
fragScripts.sort(function(a,b){return fragOrder(a)-fragOrder(b)});
|
|
2209
|
-
function runFragScripts(){
|
|
2210
|
-
fragScripts.forEach(function(entry){
|
|
2211
|
-
var code=entry.text;
|
|
2212
|
-
log('[router] exec script len:', code.length, 'has __stx_setup:', code.indexOf('__stx_setup')>-1);
|
|
2213
|
-
var placeholder=qs('script[data-stx-route-script="'+entry.slot+'"]');
|
|
2214
|
-
var nestedScopedTemplate=false;
|
|
2215
|
-
var scopedLoopSetup=false;
|
|
2216
|
-
if(entry.scoped&&placeholder){
|
|
2217
|
-
qsa('template').forEach(function(template){
|
|
2218
|
-
if(template.contains(placeholder)||(template.content&&template.content.contains(placeholder)))
|
|
2219
|
-
nestedScopedTemplate=true;
|
|
2220
|
-
});
|
|
2221
|
-
var loopRoot=placeholder.previousSibling;
|
|
2222
|
-
while(loopRoot&&(
|
|
2223
|
-
(loopRoot.nodeType===3&&!(loopRoot.textContent||'').trim())
|
|
2224
|
-
||loopRoot.nodeType===8
|
|
2225
|
-
))loopRoot=loopRoot.previousSibling;
|
|
2226
|
-
if(loopRoot&&loopRoot.nodeType===1&&loopRoot.hasAttribute('data-stx-scope')
|
|
2227
|
-
&&(loopRoot.hasAttribute(':for')||loopRoot.hasAttribute('@for')||loopRoot.hasAttribute('x-for'))){
|
|
2228
|
-
var loopScopeId=loopRoot.getAttribute('data-stx-scope');
|
|
2229
|
-
scopedLoopSetup=!!loopScopeId&&code.indexOf(loopScopeId)!==-1;
|
|
2230
|
-
}
|
|
2231
|
-
}
|
|
2232
|
-
if(entry.scoped&&(!placeholder||!placeholder.parentNode||nestedScopedTemplate||scopedLoopSetup)){
|
|
2233
|
-
log('[router] leaving loop-scoped script for component hydration');
|
|
2234
|
-
return;
|
|
2235
|
-
}
|
|
2236
|
-
// Skip scripts that were already executed (layout-level partials
|
|
2237
|
-
// like theme.stx, stores.stx, nav.stx). Their top-level const/let
|
|
2238
|
-
// declarations would throw "Identifier has already been declared"
|
|
2239
|
-
// on re-execution. Setup functions (__stx_setup_) must always
|
|
2240
|
-
// re-execute because each page has its own setup.
|
|
2241
|
-
if(ownerStays(entry.owner)){
|
|
2242
|
-
log('[router] skipping component that stayed on the page:', entry.owner);
|
|
2243
|
-
return;
|
|
2244
|
-
}
|
|
2245
|
-
var h=hashScript(code);
|
|
2246
|
-
var isSetup=code.indexOf('__stx_setup_')!==-1;
|
|
2247
|
-
// Self-contained scope scripts must re-execute on every navigation:
|
|
2248
|
-
// they own their scope, won't collide with prior runs, and
|
|
2249
|
-
// _cleanupContainer deleted their entry from window.stx._scopes on
|
|
2250
|
-
// the way out. Skipping one leaves the page leaf with no scope
|
|
2251
|
-
// registered \u2014 bindIf/@event never re-bind and every :if branch
|
|
2252
|
-
// stays visible at once. Declared by the emitter via data-stx-run,
|
|
2253
|
-
// sniffed only as a fallback (#1773).
|
|
2254
|
-
var isAlreadyScoped=runsAlways(entry.run,code);
|
|
2255
|
-
if(!isSetup&&!isAlreadyScoped&&executedScriptHashes[h]){
|
|
2256
|
-
log('[router] skipping already-executed script (hash dedup)');
|
|
2257
|
-
return;
|
|
2258
|
-
}
|
|
2259
|
-
executedScriptHashes[h]=1;
|
|
2260
|
-
var ns=ce('script');
|
|
2261
|
-
var hasImport=hasStaticImport(code);
|
|
2262
|
-
if(hasImport){
|
|
2263
|
-
ns.type='module';
|
|
2264
|
-
}
|
|
2265
|
-
ns.textContent=markRan((hasImport||isAlreadyScoped)?code:'{'+code+'}');
|
|
2266
|
-
ns.setAttribute('data-stx-page','');
|
|
2267
|
-
if(placeholder&&placeholder.parentNode){
|
|
2268
|
-
ns.setAttribute('data-stx-positioned','');
|
|
2269
|
-
placeholder.parentNode.replaceChild(ns,placeholder);
|
|
2270
|
-
}else{
|
|
2271
|
-
document.body.appendChild(ns);
|
|
2272
|
-
}
|
|
2273
|
-
});
|
|
2274
|
-
// Guarded: the router ships on every page, the signals runtime only on
|
|
2275
|
-
// pages that have a client script. log() is debug-gated but its
|
|
2276
|
-
// ARGUMENTS are evaluated at the call site regardless, so an unguarded
|
|
2277
|
-
// read here threw on every runtime-less page \u2014 aborting the swap and
|
|
2278
|
-
// falling back to a full document load (#1809).
|
|
2279
|
-
log('[router] scripts done. _latestSetup:', !!(window.stx&&window.stx._latestSetup));
|
|
2280
|
-
// THEN fire stx:load \u2014 now _latestSetup is set and processElement has the right scope
|
|
2281
|
-
window.dispatchEvent(new Event('stx:load'));
|
|
2282
|
-
}
|
|
2283
|
-
// The container's own files first, for the same reason as the
|
|
2284
|
-
// full-document path: the inline scripts call into them. Only awaited
|
|
2285
|
-
// when something actually has to load, so a fragment with no external
|
|
2286
|
-
// scripts \u2014 every fragment before this change \u2014 still runs its inline
|
|
2287
|
-
// scripts in the same synchronous turn and nothing about its timing
|
|
2288
|
-
// moves.
|
|
2289
|
-
var fragPending=[];
|
|
2290
|
-
fragExternalScripts.forEach(function(src){
|
|
2291
|
-
var pending=loadExternalScript(src,url);
|
|
2292
|
-
if(pending)fragPending.push(pending);
|
|
2293
|
-
});
|
|
2294
|
-
if(fragPending.length)Promise.all(fragPending).then(runFragScripts).catch(runFragScripts);
|
|
2295
|
-
else runFragScripts();
|
|
2296
|
-
}
|
|
2297
|
-
// With screens retained, the outgoing page is kept (or disposed once it
|
|
2298
|
-
// has animated away) by screenBegin rather than cleaned up here.
|
|
2299
|
-
var scr=null;
|
|
2300
|
-
var useScreens=screensOn();
|
|
2301
|
-
function startFragSwap(){
|
|
2302
|
-
if(ctx.id&&ctx.id!==navSeq)return Promise.resolve(false);
|
|
2303
|
-
if(pushState!==false)innerSnap=innerScroll(activeRoot());
|
|
2304
|
-
if(useScreens)settleAnimation();
|
|
2305
|
-
else if(window.stx&&window.stx._cleanupContainer)window.stx._cleanupContainer(currentContent);
|
|
2306
|
-
return new Promise(function(resolve,reject){
|
|
2307
|
-
function completeFragSwap(){
|
|
2308
|
-
try{
|
|
2309
|
-
setDirection(ctx.direction);
|
|
2310
|
-
if(useScreens){scr=screenBegin(host,ctx,html,pushState!==false);currentContent=scr.el}
|
|
2311
|
-
doFragSwap();
|
|
2312
|
-
if(scr)screenEnd(scr);
|
|
2313
|
-
resolve(true);
|
|
2314
|
-
}catch(err){reject(err)}
|
|
2315
|
-
}
|
|
2316
|
-
if(instantNav&&swapRunsInOneTask(html,url)){runInOneTask(completeFragSwap)}
|
|
2317
|
-
else if(instantNav&&runInstantSwap(completeFragSwap)){}
|
|
2318
|
-
else if(useScreens){completeFragSwap()}
|
|
2319
|
-
else if(runViewTransition(completeFragSwap)){}
|
|
2320
|
-
else if(instantNav){completeFragSwap()}
|
|
2321
|
-
else{currentContent.style.transition='opacity 0.12s ease-out';currentContent.style.opacity='0';setTimeout(function(){completeFragSwap();currentContent.style.opacity='1';setTimeout(function(){currentContent.style.transition=''},150)},120)}
|
|
2322
|
-
});
|
|
2323
|
-
}
|
|
2324
|
-
// A stylesheet this page has never loaded is fetched BEFORE the content
|
|
2325
|
-
// swaps, or the new markup paints once without its utilities and then
|
|
2326
|
-
// jumps. Cleanup waits with it so the outgoing page stays live meanwhile.
|
|
2327
|
-
// With nothing new to load this runs in the same turn, as it always did.
|
|
2328
|
-
var fragCssReady=preloadGeneratedCss(fragmentCssHrefs(html),location.href);
|
|
2329
|
-
return afterCss(fragCssReady,url,startFragSwap);
|
|
2330
|
-
}
|
|
2331
|
-
|
|
2332
|
-
// Full document mode: parse with DOMParser and extract container content
|
|
2333
|
-
var parser=new DOMParser();
|
|
2334
|
-
var doc=parser.parseFromString(html,'text/html');
|
|
2335
|
-
// Full documents carry the build id as a meta rather than a header, and
|
|
2336
|
-
// this path also serves cached HTML, so check here too (#1772).
|
|
2337
|
-
var docBuildMeta=doc.querySelector(BUILD_META);
|
|
2338
|
-
var docBuild=docBuildMeta?(docBuildMeta.getAttribute('content')||''):'';
|
|
2339
|
-
if(isBuildSkew(docBuild)){reloadForSkew(url,docBuild);return Promise.resolve(false)}
|
|
2340
|
-
// Same hand-off as the fragment path, for the same reason (#1809, #1839).
|
|
2341
|
-
// A full document DOES carry the destination's runtime \u2014 and this path
|
|
2342
|
-
// throws it away: prepareRoutedBodyScripts drops every script[src] and
|
|
2343
|
-
// everything isSignalsRuntimeScript matches, on the assumption that the
|
|
2344
|
-
// current page already has one. When it does not, nothing puts a runtime
|
|
2345
|
-
// back, so the destination's client code dies on its first line with
|
|
2346
|
-
// "defineStore is not defined" (defineStore lives on window.stx) and the
|
|
2347
|
-
// page renders quietly inert \u2014 no error reaches the user.
|
|
2348
|
-
//
|
|
2349
|
-
// Before the swap and before _cleanupContainer, so we neither dispose the
|
|
2350
|
-
// outgoing page's scopes nor half-replace the document and then reload.
|
|
2351
|
-
if(!window.stx&&documentShipsRuntime(doc,html)){
|
|
2352
|
-
log('[router] document needs the signals runtime and this page has none \u2014 full navigation to:',url);
|
|
2353
|
-
location.href=url;
|
|
2354
|
-
return Promise.resolve(false);
|
|
2355
|
-
}
|
|
2356
|
-
var newContent=doc.querySelector(containerSel)||doc.querySelector('[data-stx-content]')||doc.querySelector('main');
|
|
2357
|
-
if(!newContent){location.href=url;return Promise.resolve(false)}
|
|
2358
|
-
|
|
2359
|
-
// The destination's generated utility sheets, resolved against the page
|
|
2360
|
-
// being navigated to. Loaded before the swap and moved last during it \u2014
|
|
2361
|
-
// same reasons as the fragment path (promoteGeneratedCss).
|
|
2362
|
-
var docCssBase;
|
|
2363
|
-
try{docCssBase=new URL(url,location.href).href}catch(e){docCssBase=location.href}
|
|
2364
|
-
var docCssHrefs=[];
|
|
2365
|
-
doc.querySelectorAll('head link[data-css][href]').forEach(function(l){docCssHrefs.push(l.getAttribute('href'))});
|
|
2366
|
-
|
|
2367
|
-
function doSwap(){
|
|
2368
|
-
// \u2500\u2500 Swap <head> styles \u2500\u2500
|
|
2369
|
-
// Inject new styles FIRST, then remove old to prevent unstyled flash
|
|
2370
|
-
var keepIds={'stx-r-css':1};
|
|
2371
|
-
var curStyles=qsa('head style');
|
|
2372
|
-
var newStyles=doc.querySelectorAll('head style');
|
|
2373
|
-
|
|
2374
|
-
// Merge css styles instead of replacing \u2014 persistent elements
|
|
2375
|
-
// (nav, footer) outside <main> still need their utility classes
|
|
2376
|
-
var curCss=qs('head style[data-css]');
|
|
2377
|
-
var newCss=null;
|
|
2378
|
-
newStyles.forEach(function(s){if(s.getAttribute('data-css'))newCss=s});
|
|
2379
|
-
|
|
2380
|
-
if(curCss&&newCss){
|
|
2381
|
-
var merged=mergeCss(curCss.textContent||'',newCss.textContent||'');
|
|
2382
|
-
if(merged)curCss.textContent=merged;
|
|
2383
|
-
}
|
|
2384
|
-
|
|
2385
|
-
var incoming=[];
|
|
2386
|
-
newStyles.forEach(function(s){
|
|
2387
|
-
if(!keepIds[s.id]&&!s.getAttribute('data-css')){
|
|
2388
|
-
var ns=ce('style');
|
|
2389
|
-
ns.textContent=s.textContent;
|
|
2390
|
-
ns.setAttribute('data-stx-incoming','');
|
|
2391
|
-
dhead().appendChild(ns);
|
|
2392
|
-
incoming.push(ns);
|
|
2393
|
-
}
|
|
2394
|
-
});
|
|
2395
|
-
|
|
2396
|
-
// If no existing css but new page has one, add it
|
|
2397
|
-
if(!curCss&&newCss){
|
|
2398
|
-
var ns=ce('style');
|
|
2399
|
-
ns.textContent=newCss.textContent;
|
|
2400
|
-
ns.setAttribute('data-css',newCss.getAttribute('data-css'));
|
|
2401
|
-
dhead().appendChild(ns);
|
|
2402
|
-
}
|
|
2403
|
-
|
|
2404
|
-
// Remove old styles (except persistent ones, css, and incoming)
|
|
2405
|
-
curStyles.forEach(function(s){
|
|
2406
|
-
if(!keepIds[s.id]&&!s.hasAttribute('data-stx-incoming')&&!s.hasAttribute('data-css'))s.remove();
|
|
2407
|
-
});
|
|
2408
|
-
|
|
2409
|
-
incoming.forEach(function(s){s.removeAttribute('data-stx-incoming')});
|
|
2410
|
-
|
|
2411
|
-
// \u2500\u2500 Swap <head> stylesheet / font <link>s \u2500\u2500
|
|
2412
|
-
// Without this, an SPA navigation to a page whose <link rel="stylesheet">
|
|
2413
|
-
// (or font/preconnect <link>) the current page had not already loaded left
|
|
2414
|
-
// the swapped-in content completely unstyled (e.g. a page that pulls in its
|
|
2415
|
-
// own marketing.css, or Google Fonts). Additive and href-deduped, mirroring
|
|
2416
|
-
// the external <head script[src]> reconcile below \u2014 never removes existing
|
|
2417
|
-
// links, so stylesheets shared across pages simply persist.
|
|
2418
|
-
// Relative asset hrefs resolve against the document that declared them:
|
|
2419
|
-
// the CURRENT page for links already in <head>, and the page being
|
|
2420
|
-
// navigated TO for links from the fetched document. Against location.origin
|
|
2421
|
-
// both collapsed to the site root, so 'assets/app.css' on /docs/intro
|
|
2422
|
-
// deduped as /assets/app.css \u2014 the wrong file, and a false cache hit that
|
|
2423
|
-
// skipped the real stylesheet (#1777).
|
|
2424
|
-
var docBase;
|
|
2425
|
-
try{docBase=new URL(url,location.href).href}catch(e){docBase=location.href}
|
|
2426
|
-
var linkSel='link[rel="stylesheet"],link[rel="preconnect"]';
|
|
2427
|
-
var curLinks={};
|
|
2428
|
-
qsa('head '+linkSel).forEach(function(l){var h=l.getAttribute('href');if(h)curLinks[new URL(h,location.href).href]=1});
|
|
2429
|
-
doc.querySelectorAll('head '+linkSel).forEach(function(l){
|
|
2430
|
-
var href=l.getAttribute('href');
|
|
2431
|
-
if(!href)return;
|
|
2432
|
-
// Generated utility sheets are not additive-only: promoteGeneratedCss
|
|
2433
|
-
// below also reorders them.
|
|
2434
|
-
if(l.hasAttribute('data-css'))return;
|
|
2435
|
-
var abs=new URL(href,docBase).href;
|
|
2436
|
-
if(curLinks[abs])return;
|
|
2437
|
-
curLinks[abs]=1;
|
|
2438
|
-
var nl=ce('link');
|
|
2439
|
-
Array.from(l.attributes).forEach(function(a){nl.setAttribute(a.name,a.value)});
|
|
2440
|
-
dhead().appendChild(nl);
|
|
2441
|
-
});
|
|
2442
|
-
// After every other stylesheet link, so the destination's complete
|
|
2443
|
-
// utility sheet is last in the cascade.
|
|
2444
|
-
promoteGeneratedCss(docCssHrefs,docBase);
|
|
2445
|
-
|
|
2446
|
-
// \u2500\u2500 Swap content \u2500\u2500
|
|
2447
|
-
// For layout changes: swap the entire <body> to replace layout chrome (nav, footer, etc.)
|
|
2448
|
-
// For same-layout: swap only the container (<main>)
|
|
2449
|
-
var newBody=doc.querySelector('body');
|
|
2450
|
-
var isLayoutChange=false;
|
|
2451
|
-
var declaredLayout=pendingLayoutDecl;
|
|
2452
|
-
pendingLayoutDecl=null;
|
|
2453
|
-
if(newBody){
|
|
2454
|
-
var newMeta=doc.querySelector(LAYOUT_META);
|
|
2455
|
-
var curMeta=qs(LAYOUT_META);
|
|
2456
|
-
var curLayout=curMeta?curMeta.getAttribute('content'):'';
|
|
2457
|
-
// The server's answer wins over the document's metas. We are only here
|
|
2458
|
-
// because checkLayoutChange already said the group changed, based on
|
|
2459
|
-
// exactly these headers \u2014 re-deriving from metas and disagreeing meant
|
|
2460
|
-
// paying for the full-document fetch and then discarding the reason we
|
|
2461
|
-
// made it, so the destination's <main> was swapped into the previous
|
|
2462
|
-
// page's chrome (#1833).
|
|
2463
|
-
//
|
|
2464
|
-
// They disagree more easily than they look. The bun-plugin serve path
|
|
2465
|
-
// calls a layout-less page 'default' while the client's own fallback
|
|
2466
|
-
// calls it 'app'; a <head> with attributes silently skips the meta
|
|
2467
|
-
// injection entirely; and the i18n path rewrites every page's group to
|
|
2468
|
-
// i18n:<locale>. Any one of those leaves the metas saying "unchanged"
|
|
2469
|
-
// about a document the headers said had changed.
|
|
2470
|
-
var newLayout=declaredLayout&&declaredLayout.layout
|
|
2471
|
-
? declaredLayout.layout
|
|
2472
|
-
: (newMeta?newMeta.getAttribute('content'):'');
|
|
2473
|
-
var curGroup=getCurrentLayoutGroup();
|
|
2474
|
-
var newGroup=declaredLayout&&declaredLayout.group
|
|
2475
|
-
? declaredLayout.group
|
|
2476
|
-
: getDocLayoutGroup(doc,newLayout);
|
|
2477
|
-
isLayoutChange=curGroup!==newGroup||!!(curLayout&&newLayout&&curLayout!==newLayout);
|
|
2478
|
-
}
|
|
2479
|
-
var incomingSetupName=newBody?(newBody.getAttribute('data-stx')||''):'';
|
|
2480
|
-
// Scripts assigned through innerHTML are inert. Preserve executable
|
|
2481
|
-
// scripts as placeholders so re-execution keeps document.currentScript
|
|
2482
|
-
// beside the component template it owns.
|
|
2483
|
-
var routedBodyScripts=[];
|
|
2484
|
-
var routedExternalScripts=[];
|
|
2485
|
-
var routedBodyScriptId=0;
|
|
2486
|
-
function prepareRoutedBodyScripts(root){
|
|
2487
|
-
if(!root)return;
|
|
2488
|
-
root.querySelectorAll('script').forEach(function(s){
|
|
2489
|
-
var text=s.textContent||'';
|
|
2490
|
-
// Not discarded: queued against docBase, so a relative src resolves
|
|
2491
|
-
// against the page being navigated to rather than the current URL.
|
|
2492
|
-
// The runtime and router are dropped, not queued: see isSharedClientScript.
|
|
2493
|
-
if(s.hasAttribute('src')){if(!isSharedClientScript(s))routedExternalScripts.push(s.getAttribute('src'));s.remove();return}
|
|
2494
|
-
var type=(s.getAttribute('type')||'').trim().toLowerCase();
|
|
2495
|
-
var executable=!type||type==='text/javascript'||type==='application/javascript'||type==='module';
|
|
2496
|
-
if(!executable||!text.trim())return;
|
|
2497
|
-
if(isSignalsRuntimeScript(s,text)||text.indexOf('__stxRouter')!==-1){s.remove();return}
|
|
2498
|
-
var slot='document-'+(++routedBodyScriptId);
|
|
2499
|
-
var setupName=generatedSetupName(text);
|
|
2500
|
-
if(setupName)incomingSetupName=setupName;
|
|
2501
|
-
routedBodyScripts.push({text:text,runAlways:true,slot:slot,setupName:setupName,run:s.getAttribute('data-stx-run')||'',owner:s.getAttribute('data-stx-owner')||'',registry:isModuleRegistryScript(s),composables:isComposablesScript(s)});
|
|
2502
|
-
s.textContent='';
|
|
2503
|
-
s.setAttribute('type','application/stx-pending');
|
|
2504
|
-
s.setAttribute('data-stx-route-script',slot);
|
|
2505
|
-
});
|
|
2506
|
-
}
|
|
2507
|
-
prepareRoutedBodyScripts(isLayoutChange?newBody:newContent);
|
|
2508
|
-
if(isLayoutChange&&newBody){
|
|
2509
|
-
log('[router] full body swap for layout change');
|
|
2510
|
-
// Replace entire body content \u2014 layout chrome and all
|
|
2511
|
-
var bodyHTML=newBody.innerHTML;
|
|
2512
|
-
// The outgoing chrome's components are destroyed with it (#1958).
|
|
2513
|
-
// They are no longer re-run on every navigation, so they live as long
|
|
2514
|
-
// as their layout does, and _cleanupContainer above only covered the
|
|
2515
|
-
// container: their onDestroy and their scopes waited for the next
|
|
2516
|
-
// navigation's orphan sweep, and their effects were never disposed.
|
|
2517
|
-
// Before the swap, while the old markup is still here to walk.
|
|
2518
|
-
if(window.stx&&window.stx._cleanupContainer)window.stx._cleanupContainer(document.body);
|
|
2519
|
-
document.body.innerHTML=bodyHTML;
|
|
2520
|
-
// Copy body attributes (class, data-stx, etc.)
|
|
2521
|
-
Array.from(newBody.attributes).forEach(function(attr){document.body.setAttribute(attr.name,attr.value)});
|
|
2522
|
-
// Update layout meta tag
|
|
2523
|
-
var oldMeta=qs(LAYOUT_META);
|
|
2524
|
-
var freshMeta=doc.querySelector(LAYOUT_META);
|
|
2525
|
-
if(oldMeta&&freshMeta)oldMeta.setAttribute('content',freshMeta.getAttribute('content')||'');
|
|
2526
|
-
else if(freshMeta){var m=ce('meta');m.name='stx-layout';m.content=freshMeta.getAttribute('content')||'';dhead().appendChild(m)}
|
|
2527
|
-
var oldGroupMeta=qs(LAYOUT_GROUP_META);
|
|
2528
|
-
var freshGroupMeta=doc.querySelector(LAYOUT_GROUP_META);
|
|
2529
|
-
if(oldGroupMeta&&freshGroupMeta)oldGroupMeta.setAttribute('content',freshGroupMeta.getAttribute('content')||'');
|
|
2530
|
-
else if(freshGroupMeta){var gm=ce('meta');gm.name='stx-layout-group';gm.content=freshGroupMeta.getAttribute('content')||'';dhead().appendChild(gm)}
|
|
2531
|
-
// Update container reference for script execution below
|
|
2532
|
-
currentContent=qs(containerSel)||qs('main')||document.body;
|
|
2533
|
-
} else {
|
|
2534
|
-
// Same layout \u2014 swap only container content. Carry the destination
|
|
2535
|
-
// container's attributes across too (same reason as the fragment path:
|
|
2536
|
-
// pages whose layout lives on <main> itself would otherwise lose it).
|
|
2537
|
-
var attrMap={};
|
|
2538
|
-
Array.prototype.forEach.call(newContent.attributes,function(a){attrMap[a.name]=a.value});
|
|
2539
|
-
setContainerAttrs(currentContent,attrMap);
|
|
2540
|
-
var cleanHTML=newContent.innerHTML;
|
|
2541
|
-
currentContent.innerHTML=cleanHTML;
|
|
2542
|
-
if(incomingSetupName)document.body.setAttribute('data-stx',incomingSetupName);
|
|
2543
|
-
}
|
|
2544
|
-
|
|
2545
|
-
// Teardown (including outgoing layout chrome) has finished. Its destroy
|
|
2546
|
-
// callbacks must not be able to invalidate the incoming page's snapshot.
|
|
2547
|
-
hydrateServerData(html);
|
|
2548
|
-
|
|
2549
|
-
// \u2500\u2500 Load new external <head> scripts \u2500\u2500
|
|
2550
|
-
var loadedSrcs={};
|
|
2551
|
-
qsa('head script[src]').forEach(function(s){loadedSrcs[s.src]=1});
|
|
2552
|
-
var extPromises=[];
|
|
2553
|
-
doc.querySelectorAll('head script[src]').forEach(function(s){
|
|
2554
|
-
if(isSharedClientScript(s))return;
|
|
2555
|
-
// Fetched-document script: resolve against the page being navigated to
|
|
2556
|
-
// (docBase), not the origin root \u2014 same rationale as the <link> reconcile (#1777).
|
|
2557
|
-
var src=new URL(s.getAttribute('src'),docBase).href;
|
|
2558
|
-
if(loadedSrcs[src])return;
|
|
2559
|
-
loadedSrcs[src]=1;
|
|
2560
|
-
extPromises.push(new Promise(function(resolve,reject){
|
|
2561
|
-
var ns=ce('script');
|
|
2562
|
-
ns.src=src;
|
|
2563
|
-
ns.onload=resolve;
|
|
2564
|
-
ns.onerror=reject;
|
|
2565
|
-
dhead().appendChild(ns);
|
|
2566
|
-
}));
|
|
2567
|
-
});
|
|
2568
|
-
|
|
2569
|
-
// Before the inline scripts, and through the same barrier: an inline
|
|
2570
|
-
// script's whole reason for existing is often to call into one of these,
|
|
2571
|
-
// so running it first would fail exactly the way this bug did.
|
|
2572
|
-
routedExternalScripts.forEach(function(src){
|
|
2573
|
-
var pending=loadExternalScript(src,docBase);
|
|
2574
|
-
if(pending)extPromises.push(pending);
|
|
2575
|
-
});
|
|
2576
|
-
|
|
2577
|
-
// \u2500\u2500 Script re-execution \u2500\u2500
|
|
2578
|
-
// Remove previously injected page scripts
|
|
2579
|
-
qsa('script[data-stx-page]').forEach(function(s){s.remove()});
|
|
2580
|
-
|
|
2581
|
-
var scripts=routedBodyScripts.slice();
|
|
2582
|
-
function addScript(text, runAlways, slot, owner){
|
|
2583
|
-
scripts.push({text:text,runAlways:!!runAlways,slot:slot||'',owner:owner||'',registry:REGISTRY_TEXT.test(text),composables:COMPOSABLES_TEXT.test(text)});
|
|
2584
|
-
}
|
|
2585
|
-
if(isLayoutChange){
|
|
2586
|
-
// Also collect setup functions from <head>, and the page's module
|
|
2587
|
-
// registry (#1957). The registry is anchored to the signals runtime,
|
|
2588
|
-
// and a layout with its own <head> script (analytics, say) pulls the
|
|
2589
|
-
// runtime -- and the registry with it -- into <head>. Collecting only
|
|
2590
|
-
// setups there dropped it, so every component that imports a local
|
|
2591
|
-
// module threw "is not registered on this page" and its template
|
|
2592
|
-
// hydrated against globals: an <input id="email"> bound to
|
|
2593
|
-
// :model="email" showed "[object HTMLInputElement]".
|
|
2594
|
-
doc.querySelectorAll('head script').forEach(function(s){
|
|
2595
|
-
var text=s.textContent||'';
|
|
2596
|
-
if(s.hasAttribute('src'))return;
|
|
2597
|
-
if(!text.trim())return;
|
|
2598
|
-
if(isSignalsRuntimeScript(s,text))return;
|
|
2599
|
-
if(isModuleRegistryScript(s)||isComposablesScript(s))addScript(text,true,'','');
|
|
2600
|
-
else if(text.indexOf('__stx_setup_')!==-1)addScript(text,true);
|
|
2601
|
-
});
|
|
2602
|
-
} else {
|
|
2603
|
-
// Collect page scripts from <head> AND <body> (outside container).
|
|
2604
|
-
// SSG builds place the setup function in <body> before <main>, not
|
|
2605
|
-
// in <head>. Without this, SPA navigation on static sites never
|
|
2606
|
-
// runs the setup and reactive data stays empty.
|
|
2607
|
-
var seenSetups={};
|
|
2608
|
-
scripts.forEach(function(entry){var t=entry.text;if(t.indexOf('__stx_setup_')!==-1)seenSetups[t.substring(0,80)]=1});
|
|
2609
|
-
doc.querySelectorAll('script').forEach(function(s){
|
|
2610
|
-
var text=s.textContent||'';
|
|
2611
|
-
if(s.hasAttribute('src'))return;
|
|
2612
|
-
if(!text.trim())return;
|
|
2613
|
-
// Skip signals runtime and router (they contain __stx_setup references but aren't setup functions)
|
|
2614
|
-
if(isSignalsRuntimeScript(s,text))return;
|
|
2615
|
-
if(text.indexOf('__stxRouter')!==-1)return;
|
|
2616
|
-
if(newContent.contains(s))return;
|
|
2617
|
-
// Code only, as prepareRoutedBodyScripts takes it. A layout island's
|
|
2618
|
-
// type="stx/island" script is hydrated by its own trigger, and one
|
|
2619
|
-
// collected here was set up on every navigation, the trigger or not.
|
|
2620
|
-
var scriptType=(s.getAttribute('type')||'').trim().toLowerCase();
|
|
2621
|
-
if(scriptType&&scriptType!=='text/javascript'&&scriptType!=='application/javascript'&&scriptType!=='module')return;
|
|
2622
|
-
// A component with no scope root has no root for the owner check,
|
|
2623
|
-
// so its script is marked data-stx-instance instead. Out here it
|
|
2624
|
-
// belongs to the layout chrome, which this swap leaves in place with
|
|
2625
|
-
// its instance running; running it again set up a second one, and a
|
|
2626
|
-
// stx.mount wrapper re-run from here mounted onto the container, the
|
|
2627
|
-
// incoming page's content (#1958). Fragments leave these out.
|
|
2628
|
-
if(s.hasAttribute('data-stx-instance'))return;
|
|
2629
|
-
if(text.indexOf('__stx_setup_')===-1&&!s.hasAttribute('data-stx-scoped')&&!s.hasAttribute('data-stx-page')&&!s.hasAttribute('data-stx-route-params'))return;
|
|
2630
|
-
var key=text.substring(0,80);
|
|
2631
|
-
if(seenSetups[key])return;
|
|
2632
|
-
seenSetups[key]=1;
|
|
2633
|
-
// A layout component outside the container carries its owner, so
|
|
2634
|
-
// execScripts skips it when it stayed on the page (#1958).
|
|
2635
|
-
addScript(text,true,'',s.getAttribute('data-stx-owner')||'');
|
|
2636
|
-
});
|
|
2637
|
-
}
|
|
2638
|
-
// Component setup scripts also assign window.stx._latestSetup. Run the
|
|
2639
|
-
// destination page setup last so stx:load hydrates with the page scope,
|
|
2640
|
-
// not whichever nested component happened to render last.
|
|
2641
|
-
if(incomingSetupName){
|
|
2642
|
-
scripts.sort(function(a,b){
|
|
2643
|
-
var aPage=(a.setupName||generatedSetupName(a.text))===incomingSetupName?1:0;
|
|
2644
|
-
var bPage=(b.setupName||generatedSetupName(b.text))===incomingSetupName?1:0;
|
|
2645
|
-
return aPage-bPage;
|
|
2646
|
-
});
|
|
2647
|
-
}
|
|
2648
|
-
// The module registry runs before anything that reads it (#1957). The
|
|
2649
|
-
// container's own scripts are collected first and head scripts after,
|
|
2650
|
-
// so without this a component in the page body could run ahead of the
|
|
2651
|
-
// registry it imports from. Stable: the order within each group holds.
|
|
2652
|
-
var registryFirst=scripts.filter(function(e){return e.registry});
|
|
2653
|
-
var composablesNext=scripts.filter(function(e){return !e.registry&&e.composables});
|
|
2654
|
-
if(registryFirst.length||composablesNext.length)scripts=registryFirst.concat(composablesNext,scripts.filter(function(e){return !e.registry&&!e.composables}));
|
|
2655
|
-
|
|
2656
|
-
// Push history state (before active link updates so location.pathname is current)
|
|
2657
|
-
if(pushState!==false)writeHistory(pushState,url+(hash||''));
|
|
2658
|
-
|
|
2659
|
-
// Update active nav links
|
|
2660
|
-
refreshCurrentLinks();
|
|
2661
|
-
|
|
2662
|
-
// Scroll
|
|
2663
|
-
applyScroll(hash);
|
|
2664
|
-
|
|
2665
|
-
// Update title
|
|
2666
|
-
var newTitle=doc.querySelector('title');
|
|
2667
|
-
if(newTitle)document.title=newTitle.textContent;
|
|
2668
|
-
|
|
2669
|
-
// Update <html lang> from the destination doc so screen readers,
|
|
2670
|
-
// CSS :lang() selectors, and any i18n picker that mirrors
|
|
2671
|
-
// document.documentElement.lang stay accurate after SPA hops.
|
|
2672
|
-
if(doc.documentElement&&doc.documentElement.lang){
|
|
2673
|
-
document.documentElement.lang=doc.documentElement.lang;
|
|
2674
|
-
}
|
|
2675
|
-
|
|
2676
|
-
// \u2500\u2500 Reconcile <html> attributes (stacksjs/stx#1798) \u2500\u2500
|
|
2677
|
-
// A layout that scopes its design tokens to the root element
|
|
2678
|
-
// (html.marketing { --bg: \u2026 }) needs that class to LEAVE when you
|
|
2679
|
-
// navigate to a layout that doesn't want it \u2014 otherwise both token sets
|
|
2680
|
-
// match at once and the second layout paints with the first one's
|
|
2681
|
-
// palette. Head stylesheets are additive across a swap, so the class is
|
|
2682
|
-
// the only thing disambiguating them.
|
|
2683
|
-
//
|
|
2684
|
-
// Only what stx wrote is touched, per the markers emitted by
|
|
2685
|
-
// document-shell.ts. Diffing the whole element against the incoming
|
|
2686
|
-
// document would strip the color-mode boot's dark class and
|
|
2687
|
-
// data-reduced-motion, which exist only on the live page.
|
|
2688
|
-
if(doc.documentElement){
|
|
2689
|
-
var curRoot=document.documentElement,incRoot=doc.documentElement;
|
|
2690
|
-
var tokens=function(el,attr){var v=el.getAttribute(attr);return v?v.split(/\\s+/).filter(Boolean):[]};
|
|
2691
|
-
var prevCls=tokens(curRoot,'data-stx-html-class'),nextCls=tokens(incRoot,'data-stx-html-class');
|
|
2692
|
-
prevCls.forEach(function(c){if(nextCls.indexOf(c)===-1)curRoot.classList.remove(c)});
|
|
2693
|
-
nextCls.forEach(function(c){curRoot.classList.add(c)});
|
|
2694
|
-
if(nextCls.length)curRoot.setAttribute('data-stx-html-class',nextCls.join(' '));
|
|
2695
|
-
else curRoot.removeAttribute('data-stx-html-class');
|
|
2696
|
-
|
|
2697
|
-
var prevNames=tokens(curRoot,'data-stx-html-attrs'),nextNames=tokens(incRoot,'data-stx-html-attrs');
|
|
2698
|
-
prevNames.forEach(function(n){if(nextNames.indexOf(n)===-1)curRoot.removeAttribute(n)});
|
|
2699
|
-
nextNames.forEach(function(n){var v=incRoot.getAttribute(n);if(v!==null)curRoot.setAttribute(n,v)});
|
|
2700
|
-
if(nextNames.length)curRoot.setAttribute('data-stx-html-attrs',nextNames.join(' '));
|
|
2701
|
-
else curRoot.removeAttribute('data-stx-html-attrs');
|
|
2702
|
-
}
|
|
2703
|
-
|
|
2704
|
-
window.dispatchEvent(new CustomEvent('stx:navigate',{detail:{url:url,direction:ctx.direction}}));
|
|
2705
|
-
|
|
2706
|
-
// Execute page scripts FIRST \u2014 they define setup functions and set _latestSetup
|
|
2707
|
-
function execScripts(){
|
|
2708
|
-
scripts.forEach(function(entry){
|
|
2709
|
-
var text=typeof entry==='string'?entry:entry.text;
|
|
2710
|
-
var runAlways=typeof entry==='string'?false:entry.runAlways;
|
|
2711
|
-
if(typeof entry!=='string'&&ownerStays(entry.owner))return;
|
|
2712
|
-
// Skip scripts that were already executed (layout-level partials
|
|
2713
|
-
// like theme.stx). Their top-level const/function declarations
|
|
2714
|
-
// would throw "Identifier has already been declared" on re-execution.
|
|
2715
|
-
// Exception: setup functions (__stx_setup_) must always re-execute
|
|
2716
|
-
// because each page has its own setup, and import statements
|
|
2717
|
-
// need special handling.
|
|
2718
|
-
var h=hashScript(text);
|
|
2719
|
-
var isSetup=text.indexOf('__stx_setup_')!==-1;
|
|
2720
|
-
var hasImport=hasStaticImport(text);
|
|
2721
|
-
if(!runAlways&&!isSetup&&executedScriptHashes[h])return;
|
|
2722
|
-
executedScriptHashes[h]=1;
|
|
2723
|
-
// Wrap scripts with import statements as modules (Bug 3 fix)
|
|
2724
|
-
var ns=ce('script');
|
|
2725
|
-
if(hasImport){
|
|
2726
|
-
ns.type='module';
|
|
2727
|
-
}
|
|
2728
|
-
// Wrap in block scope to prevent const/let collisions on re-navigation.
|
|
2729
|
-
// Top-level const/let in <script> tags are global-scoped in browsers
|
|
2730
|
-
// and throw "Identifier has already been declared" on re-execution.
|
|
2731
|
-
// Skip the wrap for module scripts \u2014 they get their own scope from
|
|
2732
|
-
// ESM, and top-level 'import' is illegal inside a block, which
|
|
2733
|
-
// would throw SyntaxError before the script ever runs.
|
|
2734
|
-
var alreadyScoped=runsAlways(typeof entry==='string'?'':entry.run,text);
|
|
2735
|
-
ns.textContent=markRan((hasImport||alreadyScoped)?text:'{'+text+republishTopLevel(text)+'}');
|
|
2736
|
-
ns.setAttribute('data-stx-page','');
|
|
2737
|
-
var placeholder=entry.slot?qs('script[data-stx-route-script="'+entry.slot+'"]'):null;
|
|
2738
|
-
if(placeholder&&placeholder.parentNode){
|
|
2739
|
-
ns.setAttribute('data-stx-positioned','');
|
|
2740
|
-
placeholder.parentNode.replaceChild(ns,placeholder);
|
|
2741
|
-
}else{
|
|
2742
|
-
document.body.appendChild(ns);
|
|
2743
|
-
}
|
|
2744
|
-
});
|
|
2745
|
-
// THEN fire stx:load \u2014 now _latestSetup is set and processElement has the right scope
|
|
2746
|
-
window.dispatchEvent(new Event('stx:load'));
|
|
2747
|
-
}
|
|
2748
|
-
|
|
2749
|
-
if(extPromises.length>0){
|
|
2750
|
-
Promise.all(extPromises).then(execScripts).catch(execScripts);
|
|
2751
|
-
}
|
|
2752
|
-
else {
|
|
2753
|
-
execScripts();
|
|
2754
|
-
}
|
|
2755
|
-
}
|
|
2756
|
-
|
|
2757
|
-
function startSwap(){
|
|
2758
|
-
if(ctx.id&&ctx.id!==navSeq)return Promise.resolve(false);
|
|
2759
|
-
if(pushState!==false)innerSnap=innerScroll(activeRoot());
|
|
2760
|
-
setDirection(ctx.direction);
|
|
2761
|
-
// A whole document is written into the container as it is, so retained
|
|
2762
|
-
// screens go first: they would otherwise outlive the page they belong to.
|
|
2763
|
-
collapseScreens();
|
|
2764
|
-
currentContent=getContainer();
|
|
2765
|
-
// Clean up existing signals/effects
|
|
2766
|
-
if(window.stx&&window.stx._cleanupContainer){
|
|
2767
|
-
window.stx._cleanupContainer(currentContent);
|
|
2768
|
-
}
|
|
2769
|
-
return new Promise(function(resolve,reject){
|
|
2770
|
-
function completeSwap(){
|
|
2771
|
-
try{
|
|
2772
|
-
doSwap();
|
|
2773
|
-
resolve(true);
|
|
2774
|
-
}catch(err){reject(err)}
|
|
2775
|
-
}
|
|
2776
|
-
if(instantNav&&runInstantSwap(completeSwap)){
|
|
2777
|
-
}
|
|
2778
|
-
else if(runViewTransition(completeSwap)){
|
|
2779
|
-
}
|
|
2780
|
-
else if(instantNav){
|
|
2781
|
-
// A tab: swap at once, no fade either way.
|
|
2782
|
-
completeSwap();
|
|
2783
|
-
}
|
|
2784
|
-
else {
|
|
2785
|
-
// Fallback fade for browsers without View Transitions API
|
|
2786
|
-
currentContent.style.transition='opacity 0.12s ease-out';
|
|
2787
|
-
currentContent.style.opacity='0';
|
|
2788
|
-
setTimeout(function(){
|
|
2789
|
-
completeSwap();
|
|
2790
|
-
currentContent.style.opacity='1';
|
|
2791
|
-
setTimeout(function(){currentContent.style.transition=''},150);
|
|
2792
|
-
},120);
|
|
2793
|
-
}
|
|
2794
|
-
});
|
|
2795
|
-
}
|
|
2796
|
-
var docCssReady=preloadGeneratedCss(docCssHrefs,docCssBase);
|
|
2797
|
-
return afterCss(docCssReady,url,startSwap);
|
|
2798
|
-
}
|
|
2799
|
-
|
|
2800
|
-
// \u2500\u2500 Screens \u2500\u2500
|
|
2801
|
-
// A phone app keeps the screens it has shown. Going back finds the list
|
|
2802
|
-
// exactly where it was left, and switching tabs finds the other tab as it
|
|
2803
|
-
// was: the same DOM, its scroll, its signals still live. Re-rendering each
|
|
2804
|
-
// of them from the server on every visit is what made an stx app read as a
|
|
2805
|
-
// web page in a shell.
|
|
2806
|
-
//
|
|
2807
|
-
// So with screens on, each page is written into a screen of its own,
|
|
2808
|
-
// <div data-stx-screen>, inside the routed container. Leaving one by a push
|
|
2809
|
-
// or a tab switch hides it instead of disposing it; Back and the tab bar
|
|
2810
|
-
// show it again without running anything. Each tab owns a stack of them,
|
|
2811
|
-
// mirrored by the history entries above the one the app opened with.
|
|
2812
|
-
//
|
|
2813
|
-
// The runtime binds whichever element carries data-stx-content before it
|
|
2814
|
-
// falls back to the router's container, so the screen being shown wears
|
|
2815
|
-
// that marker and a navigation binds the new screen alone, leaving the
|
|
2816
|
-
// retained ones as they are. A container that is itself [data-stx-content]
|
|
2817
|
-
// (the app-shell mode) keeps the old behaviour, as does a page without the
|
|
2818
|
-
// signals runtime, which has nothing to retain.
|
|
2819
|
-
//
|
|
2820
|
-
// On when the page has tab links (data-stx-nav="tab"), or always with
|
|
2821
|
-
// screens:true; never with screens:false.
|
|
2822
|
-
var SCREEN='data-stx-screen';
|
|
2823
|
-
var HIDDEN='data-stx-screen-hidden';
|
|
2824
|
-
var TAB_SEL='[data-stx-nav="tab"],[data-native-tab]';
|
|
2825
|
-
var tabs={};
|
|
2826
|
-
var curTab='';
|
|
2827
|
-
var active=null;
|
|
2828
|
-
var shownSeq=0;
|
|
2829
|
-
var screensLive=false;
|
|
2830
|
-
function screensOn(){
|
|
2831
|
-
if(screensLive)return true;
|
|
2832
|
-
if(o.screens===false||o.screens==='false'||!shouldUseFragmentResponse())return false;
|
|
2833
|
-
var stx=window.stx;
|
|
2834
|
-
if(!stx||typeof stx._cleanupContainer!=='function')return false;
|
|
2835
|
-
var host=getContainer();
|
|
2836
|
-
if(!host||(host.closest&&host.closest('[data-stx-content]')))return false;
|
|
2837
|
-
return o.screens===true||!!qs(TAB_SEL);
|
|
2838
|
-
}
|
|
2839
|
-
// Where the current page lives: its screen, or the container.
|
|
2840
|
-
function activeRoot(){return active&&active.el&&active.el.isConnected?active.el:getContainer()}
|
|
2841
|
-
// A selector looked up in the page on screen first: a retained screen
|
|
2842
|
-
// carries the same ids as the page that rendered it.
|
|
2843
|
-
function findIn(sel){
|
|
2844
|
-
var root=activeRoot(),el=null;
|
|
2845
|
-
try{el=root&&root.querySelector?root.querySelector(sel):null;if(!el)el=qs(sel)}catch(e){}
|
|
2846
|
-
return el;
|
|
2847
|
-
}
|
|
2848
|
-
function stackOf(id){return tabs[id]||(tabs[id]=[])}
|
|
2849
|
-
function newEntry(url,token,depth){return{url:url,key:cacheKey(url),token:token,depth:depth,tab:curTab,el:null,sx:0,sy:0,inner:null,destroys:[],cattrs:'',shown:0}}
|
|
2850
|
-
function tabId(href){return cacheKey(withCurrentLocale(href))}
|
|
2851
|
-
function tabLinks(){return qsa(TAB_SEL)}
|
|
2852
|
-
// The tab the page on screen belongs to: the one the tab bar has current,
|
|
2853
|
-
// which the sticky nav keeps lit for a screen opened from it.
|
|
2854
|
-
function currentTabId(){
|
|
2855
|
-
var links=tabLinks(),best='',i;
|
|
2856
|
-
for(i=0;i<links.length;i++)if(links[i].hasAttribute('data-stx-nav-current'))return tabId(links[i].getAttribute('href')||'');
|
|
2857
|
-
for(i=0;i<links.length;i++){
|
|
2858
|
-
var st=linkState(links[i].getAttribute('href')||'',links[i].getAttribute('data-stx-active-match'));
|
|
2859
|
-
if(st&&(st.exact||st.matched))return tabId(links[i].getAttribute('href')||'');
|
|
2860
|
-
if(st&&st.active&&!best)best=tabId(links[i].getAttribute('href')||'');
|
|
2861
|
-
}
|
|
2862
|
-
return best;
|
|
2863
|
-
}
|
|
2864
|
-
// Which tab link the bar shows as current. Set by hand on a switch: the
|
|
2865
|
-
// sticky nav would otherwise keep the old tab lit over the other tab's
|
|
2866
|
-
// pushed screen, since neither tab's own URL is on screen.
|
|
2867
|
-
function markTab(id){
|
|
2868
|
-
tabLinks().forEach(function(a){
|
|
2869
|
-
if(tabId(a.getAttribute('href')||'')===id)a.setAttribute('data-stx-nav-current','');
|
|
2870
|
-
else a.removeAttribute('data-stx-nav-current');
|
|
2871
|
-
});
|
|
2872
|
-
}
|
|
2873
|
-
function hostAttrString(host){
|
|
2874
|
-
var out=[];
|
|
2875
|
-
if(host&&host.getAttributeNames)host.getAttributeNames().forEach(function(n){
|
|
2876
|
-
if(n==='data-stx-cattrs'||n==='data-stx-route-focus'||n==='data-stx-animating'||(n==='tabindex'&&host.hasAttribute('data-stx-route-focus')))return;
|
|
2877
|
-
out.push(n+'="'+escAttr(host.getAttribute(n))+'"');
|
|
2878
|
-
});
|
|
2879
|
-
return encodeURIComponent(out.join(' '));
|
|
2880
|
-
}
|
|
2881
|
-
// The page the app opened with becomes the first screen the first time a
|
|
2882
|
-
// screen is needed. Moved, not re-rendered: its bindings stay on its nodes.
|
|
2883
|
-
function adoptInitial(host){
|
|
2884
|
-
if(active&&active.el&&active.el.isConnected)return;
|
|
2885
|
-
screensLive=true;
|
|
2886
|
-
var el=ce('div');
|
|
2887
|
-
el.setAttribute(SCREEN,'');
|
|
2888
|
-
while(host.firstChild)el.appendChild(host.firstChild);
|
|
2889
|
-
host.appendChild(el);
|
|
2890
|
-
if(host.__stx_disposers){el.__stx_disposers=host.__stx_disposers;host.__stx_disposers=null}
|
|
2891
|
-
if(!curTab)curTab=currentTabId();
|
|
2892
|
-
active=newEntry(location.pathname+location.search+location.hash,scrollToken,histDepth);
|
|
2893
|
-
active.el=el;
|
|
2894
|
-
active.cattrs=hostAttrString(host);
|
|
2895
|
-
active.shown=++shownSeq;
|
|
2896
|
-
stackOf(curTab)[histDepth]=active;
|
|
2897
|
-
markActive(el);
|
|
2898
|
-
}
|
|
2899
|
-
function markActive(el){
|
|
2900
|
-
var host=getContainer();
|
|
2901
|
-
if(host)Array.prototype.forEach.call(host.querySelectorAll('['+SCREEN+'][data-stx-content]'),function(other){if(other!==el)other.removeAttribute('data-stx-content')});
|
|
2902
|
-
el.setAttribute('data-stx-content','');
|
|
2903
|
-
el.removeAttribute(HIDDEN);
|
|
2904
|
-
}
|
|
2905
|
-
function saveScroll(e){
|
|
2906
|
-
e.sx=window.pageXOffset||window.scrollX||0;
|
|
2907
|
-
e.sy=window.pageYOffset||window.scrollY||0;
|
|
2908
|
-
e.inner=innerScroll(e.el);
|
|
2909
|
-
}
|
|
2910
|
-
// Everything that is global to the page on screen, kept with it while it is
|
|
2911
|
-
// hidden: its page-level onDestroy callbacks (the runtime runs whatever is
|
|
2912
|
-
// queued on the next stx:load, which would tear a retained page down), its
|
|
2913
|
-
// setup, its server data and its route params.
|
|
2914
|
-
function saveEntry(e){
|
|
2915
|
-
var stx=window.stx||{};
|
|
2916
|
-
e.title=document.title;
|
|
2917
|
-
e.setup=stx._latestSetup;
|
|
2918
|
-
e.data=window.__STX_DATA__;
|
|
2919
|
-
e.config=window.__STX_RUNTIME_CONFIG__;
|
|
2920
|
-
e.params=stx._rp||window.__stx_rp||{};
|
|
2921
|
-
if(stx._destroyCallbacks)e.destroys=e.destroys.concat(stx._destroyCallbacks.splice(0));
|
|
2922
|
-
}
|
|
2923
|
-
function restoreEntry(e){
|
|
2924
|
-
var stx=window.stx||{};
|
|
2925
|
-
if(e.title)document.title=e.title;
|
|
2926
|
-
if(e.setup)stx._latestSetup=e.setup;
|
|
2927
|
-
window.__STX_DATA__=e.data||{};
|
|
2928
|
-
window.__STX_RUNTIME_CONFIG__=e.config||{};
|
|
2929
|
-
if(stx._destroyCallbacks&&e.destroys.length)Array.prototype.push.apply(stx._destroyCallbacks,e.destroys);
|
|
2930
|
-
e.destroys=[];
|
|
2931
|
-
applyContainerAttrs(getContainer(),e.cattrs);
|
|
2932
|
-
}
|
|
2933
|
-
// A new page must not share the outgoing page's scope object, or its setup
|
|
2934
|
-
// merges into the one the retained screen's bindings read. Cleaning an
|
|
2935
|
-
// empty element resets that scope and nothing else.
|
|
2936
|
-
function resetScope(){
|
|
2937
|
-
var stx=window.stx;
|
|
2938
|
-
if(!stx||!stx._cleanupContainer)return;
|
|
2939
|
-
var keep=stx._mountCallbacks?stx._mountCallbacks.splice(0):[];
|
|
2940
|
-
stx._cleanupContainer(ce('div'));
|
|
2941
|
-
if(stx._mountCallbacks)Array.prototype.push.apply(stx._mountCallbacks,keep);
|
|
2942
|
-
}
|
|
2943
|
-
function hideScreen(e){
|
|
2944
|
-
if(!e||!e.el)return;
|
|
2945
|
-
unfreeze(e.el);
|
|
2946
|
-
e.el.setAttribute(HIDDEN,'');
|
|
2947
|
-
e.el.removeAttribute('data-stx-content');
|
|
2948
|
-
}
|
|
2949
|
-
// Disposal runs the same paths a navigation always has: the runtime's
|
|
2950
|
-
// container cleanup (effects, scopes, element hooks) and the page-level
|
|
2951
|
-
// onDestroy callbacks that were put aside when the screen was hidden.
|
|
2952
|
-
// Pending mount callbacks belong to whatever is arriving, so they survive.
|
|
2953
|
-
function disposeEntry(e){
|
|
2954
|
-
if(!e||!e.el)return;
|
|
2955
|
-
var el=e.el,stx=window.stx;
|
|
2956
|
-
e.el=null;
|
|
2957
|
-
if(e===active)saveEntry(e);
|
|
2958
|
-
if(stx&&stx._cleanupContainer){
|
|
2959
|
-
var keep=stx._mountCallbacks?stx._mountCallbacks.splice(0):[];
|
|
2960
|
-
try{stx._cleanupContainer(el)}catch(err){console.error('[router] screen cleanup failed:',err)}
|
|
2961
|
-
if(stx._mountCallbacks)Array.prototype.push.apply(stx._mountCallbacks,keep);
|
|
2962
|
-
}
|
|
2963
|
-
var fns=e.destroys;e.destroys=[];
|
|
2964
|
-
fns.forEach(function(fn){try{fn()}catch(err){console.warn('[stx] destroy callback error:',err)}});
|
|
2965
|
-
if(el.parentNode)el.parentNode.removeChild(el);
|
|
2966
|
-
}
|
|
2967
|
-
function eachEntry(fn){for(var id in tabs)tabs[id].forEach(function(e){if(e)fn(e,id)})}
|
|
2968
|
-
// Keep at most screenLimit screens alive, the least recently shown going
|
|
2969
|
-
// first. An evicted screen stays in its stack as an address: Back to it
|
|
2970
|
-
// loads it again.
|
|
2971
|
-
function enforceLimit(){
|
|
2972
|
-
var live=[];
|
|
2973
|
-
eachEntry(function(e){if(e.el&&e!==active&&!e.leaving)live.push(e)});
|
|
2974
|
-
live.sort(function(a,b){return a.shown-b.shown});
|
|
2975
|
-
while(live.length+1>Math.max(1,o.screenLimit|0))disposeEntry(live.shift());
|
|
2976
|
-
}
|
|
2977
|
-
// Everything back to the plain container: before a whole-document swap and
|
|
2978
|
-
// when the screens are given up.
|
|
2979
|
-
// The screen on screen is left in place: the swap cleans the container,
|
|
2980
|
-
// and it with it, as it always has.
|
|
2981
|
-
function collapseScreens(){
|
|
2982
|
-
if(!screensLive)return;
|
|
2983
|
-
settleAnimation();
|
|
2984
|
-
eachEntry(function(e){if(e!==active)disposeEntry(e)});
|
|
2985
|
-
tabs={};active=null;screensLive=false;
|
|
2986
|
-
}
|
|
2987
|
-
// Component scopes are keyed by ids the server derives from the page, so a
|
|
2988
|
-
// second visit to the same page, pushed over a retained first one, brings
|
|
2989
|
-
// the same ids. The retained copies are renamed out of the way, registry
|
|
2990
|
-
// entry and all, so each instance keeps its own and disposing one cannot
|
|
2991
|
-
// reach the other.
|
|
2992
|
-
var retagSeq=0;
|
|
2993
|
-
function retagCollisions(html){
|
|
2994
|
-
var ids={},any=false;
|
|
2995
|
-
html.replace(/data-stx-scope="([^"]+)"/g,function(m,id){ids[id]=1;any=true;return m});
|
|
2996
|
-
if(!any)return;
|
|
2997
|
-
var scopes=window.stx&&window.stx._scopes;
|
|
2998
|
-
eachEntry(function(e){
|
|
2999
|
-
if(!e.el)return;
|
|
3000
|
-
Array.prototype.forEach.call(e.el.querySelectorAll('[data-stx-scope]'),function(node){
|
|
3001
|
-
var id=node.getAttribute('data-stx-scope');
|
|
3002
|
-
if(!ids[id])return;
|
|
3003
|
-
var next=id+'-r'+(++retagSeq);
|
|
3004
|
-
node.setAttribute('data-stx-scope',next);
|
|
3005
|
-
if(scopes&&scopes[id]&&(!scopes[id].__el||scopes[id].__el===node)){scopes[next]=scopes[id];delete scopes[id]}
|
|
3006
|
-
});
|
|
3007
|
-
});
|
|
3008
|
-
}
|
|
3009
|
-
|
|
3010
|
-
// The swap's half: before the page is written, the outgoing screen is put
|
|
3011
|
-
// aside (lifted out of the page for a slide, or hidden), and a new screen
|
|
3012
|
-
// is made for the page to go into, first in the container so that a lookup
|
|
3013
|
-
// by id finds the page on screen before any retained copy of it.
|
|
3014
|
-
function screenBegin(host,ctx,html,writes){
|
|
3015
|
-
adoptInitial(host);
|
|
3016
|
-
var from=active;
|
|
3017
|
-
retagCollisions(html);
|
|
3018
|
-
if(from&&from.el){
|
|
3019
|
-
// A popstate saved the outgoing position already, under the token it
|
|
3020
|
-
// has since replaced; saving again here would file it under the wrong one.
|
|
3021
|
-
if(writes){rememberScroll();scrollSaved=true}
|
|
3022
|
-
saveEntry(from);
|
|
3023
|
-
}
|
|
3024
|
-
resetScope();
|
|
3025
|
-
var el=ce('div');
|
|
3026
|
-
el.setAttribute(SCREEN,'');
|
|
3027
|
-
var anim=!!(from&&from.el&&canAnimate(ctx.direction));
|
|
3028
|
-
if(anim)freeze(from.el);
|
|
3029
|
-
else if(from&&from.el)hideScreen(from);
|
|
3030
|
-
host.insertBefore(el,host.firstChild);
|
|
3031
|
-
markActive(el);
|
|
3032
|
-
return{el:el,from:from,ctx:ctx,anim:anim,cattrs:''};
|
|
3033
|
-
}
|
|
3034
|
-
// After the history write, so the entry knows its token and depth.
|
|
3035
|
-
function screenCommit(scr,href){
|
|
3036
|
-
var e=scr.ctx.entry||newEntry(href,scrollToken,histDepth);
|
|
3037
|
-
e.url=href;e.key=cacheKey(href);e.token=scrollToken;e.depth=histDepth;e.tab=curTab;
|
|
3038
|
-
e.el=scr.el;e.cattrs=scr.cattrs;e.shown=++shownSeq;e.destroys=[];
|
|
3039
|
-
var stack=stackOf(curTab);
|
|
3040
|
-
// A push drops whatever was forward of it; a pop or a replacement takes
|
|
3041
|
-
// the place of what was at its depth. The outgoing screen is left to the
|
|
3042
|
-
// transition, which disposes it once it is off screen.
|
|
3043
|
-
for(var i=e.depth;i<stack.length;i++){var x=stack[i];if(x&&x!==e&&x!==scr.from)disposeEntry(x)}
|
|
3044
|
-
stack.length=e.depth;
|
|
3045
|
-
stack[e.depth]=e;
|
|
3046
|
-
active=e;
|
|
3047
|
-
scrollSaved=false;
|
|
3048
|
-
}
|
|
3049
|
-
function screenEnd(scr){
|
|
3050
|
-
var from=scr.from,dir=scr.ctx.direction;
|
|
3051
|
-
if(!from||!from.el){finishScreen();return}
|
|
3052
|
-
// Retained: left by a push, or by a tab switch. Popped or replaced: gone.
|
|
3053
|
-
var keep=dir==='push'||dir==='tab';
|
|
3054
|
-
var stack=tabs[from.tab];
|
|
3055
|
-
if(keep&&!(stack&&stack[from.depth]===from))keep=false;
|
|
3056
|
-
from.leaving=true;
|
|
3057
|
-
var after=function(){
|
|
3058
|
-
from.leaving=false;
|
|
3059
|
-
if(keep&&from.el)hideScreen(from);
|
|
3060
|
-
else disposeEntry(from);
|
|
3061
|
-
finishScreen();
|
|
3062
|
-
};
|
|
3063
|
-
if(scr.anim)animateScreens(from.el,scr.el,dir,after);
|
|
3064
|
-
else after();
|
|
3065
|
-
}
|
|
3066
|
-
function finishScreen(){
|
|
3067
|
-
enforceLimit();
|
|
3068
|
-
observeLinks();
|
|
3069
|
-
}
|
|
3070
|
-
|
|
3071
|
-
// Show a screen that is already here: Back to one below in the stack, or a
|
|
3072
|
-
// tab's top screen. Nothing is fetched and no page script runs; the page is
|
|
3073
|
-
// told it is on screen again (stx:screen-shown) so it can refresh whatever
|
|
3074
|
-
// may have gone stale while it was hidden.
|
|
3075
|
-
function showRetained(to,dir,opts){
|
|
3076
|
-
opts=opts||{};
|
|
3077
|
-
settleAnimation();
|
|
3078
|
-
var from=active,host=getContainer();
|
|
3079
|
-
if(navAbort){try{navAbort.abort()}catch(e){}navAbort=null}
|
|
3080
|
-
navSeq++;
|
|
3081
|
-
isNavigating=false;
|
|
3082
|
-
document.body.classList.remove(o.loadingClass);
|
|
3083
|
-
if(from&&from.el){if(!opts.settled)saveScroll(from);saveEntry(from)}
|
|
3084
|
-
setDirection(dir);
|
|
3085
|
-
var anim=!opts.settled&&from&&from.el&&from!==to&&canAnimate(dir,true);
|
|
3086
|
-
if(anim)freeze(from.el);
|
|
3087
|
-
else if(from&&from.el&&from!==to&&!opts.settled)hideScreen(from);
|
|
3088
|
-
unfreeze(to.el);
|
|
3089
|
-
markActive(to.el);
|
|
3090
|
-
active=to;
|
|
3091
|
-
to.shown=++shownSeq;
|
|
3092
|
-
restoreEntry(to);
|
|
3093
|
-
pendingScroll=null;pendingInner=null;
|
|
3094
|
-
window.scrollTo({left:to.sx,top:to.sy,behavior:'instant'});
|
|
3095
|
-
applyInner(to.el,to.inner);
|
|
3096
|
-
if(opts.settled&&from&&from!==to&&from.el)hideScreen(from);
|
|
3097
|
-
refreshCurrentLinks();
|
|
3098
|
-
window.dispatchEvent(new CustomEvent('stx:navigate',{detail:{url:to.url,direction:dir,retained:true,params:to.params||{}}}));
|
|
3099
|
-
focusAfterNavigation('');
|
|
3100
|
-
setTimeout(announceRoute,0);
|
|
3101
|
-
window.dispatchEvent(new CustomEvent('stx:screen-shown',{detail:{url:to.url,direction:dir}}));
|
|
3102
|
-
revalidate(to.key,to.url);
|
|
3103
|
-
var keep=dir==='push'||dir==='tab';
|
|
3104
|
-
var after=function(){
|
|
3105
|
-
if(from&&from!==to){
|
|
3106
|
-
if(dir==='pop'){
|
|
3107
|
-
// Everything above the screen shown is gone from the stack.
|
|
3108
|
-
var stack=tabs[to.tab]||[];
|
|
3109
|
-
for(var i=to.depth+1;i<stack.length;i++)if(stack[i]&&stack[i]!==from)disposeEntry(stack[i]);
|
|
3110
|
-
stack.length=to.depth+1;
|
|
3111
|
-
}
|
|
3112
|
-
if(keep&&from.el)hideScreen(from);
|
|
3113
|
-
else if(!keep)disposeEntry(from);
|
|
3114
|
-
}
|
|
3115
|
-
finishScreen();
|
|
3116
|
-
settleDirection();
|
|
3117
|
-
};
|
|
3118
|
-
if(anim){from.leaving=true;animateScreens(from.el,to.el,dir,function(){from.leaving=false;after()})}
|
|
3119
|
-
else after();
|
|
3120
|
-
return true;
|
|
3121
|
-
}
|
|
3122
|
-
|
|
3123
|
-
// \u2500\u2500 Direction and motion \u2500\u2500
|
|
3124
|
-
// data-nav-direction on <html> says what kind of move is under way --
|
|
3125
|
-
// push, pop, tab or replace -- for as long as it is, so CSS (and a page's
|
|
3126
|
-
// own transitions) can tell a step forward from a step back.
|
|
3127
|
-
var animEnd=null;
|
|
3128
|
-
function setDirection(d){if(d)document.documentElement.setAttribute('data-nav-direction',d)}
|
|
3129
|
-
function settleDirection(){
|
|
3130
|
-
if(isNavigating||animEnd)return;
|
|
3131
|
-
document.documentElement.removeAttribute('data-nav-direction');
|
|
3132
|
-
}
|
|
3133
|
-
function reducedMotion(){
|
|
3134
|
-
try{return !!(window.matchMedia&&window.matchMedia('(prefers-reduced-motion: reduce)').matches)}catch(e){return false}
|
|
3135
|
-
}
|
|
3136
|
-
// instantNav belongs to a navigation that fetched; a screen shown from
|
|
3137
|
-
// the stack (Back) is not one, and must not inherit the last tab tap's.
|
|
3138
|
-
function canAnimate(dir,shown){
|
|
3139
|
-
return (dir==='push'||dir==='pop')&&(shown||!instantNav)&&o.navDuration>0&&typeof window.getComputedStyle==='function';
|
|
3140
|
-
}
|
|
3141
|
-
// The first background behind the container that is not transparent: the
|
|
3142
|
-
// screens are transparent, and two of them sliding over each other need
|
|
3143
|
-
// the page's colour to hide the one underneath.
|
|
3144
|
-
function screenBackground(host){
|
|
3145
|
-
for(var n=host;n&&n.nodeType===1;n=n.parentNode){
|
|
3146
|
-
var c='';
|
|
3147
|
-
try{c=getComputedStyle(n).backgroundColor}catch(e){}
|
|
3148
|
-
if(c&&c!=='transparent'&&c!=='rgba(0, 0, 0, 0)')return c;
|
|
3149
|
-
}
|
|
3150
|
-
return '';
|
|
3151
|
-
}
|
|
3152
|
-
// Lifted out of the flow and pinned where it is on screen, as a box the
|
|
3153
|
-
// height of the viewport scrolled to the same place: the page can scroll
|
|
3154
|
-
// to wherever the next screen wants, the outgoing one does not move, and a
|
|
3155
|
-
// sticky header inside it stays stuck.
|
|
3156
|
-
function freeze(el,top){
|
|
3157
|
-
var r=el.getBoundingClientRect();
|
|
3158
|
-
var vh=window.innerHeight||document.documentElement.clientHeight||0;
|
|
3159
|
-
var at=top===undefined?r.top:top;
|
|
3160
|
-
var y=Math.max(at,0);
|
|
3161
|
-
var s=el.style;
|
|
3162
|
-
s.position='fixed';s.top=y+'px';s.left=r.left+'px';s.width=r.width+'px';s.height=Math.max(vh-y,0)+'px';s.overflow='hidden';s.margin='0';
|
|
3163
|
-
el.scrollTop=y-at;
|
|
3164
|
-
}
|
|
3165
|
-
function unfreeze(el){
|
|
3166
|
-
if(!el)return;
|
|
3167
|
-
var s=el.style;
|
|
3168
|
-
['position','top','left','width','height','overflow','margin','transform','transition','zIndex','backgroundColor','pointerEvents'].forEach(function(k){s[k]=''});
|
|
3169
|
-
el.removeAttribute('data-stx-screen-enter');
|
|
3170
|
-
el.removeAttribute('data-stx-screen-leave');
|
|
3171
|
-
el.scrollTop=0;
|
|
3172
|
-
}
|
|
3173
|
-
function parseDuration(v){
|
|
3174
|
-
v=String(v||'').split(',')[0].trim();
|
|
3175
|
-
var n=parseFloat(v);
|
|
3176
|
-
if(!(n>=0))return -1;
|
|
3177
|
-
return v.indexOf('ms')>0?n:n*1000;
|
|
3178
|
-
}
|
|
3179
|
-
function dimLayer(host){
|
|
3180
|
-
var r=host.getBoundingClientRect();
|
|
3181
|
-
var dim=ce('div');
|
|
3182
|
-
dim.setAttribute('data-stx-screen-dim','');
|
|
3183
|
-
dim.style.left=r.left+'px';
|
|
3184
|
-
dim.style.width=r.width+'px';
|
|
3185
|
-
return dim;
|
|
3186
|
-
}
|
|
3187
|
-
// Two real screens, animated by the shipped CSS: the one arriving and the
|
|
3188
|
-
// one leaving, and a dim between them over the one underneath.
|
|
3189
|
-
function animateScreens(fromEl,toEl,dir,done){
|
|
3190
|
-
var host=getContainer();
|
|
3191
|
-
var bg=screenBackground(host);
|
|
3192
|
-
var dim=dimLayer(host);
|
|
3193
|
-
host.setAttribute('data-stx-animating','');
|
|
3194
|
-
fromEl.style.backgroundColor=bg;toEl.style.backgroundColor=bg;
|
|
3195
|
-
fromEl.style.pointerEvents='none';
|
|
3196
|
-
fromEl.setAttribute('data-stx-screen-leave','');
|
|
3197
|
-
toEl.setAttribute('data-stx-screen-enter','');
|
|
3198
|
-
host.appendChild(dim);
|
|
3199
|
-
var ms=-1;
|
|
3200
|
-
try{ms=parseDuration(getComputedStyle(toEl).animationDuration)}catch(e){}
|
|
3201
|
-
if(!(ms>0))ms=o.navDuration;
|
|
3202
|
-
var timer=setTimeout(end,ms+120);
|
|
3203
|
-
function onEnd(e){if(e.target===toEl)end()}
|
|
3204
|
-
toEl.addEventListener('animationend',onEnd);
|
|
3205
|
-
function end(){
|
|
3206
|
-
if(animEnd!==end)return;
|
|
3207
|
-
animEnd=null;
|
|
3208
|
-
clearTimeout(timer);
|
|
3209
|
-
toEl.removeEventListener('animationend',onEnd);
|
|
3210
|
-
if(dim.parentNode)dim.parentNode.removeChild(dim);
|
|
3211
|
-
host.removeAttribute('data-stx-animating');
|
|
3212
|
-
toEl.removeAttribute('data-stx-screen-enter');
|
|
3213
|
-
toEl.style.backgroundColor='';
|
|
3214
|
-
done();
|
|
3215
|
-
settleDirection();
|
|
3216
|
-
}
|
|
3217
|
-
animEnd=end;
|
|
3218
|
-
}
|
|
3219
|
-
// A navigation that starts while a slide is still running finishes the
|
|
3220
|
-
// slide first, rather than lifting a screen that is halfway across.
|
|
3221
|
-
function settleAnimation(){if(animEnd)animEnd()}
|
|
3222
|
-
|
|
3223
|
-
// \u2500\u2500 Tabs \u2500\u2500
|
|
3224
|
-
// Back never crosses tabs: the history above the entry the app opened with
|
|
3225
|
-
// is always the current tab's stack. A switch rewinds to that entry, writes
|
|
3226
|
-
// the target tab's stack in its place, and shows its top screen exactly as
|
|
3227
|
-
// it was left.
|
|
3228
|
-
var rewinding=null;
|
|
3229
|
-
function rewind(){
|
|
3230
|
-
if(histDepth<=0)return Promise.resolve();
|
|
3231
|
-
return new Promise(function(resolve){
|
|
3232
|
-
var n=histDepth;
|
|
3233
|
-
var timer=setTimeout(finish,700);
|
|
3234
|
-
function finish(){
|
|
3235
|
-
clearTimeout(timer);
|
|
3236
|
-
if(rewinding===finish)rewinding=null;
|
|
3237
|
-
histDepth=(history.state&&history.state[DEPTH])|0;
|
|
3238
|
-
var token=history.state&&history.state[SCROLL_TOKEN];
|
|
3239
|
-
if(token)scrollToken=token;
|
|
3240
|
-
resolve();
|
|
3241
|
-
}
|
|
3242
|
-
rewinding=finish;
|
|
3243
|
-
history.go(-n);
|
|
3244
|
-
});
|
|
3245
|
-
}
|
|
3246
|
-
function entryState(e,pushed){
|
|
3247
|
-
var st={};
|
|
3248
|
-
st[SCROLL_TOKEN]=e.token;st[DEPTH]=e.depth;st[TAB]=e.tab;
|
|
3249
|
-
if(pushed)st[PUSHED_MARK]=true;
|
|
3250
|
-
return st;
|
|
3251
|
-
}
|
|
3252
|
-
function selectTab(href){
|
|
3253
|
-
if(!href)return Promise.resolve(false);
|
|
3254
|
-
var id=tabId(href);
|
|
3255
|
-
var on=screensOn();
|
|
3256
|
-
if(on&&!screensLive)adoptInitial(getContainer());
|
|
3257
|
-
if(!curTab)curTab=on&&active?active.tab:currentTabId();
|
|
3258
|
-
if(id===curTab)return reselectTab(href);
|
|
3259
|
-
if(active&&active.el){rememberScroll();saveScroll(active)}
|
|
3260
|
-
settleAnimation();
|
|
3261
|
-
if(navAbort){try{navAbort.abort()}catch(e){}navAbort=null}
|
|
3262
|
-
var seq=++navSeq;
|
|
3263
|
-
return rewind().then(function(){
|
|
3264
|
-
if(seq!==navSeq)return false;
|
|
3265
|
-
curTab=id;
|
|
3266
|
-
markTab(id);
|
|
3267
|
-
var stack=on?(tabs[id]||[]).filter(Boolean):[];
|
|
3268
|
-
if(!stack.length){
|
|
3269
|
-
if(on)tabs[id]=[];
|
|
3270
|
-
navCtxNext={direction:'tab'};
|
|
3271
|
-
instantNext=true;
|
|
3272
|
-
return navigate(href,'tab');
|
|
3273
|
-
}
|
|
3274
|
-
// Rewritten in order, so Back walks this tab's own stack.
|
|
3275
|
-
stack.forEach(function(e,i){
|
|
3276
|
-
e.depth=i;e.tab=id;
|
|
3277
|
-
if(i===0)history.replaceState(entryState(e,false),'',e.url);
|
|
3278
|
-
else history.pushState(entryState(e,true),'',e.url);
|
|
3279
|
-
});
|
|
3280
|
-
tabs[id]=stack;
|
|
3281
|
-
var top=stack[stack.length-1];
|
|
3282
|
-
histDepth=top.depth;
|
|
3283
|
-
scrollToken=top.token;
|
|
3284
|
-
if(top.el)return showRetained(top,'tab');
|
|
3285
|
-
// Its top screen was let go: load it into the same entry.
|
|
3286
|
-
pendingScroll=[top.sx,top.sy];
|
|
3287
|
-
navCtxNext={direction:'tab',entry:top};
|
|
3288
|
-
instantNext=true;
|
|
3289
|
-
return navigate(top.url,false);
|
|
3290
|
-
});
|
|
3291
|
-
}
|
|
3292
|
-
// The tab that is already current, selected again: back to the top of its
|
|
3293
|
-
// screen, and when it is there already, back to the tab's first screen.
|
|
3294
|
-
// stx:tabreselect says which, and a page that cancels it keeps its own.
|
|
3295
|
-
function reselectTab(href){
|
|
3296
|
-
var root=activeRoot();
|
|
3297
|
-
var y=window.pageYOffset||window.scrollY||0;
|
|
3298
|
-
var inner=innerScroll(root);
|
|
3299
|
-
var atRoot=active?active.depth===0&&active.key===tabId(href):cacheKey(location.href)===tabId(href);
|
|
3300
|
-
var action=(y>0||inner)?'top':atRoot?'none':'root';
|
|
3301
|
-
var ev=new CustomEvent('stx:tabreselect',{cancelable:true,detail:{href:href,action:action}});
|
|
3302
|
-
window.dispatchEvent(ev);
|
|
3303
|
-
if(ev.defaultPrevented||action==='none')return Promise.resolve(false);
|
|
3304
|
-
if(action==='top'){
|
|
3305
|
-
window.scrollTo({top:0,left:0,behavior:reducedMotion()?'instant':'smooth'});
|
|
3306
|
-
if(root&&root.querySelectorAll)root.querySelectorAll('[data-stx-scroll]').forEach(function(el){if(el.scrollTo)el.scrollTo({top:0,left:0,behavior:'smooth'});else el.scrollTop=0});
|
|
3307
|
-
return Promise.resolve(true);
|
|
3308
|
-
}
|
|
3309
|
-
var stack=tabs[curTab]||[];
|
|
3310
|
-
var first=stack.filter(Boolean)[0];
|
|
3311
|
-
if(first&&first.el&&first.key===tabId(href)&&histDepth>0){
|
|
3312
|
-
var target=first;
|
|
3313
|
-
return new Promise(function(resolve){
|
|
3314
|
-
history.go(target.depth-histDepth);
|
|
3315
|
-
setTimeout(function(){resolve(true)},0);
|
|
3316
|
-
});
|
|
3317
|
-
}
|
|
3318
|
-
// The tab's first screen is not here (opened on a deep link, or let
|
|
3319
|
-
// go): load the tab's own page in its place.
|
|
3320
|
-
return rewind().then(function(){
|
|
3321
|
-
navCtxNext={direction:'pop'};
|
|
3322
|
-
return navigate(href,'tab');
|
|
3323
|
-
});
|
|
3324
|
-
}
|
|
3325
|
-
|
|
3326
|
-
// \u2500\u2500 Swipe back \u2500\u2500
|
|
3327
|
-
// A pan from the left edge drags the screen on top away and shows the
|
|
3328
|
-
// retained one underneath, the way iOS does: the one underneath moves from
|
|
3329
|
-
// -30% and brightens as the top one goes. Released past half way, or
|
|
3330
|
-
// flicked, it completes as a Back (history.back(), shown without a second
|
|
3331
|
-
// animation); otherwise it springs back. Only where there is a screen to
|
|
3332
|
-
// go back to in this tab, never from inside data-stx-no-swipe, and never
|
|
3333
|
-
// over something that pans sideways on its own (touch-action none, pan-x,
|
|
3334
|
-
// pan-y, or a horizontal scroller that is not at its start).
|
|
3335
|
-
var swipe=null;
|
|
3336
|
-
var swipeSettled=null;
|
|
3337
|
-
function swipePrev(){
|
|
3338
|
-
if(o.swipeBack===false||!screensLive||!active||!active.el||isNavigating||animEnd)return null;
|
|
3339
|
-
var prev=(tabs[curTab]||[])[active.depth-1];
|
|
3340
|
-
return prev&&prev.el?prev:null;
|
|
3341
|
-
}
|
|
3342
|
-
function swipeBlocked(target){
|
|
3343
|
-
var host=getContainer();
|
|
3344
|
-
for(var n=target;n&&n.nodeType===1&&n!==host;n=n.parentNode){
|
|
3345
|
-
if(n.hasAttribute('data-stx-no-swipe'))return true;
|
|
3346
|
-
var cs=null;
|
|
3347
|
-
try{cs=getComputedStyle(n)}catch(e){}
|
|
3348
|
-
if(!cs)continue;
|
|
3349
|
-
var ta=String(cs.touchAction||'').split(' ');
|
|
3350
|
-
var has=function(v){return ta.indexOf(v)!==-1};
|
|
3351
|
-
if(has('none')||(has('pan-x')||has('pan-left')||has('pan-right'))!==(has('pan-y')||has('pan-up')||has('pan-down')))return true;
|
|
3352
|
-
var ox=String(cs.overflowX||'');
|
|
3353
|
-
if((ox==='auto'||ox==='scroll')&&n.scrollWidth>n.clientWidth&&n.scrollLeft>0)return true;
|
|
3354
|
-
}
|
|
3355
|
-
return false;
|
|
3356
|
-
}
|
|
3357
|
-
function now(){return window.performance&&performance.now?performance.now():Date.now()}
|
|
3358
|
-
function touchOf(e){return (e.touches&&e.touches[0])||(e.changedTouches&&e.changedTouches[0])||null}
|
|
3359
|
-
function swipeStart(e){
|
|
3360
|
-
if(swipe||!e.touches||e.touches.length!==1)return;
|
|
3361
|
-
var t=e.touches[0];
|
|
3362
|
-
if(t.clientX>o.swipeEdge)return;
|
|
3363
|
-
var prev=swipePrev();
|
|
3364
|
-
if(!prev||swipeBlocked(e.target))return;
|
|
3365
|
-
swipe={prev:prev,x0:t.clientX,y0:t.clientY,x:t.clientX,t:now(),v:0,on:false};
|
|
3366
|
-
}
|
|
3367
|
-
function swipeBegin(){
|
|
3368
|
-
var cur=active,prev=swipe.prev,host=getContainer();
|
|
3369
|
-
var w=host.getBoundingClientRect().width||window.innerWidth||1;
|
|
3370
|
-
saveScroll(cur);
|
|
3371
|
-
var docTop=cur.el.getBoundingClientRect().top+(window.pageYOffset||window.scrollY||0);
|
|
3372
|
-
swipe.w=w;
|
|
3373
|
-
swipe.cur=cur;
|
|
3374
|
-
swipe.bg=screenBackground(host);
|
|
3375
|
-
swipe.dim=dimLayer(host);
|
|
3376
|
-
swipe.dim.setAttribute('data-stx-drag','');
|
|
3377
|
-
// Shown before it is measured: a hidden screen has no box to pin.
|
|
3378
|
-
prev.el.removeAttribute(HIDDEN);
|
|
3379
|
-
freeze(prev.el,docTop-prev.sy);
|
|
3380
|
-
applyInner(prev.el,prev.inner);
|
|
3381
|
-
prev.el.style.zIndex='1';
|
|
3382
|
-
prev.el.style.backgroundColor=swipe.bg;
|
|
3383
|
-
cur.el.style.position='relative';
|
|
3384
|
-
cur.el.style.zIndex='3';
|
|
3385
|
-
cur.el.style.backgroundColor=swipe.bg;
|
|
3386
|
-
host.setAttribute('data-stx-animating','');
|
|
3387
|
-
host.appendChild(swipe.dim);
|
|
3388
|
-
setDirection('pop');
|
|
3389
|
-
swipe.on=true;
|
|
3390
|
-
}
|
|
3391
|
-
function swipeDraw(dx){
|
|
3392
|
-
var p=Math.min(Math.max(dx/swipe.w,0),1);
|
|
3393
|
-
swipe.p=p;
|
|
3394
|
-
swipe.cur.el.style.transform='translateX('+(p*swipe.w)+'px)';
|
|
3395
|
-
swipe.prev.el.style.transform='translateX('+(-0.3*swipe.w*(1-p))+'px)';
|
|
3396
|
-
swipe.dim.style.opacity=String(1-p);
|
|
3397
|
-
}
|
|
3398
|
-
function swipeMove(e){
|
|
3399
|
-
if(!swipe)return;
|
|
3400
|
-
var t=touchOf(e);
|
|
3401
|
-
if(!t)return;
|
|
3402
|
-
var dx=t.clientX-swipe.x0,dy=t.clientY-swipe.y0;
|
|
3403
|
-
if(!swipe.on){
|
|
3404
|
-
if(Math.abs(dx)<8&&Math.abs(dy)<8)return;
|
|
3405
|
-
// Vertical first, or leftwards: a scroll, not a Back.
|
|
3406
|
-
if(dx<=0||Math.abs(dy)>=Math.abs(dx)){swipe=null;return}
|
|
3407
|
-
swipeBegin();
|
|
3408
|
-
}
|
|
3409
|
-
if(e.cancelable)e.preventDefault();
|
|
3410
|
-
var n=now(),dt=n-swipe.t;
|
|
3411
|
-
if(dt>0)swipe.v=swipe.v*0.2+((t.clientX-swipe.x)/dt)*0.8;
|
|
3412
|
-
swipe.x=t.clientX;swipe.t=n;
|
|
3413
|
-
swipeDraw(dx);
|
|
3414
|
-
}
|
|
3415
|
-
function swipeEnd(e){
|
|
3416
|
-
if(!swipe)return;
|
|
3417
|
-
var s=swipe;
|
|
3418
|
-
swipe=null;
|
|
3419
|
-
if(!s.on)return;
|
|
3420
|
-
// A finger held still before lifting has no speed left.
|
|
3421
|
-
if(now()-s.t>80)s.v=0;
|
|
3422
|
-
var commit=e.type!=='touchcancel'&&(s.p>0.5||s.v>0.5);
|
|
3423
|
-
var remaining=commit?(1-s.p)*s.w:s.p*s.w;
|
|
3424
|
-
var ms=Math.round(Math.min(o.navDuration,Math.max(120,remaining/Math.max(Math.abs(s.v),0.6))));
|
|
3425
|
-
var ease='cubic-bezier(0.2, 0.9, 0.3, 1)';
|
|
3426
|
-
[s.cur.el,s.prev.el].forEach(function(el){el.style.transition='transform '+ms+'ms '+ease});
|
|
3427
|
-
s.dim.style.transition='opacity '+ms+'ms '+ease;
|
|
3428
|
-
s.cur.el.style.transform=commit?'translateX('+s.w+'px)':'translateX(0px)';
|
|
3429
|
-
s.prev.el.style.transform=commit?'translateX(0px)':'translateX('+(-0.3*s.w)+'px)';
|
|
3430
|
-
s.dim.style.opacity=commit?'0':'1';
|
|
3431
|
-
var finished=false;
|
|
3432
|
-
var finish=function(){
|
|
3433
|
-
if(finished)return;
|
|
3434
|
-
finished=true;
|
|
3435
|
-
animEnd=null;
|
|
3436
|
-
if(s.dim.parentNode)s.dim.parentNode.removeChild(s.dim);
|
|
3437
|
-
getContainer().removeAttribute('data-stx-animating');
|
|
3438
|
-
if(commit){
|
|
3439
|
-
// Already where Back would put it: the popstate that follows shows
|
|
3440
|
-
// the screen underneath as it is, with no second animation.
|
|
3441
|
-
s.cur.el.style.visibility='hidden';
|
|
3442
|
-
swipeSettled=s.prev;
|
|
3443
|
-
history.back();
|
|
3444
|
-
}
|
|
3445
|
-
else{
|
|
3446
|
-
hideScreen(s.prev);
|
|
3447
|
-
s.cur.el.style.transform='';s.cur.el.style.transition='';s.cur.el.style.position='';s.cur.el.style.zIndex='';s.cur.el.style.backgroundColor='';
|
|
3448
|
-
settleDirection();
|
|
3449
|
-
}
|
|
3450
|
-
};
|
|
3451
|
-
animEnd=finish;
|
|
3452
|
-
setTimeout(finish,ms+40);
|
|
3453
|
-
}
|
|
3454
|
-
document.addEventListener('touchstart',swipeStart,{capture:true,passive:true});
|
|
3455
|
-
document.addEventListener('touchmove',swipeMove,{capture:true,passive:false});
|
|
3456
|
-
document.addEventListener('touchend',swipeEnd,{capture:true,passive:true});
|
|
3457
|
-
document.addEventListener('touchcancel',swipeEnd,{capture:true,passive:true});
|
|
3458
|
-
// The system is short of memory: let go of every hidden screen except the
|
|
3459
|
-
// one a swipe back would show.
|
|
3460
|
-
window.addEventListener('craftMemoryWarning',function(){
|
|
3461
|
-
var keep=active?(tabs[curTab]||[])[active.depth-1]:null;
|
|
3462
|
-
eachEntry(function(e){if(e!==active&&e!==keep&&!e.leaving)disposeEntry(e)});
|
|
3463
|
-
});
|
|
3464
|
-
|
|
3465
|
-
// \u2500\u2500 Revalidation and updates \u2500\u2500
|
|
3466
|
-
// A page served from the router's cache, or a screen shown again, is asked
|
|
3467
|
-
// for once more behind the swap. If it changed, the cache takes the new copy
|
|
3468
|
-
// and stx:updated says so, so the page can refresh what it shows; the
|
|
3469
|
-
// screen is never swapped out from under the user.
|
|
3470
|
-
function revalidate(key,url){
|
|
3471
|
-
if(!o.revalidate||!o.cache||!key||prefetching[key])return;
|
|
3472
|
-
if(Date.now()-(cacheAt[key]||0)<o.revalidateAfter)return;
|
|
3473
|
-
var wantsFragment=shouldUseFragmentResponse();
|
|
3474
|
-
var before=cache[key];
|
|
3475
|
-
prefetching[key]=fetch(url,{headers:wantsFragment?{'X-STX-Router':'true','Accept':'text/html'}:{'Accept':'text/html'}}).then(function(r){
|
|
3476
|
-
var incoming=r.headers.get('X-STX-Build')||'';
|
|
3477
|
-
if(isBuildSkew(incoming)){reloadForSkew(url,incoming,true);return null}
|
|
3478
|
-
return readPrefetchResponse(r,wantsFragment);
|
|
3479
|
-
}).then(function(result){
|
|
3480
|
-
if(!result)return;
|
|
3481
|
-
if(o.cache)setCache(key,result.html,result.layout,result.layoutGroup,result.title,result.containerAttrs);
|
|
3482
|
-
if(before!==undefined&&before!==result.html&&cacheKey(location.href)===key)emitUpdated(url,'page');
|
|
3483
|
-
}).catch(function(){}).finally(function(){delete prefetching[key]});
|
|
3484
|
-
}
|
|
3485
|
-
// The offline worker answered from its cache and found something newer
|
|
3486
|
-
// behind it. It posts { type: 'stx:updated', url, kind }, which its own
|
|
3487
|
-
// register script turns into a window event; the router does that only
|
|
3488
|
-
// where that script is not on the page, so the event is heard once. Either
|
|
3489
|
-
// way a page the router has cached is dropped, so the next visit gets the
|
|
3490
|
-
// new one.
|
|
3491
|
-
var ownUpdate=false;
|
|
3492
|
-
function emitUpdated(url,kind){
|
|
3493
|
-
ownUpdate=true;
|
|
3494
|
-
try{window.dispatchEvent(new CustomEvent('stx:updated',{detail:{url:url,kind:kind||'page'}}))}finally{ownUpdate=false}
|
|
3495
|
-
}
|
|
3496
|
-
window.addEventListener('stx:updated',function(e){
|
|
3497
|
-
var d=e&&e.detail;
|
|
3498
|
-
if(ownUpdate||!d||!d.url||d.kind==='api')return;
|
|
3499
|
-
try{evictCache(cacheKey(d.url))}catch(err){}
|
|
3500
|
-
});
|
|
3501
|
-
try{
|
|
3502
|
-
var sw=navigator.serviceWorker;
|
|
3503
|
-
if(sw&&sw.addEventListener)sw.addEventListener('message',function(e){
|
|
3504
|
-
var d=e&&e.data;
|
|
3505
|
-
if(!d||d.type!=='stx:updated'||window.stxOffline)return;
|
|
3506
|
-
window.dispatchEvent(new CustomEvent('stx:updated',{detail:{url:d.url,kind:d.kind||'page'}}));
|
|
3507
|
-
});
|
|
3508
|
-
}catch(e){}
|
|
3509
|
-
|
|
3510
|
-
// \u2500\u2500 Prefetch on sight \u2500\u2500
|
|
3511
|
-
// Links are fetched as they scroll into view, a few per screen, so most
|
|
3512
|
-
// taps find their page already here. Not on a connection that asked to
|
|
3513
|
-
// save data, and not for a link marked data-stx-prefetch="false" or
|
|
3514
|
-
// "hover" (hover and touch still prefetch everything).
|
|
3515
|
-
var sightObserver=null;
|
|
3516
|
-
var sightCount=0;
|
|
3517
|
-
function observeLinks(){
|
|
3518
|
-
if(!o.prefetch||!o.cache||o.prefetchVisible===false||typeof IntersectionObserver!=='function')return;
|
|
3519
|
-
var c=navigator.connection;
|
|
3520
|
-
if(c&&(c.saveData||/2g/.test(c.effectiveType||'')))return;
|
|
3521
|
-
if(!sightObserver)sightObserver=new IntersectionObserver(function(entries){
|
|
3522
|
-
entries.forEach(function(en){
|
|
3523
|
-
if(!en.isIntersecting)return;
|
|
3524
|
-
sightObserver.unobserve(en.target);
|
|
3525
|
-
if(sightCount>=o.prefetchVisibleMax)return;
|
|
3526
|
-
sightCount++;
|
|
3527
|
-
prefetchLink(en.target);
|
|
3528
|
-
});
|
|
3529
|
-
});
|
|
3530
|
-
sightCount=0;
|
|
3531
|
-
var root=activeRoot()||document.body;
|
|
3532
|
-
if(!root||!root.querySelectorAll)return;
|
|
3533
|
-
root.querySelectorAll('[data-stx-link][href]').forEach(function(a){
|
|
3534
|
-
if(a.__stxSeen)return;
|
|
3535
|
-
var p=a.getAttribute('data-stx-prefetch');
|
|
3536
|
-
if(p==='false'||p==='hover'||p==='none')return;
|
|
3537
|
-
a.__stxSeen=1;
|
|
3538
|
-
sightObserver.observe(a);
|
|
3539
|
-
});
|
|
3540
|
-
}
|
|
3541
|
-
|
|
3542
|
-
// \u2500\u2500 Link interception \u2500\u2500
|
|
3543
|
-
// Links the router must never touch, however it found them. This is the
|
|
3544
|
-
// opt-out set, and it is deliberately separate from shouldIntercept's
|
|
3545
|
-
// eligibility rules below: [data-stx-link] is an author saying "this is a
|
|
3546
|
-
// router link", which answers eligibility but must NOT override an explicit
|
|
3547
|
-
// opt-out or a non-navigational scheme.
|
|
3548
|
-
function isRouterExcluded(link){
|
|
3549
|
-
if(!link)return true;
|
|
3550
|
-
var href=link.getAttribute('href');
|
|
3551
|
-
if(!href)return true;
|
|
3552
|
-
if(href.startsWith('http')||href.startsWith('#')||href.startsWith('mailto:')||href.startsWith('tel:')||href.startsWith('javascript:'))return true;
|
|
3553
|
-
if(link.target==='_blank')return true;
|
|
3554
|
-
if(link.hasAttribute('data-stx-no-router')||link.hasAttribute('data-no-router')||link.hasAttribute('download'))return true;
|
|
3555
|
-
return false;
|
|
3556
|
-
}
|
|
3557
|
-
|
|
3558
|
-
// Paths the router can actually render, compiled from the server's own route
|
|
3559
|
-
// table and shipped as regex sources (#1864). Absent when discovery found
|
|
3560
|
-
// nothing, which means UNKNOWN, not "owns nothing" \u2014 treating it as the
|
|
3561
|
-
// latter would disable SPA navigation for a whole site.
|
|
3562
|
-
var ownedRoutes=null;
|
|
3563
|
-
(function(){
|
|
3564
|
-
var raw=o.ownedRoutes;
|
|
3565
|
-
if(!raw||!raw.length)return;
|
|
3566
|
-
var compiled=[];
|
|
3567
|
-
for(var i=0;i<raw.length;i++){
|
|
3568
|
-
try{compiled.push(new RegExp(raw[i]))}
|
|
3569
|
-
catch(e){}
|
|
3570
|
-
}
|
|
3571
|
-
if(compiled.length)ownedRoutes=compiled;
|
|
3572
|
-
})();
|
|
3573
|
-
|
|
3574
|
-
// Does the router own this path? Only meaningful when the route table made it
|
|
3575
|
-
// to the client; otherwise every path is treated as owned, preserving the old
|
|
3576
|
-
// behaviour.
|
|
3577
|
-
function routerOwns(pathname){
|
|
3578
|
-
if(!ownedRoutes)return true;
|
|
3579
|
-
for(var i=0;i<ownedRoutes.length;i++){
|
|
3580
|
-
if(ownedRoutes[i].test(pathname))return true;
|
|
3581
|
-
}
|
|
3582
|
-
return false;
|
|
3583
|
-
}
|
|
3584
|
-
|
|
3585
|
-
// Eligibility for auto-claiming an arbitrary same-origin anchor under
|
|
3586
|
-
// interceptAllLinks. Only reached for anchors the author did not mark.
|
|
3587
|
-
function shouldIntercept(link){
|
|
3588
|
-
if(isRouterExcluded(link))return false;
|
|
3589
|
-
var href=link.getAttribute('href');
|
|
3590
|
-
if(href.startsWith('?'))return true;
|
|
3591
|
-
if(href===location.pathname)return false;
|
|
3592
|
-
if(!getContainer())return false;
|
|
3593
|
-
// An endpoint the router cannot render must go to the browser. Claiming it
|
|
3594
|
-
// meant a failed fragment fetch, a fallback navigation, and the endpoint
|
|
3595
|
-
// hit TWICE \u2014 on OAuth redirects, that mints state twice.
|
|
3596
|
-
var path=href;
|
|
3597
|
-
var q=path.indexOf('?');if(q!==-1)path=path.slice(0,q);
|
|
3598
|
-
var h=path.indexOf('#');if(h!==-1)path=path.slice(0,h);
|
|
3599
|
-
if(path.charAt(0)==='/'&&!routerOwns(path)){
|
|
3600
|
-
log('[router] not a known route, leaving to the browser:',href);
|
|
3601
|
-
return false;
|
|
3602
|
-
}
|
|
3603
|
-
return true;
|
|
3604
|
-
}
|
|
3605
|
-
|
|
3606
|
-
// SPA click handling. By default only [data-stx-link] elements get
|
|
3607
|
-
// SPA navigation; regular <a href> does native full page reload. Set
|
|
3608
|
-
// interceptAllLinks:true (window.__stxRouterConfig or build-time
|
|
3609
|
-
// config) to intercept any same-origin anchor \u2014 used by static-site
|
|
3610
|
-
// mode where every page is part of the same SPA shell.
|
|
3611
|
-
document.addEventListener('click',function(e){
|
|
3612
|
-
if(e.metaKey||e.ctrlKey||e.shiftKey||e.altKey||e.button!==0)return;
|
|
3613
|
-
// A handler on the page has already decided this click is not a
|
|
3614
|
-
// navigation, so the router must not perform one - the same contract the
|
|
3615
|
-
// browser honours for a plain anchor.
|
|
3616
|
-
//
|
|
3617
|
-
// This listener used to run in the CAPTURE phase, which broke that twice
|
|
3618
|
-
// over: it saw the click before the anchor did, so defaultPrevented was
|
|
3619
|
-
// necessarily false by the time it was read here, and the stopPropagation
|
|
3620
|
-
// below then stopped the event ever reaching the element - so a page's own
|
|
3621
|
-
// click handler on an intercepted link never ran AT ALL. Progressive
|
|
3622
|
-
// enhancement on an anchor was impossible; the only way out was to swap it
|
|
3623
|
-
// for a button in script (stacksjs/stacks#2393).
|
|
3624
|
-
//
|
|
3625
|
-
// Bubbling instead puts the page's handlers first, which is where a
|
|
3626
|
-
// decision about the page's own markup belongs.
|
|
3627
|
-
if(e.defaultPrevented)return;
|
|
3628
|
-
if(!e.target||!e.target.closest)return;
|
|
3629
|
-
var link=e.target.closest('[data-stx-link]');
|
|
3630
|
-
// A marked link still has to clear the opt-out set. It used to skip it
|
|
3631
|
-
// entirely, so data-no-router on a [data-stx-link] anchor was silently
|
|
3632
|
-
// ignored and an OAuth redirect got claimed by the router.
|
|
3633
|
-
if(link&&isRouterExcluded(link)){log('[router] excluded:',link.getAttribute('href'));return}
|
|
3634
|
-
if(!link&&o.interceptAllLinks){
|
|
3635
|
-
var anchor=e.target.closest('a[href]');
|
|
3636
|
-
if(anchor&&shouldIntercept(anchor))link=anchor;
|
|
3637
|
-
}
|
|
3638
|
-
if(!link){return}
|
|
3639
|
-
var href=link.getAttribute('href');
|
|
3640
|
-
log('[router] click intercepted:',href,'container:',!!getContainer(),'defaultPrevented:',e.defaultPrevented);
|
|
3641
|
-
e.preventDefault();
|
|
3642
|
-
e.stopPropagation();
|
|
3643
|
-
log('[router] navigating to:',href);
|
|
3644
|
-
// A tab: its own stack, never a back entry (selectTab).
|
|
3645
|
-
if(link.getAttribute('data-stx-nav')==='tab'||link.hasAttribute('data-native-tab')){selectTab(href);return}
|
|
3646
|
-
instantNext=link.getAttribute('data-stx-transition')==='none';
|
|
3647
|
-
navigate(withCurrentLocale(href));
|
|
3648
|
-
});
|
|
3649
|
-
|
|
3650
|
-
// \u2500\u2500 Form submission \u2500\u2500
|
|
3651
|
-
// A submit IS a navigation, and the router only ever watched clicks. So any
|
|
3652
|
-
// in-app form tore down the SPA with a full document load and discarded
|
|
3653
|
-
// every signal on the page, and the only way out was a hand-written fetch in
|
|
3654
|
-
// a @submit handler with its own loading and error state.
|
|
3655
|
-
//
|
|
3656
|
-
// Gated exactly like links: a form opts in with data-stx-form, or
|
|
3657
|
-
// interceptForms claims them all \u2014 the same shape as [data-stx-link] /
|
|
3658
|
-
// interceptAllLinks. Deliberately not on by default: a POST has side
|
|
3659
|
-
// effects, so claiming one has to be a decision rather than something that
|
|
3660
|
-
// starts happening on upgrade (#1863).
|
|
3661
|
-
function formAttrOf(form,submitter,submitterAttr,formAttr){
|
|
3662
|
-
// A submit button overrides the form: formaction / formmethod / formenctype.
|
|
3663
|
-
var v=submitter&&submitter.getAttribute?submitter.getAttribute(submitterAttr):null;
|
|
3664
|
-
return (v!==null&&v!==undefined)?v:form.getAttribute(formAttr);
|
|
3665
|
-
}
|
|
3666
|
-
|
|
3667
|
-
function formTargetUrl(form,submitter){
|
|
3668
|
-
return formAttrOf(form,submitter,'formaction','action')||(location.pathname+location.search);
|
|
3669
|
-
}
|
|
3670
|
-
|
|
3671
|
-
function formMethodOf(form,submitter){
|
|
3672
|
-
return String(formAttrOf(form,submitter,'formmethod','method')||'get').toLowerCase();
|
|
3673
|
-
}
|
|
3674
|
-
|
|
3675
|
-
function formExcluded(form,submitter){
|
|
3676
|
-
if(!form||form.tagName!=='FORM')return true;
|
|
3677
|
-
if(form.hasAttribute('data-stx-no-router')||form.hasAttribute('data-no-router'))return true;
|
|
3678
|
-
// A form aimed at another browsing context is not this page's navigation.
|
|
3679
|
-
if(form.target&&form.target!=='_self')return true;
|
|
3680
|
-
try{
|
|
3681
|
-
// Resolving against location handles relative, absolute and foreign
|
|
3682
|
-
// schemes in one step: mailto: parses to a null origin and is excluded.
|
|
3683
|
-
if(new URL(formTargetUrl(form,submitter),location.href).origin!==location.origin)return true;
|
|
3684
|
-
}
|
|
3685
|
-
catch(e){return true}
|
|
3686
|
-
return false;
|
|
3687
|
-
}
|
|
3688
|
-
|
|
3689
|
-
function setFormPending(form,on){
|
|
3690
|
-
if(on){form.classList.add('stx-submitting');form.setAttribute('aria-busy','true')}
|
|
3691
|
-
else{form.classList.remove('stx-submitting');form.removeAttribute('aria-busy')}
|
|
3692
|
-
}
|
|
3693
|
-
|
|
3694
|
-
function submitForm(form,submitter){
|
|
3695
|
-
var method=formMethodOf(form,submitter);
|
|
3696
|
-
var action=formTargetUrl(form,submitter);
|
|
3697
|
-
var fd=new FormData(form);
|
|
3698
|
-
// A named submit button contributes its value, exactly as a native submit
|
|
3699
|
-
// would. FormData does not include it, so multi-button forms (Save vs
|
|
3700
|
-
// Delete) would otherwise lose the one piece of state that distinguishes them.
|
|
3701
|
-
if(submitter&&submitter.name)fd.append(submitter.name,submitter.value||'');
|
|
3702
|
-
|
|
3703
|
-
if(method==='get'){
|
|
3704
|
-
// A GET form is a link whose href the user filled in. Serialise and hand
|
|
3705
|
-
// it to the ordinary navigation path \u2014 no special swap handling, and it
|
|
3706
|
-
// inherits caching, prefetch and the guard-redirect behaviour for free.
|
|
3707
|
-
var qs=new URLSearchParams();
|
|
3708
|
-
fd.forEach(function(v,k){if(typeof v==='string')qs.append(k,v)});
|
|
3709
|
-
var gu=new URL(action,location.href);
|
|
3710
|
-
gu.search=qs.toString();
|
|
3711
|
-
return navigate(gu.pathname+gu.search+gu.hash);
|
|
3712
|
-
}
|
|
3713
|
-
|
|
3714
|
-
setFormPending(form,true);
|
|
3715
|
-
form.dispatchEvent(new CustomEvent('stx:form-submit',{bubbles:true,detail:{action:action,method:method}}));
|
|
3716
|
-
|
|
3717
|
-
var enctype=String(formAttrOf(form,submitter,'formenctype','enctype')||'').toLowerCase();
|
|
3718
|
-
// Respect the declared encoding. Handing FormData to fetch always produces
|
|
3719
|
-
// multipart, which a server expecting urlencoded will read as empty.
|
|
3720
|
-
var body=enctype.indexOf('multipart')===0?fd:new URLSearchParams(fd);
|
|
3721
|
-
var wantsFragment=shouldUseFragmentResponse();
|
|
3722
|
-
|
|
3723
|
-
return fetch(action,{
|
|
3724
|
-
method:method.toUpperCase(),
|
|
3725
|
-
body:body,
|
|
3726
|
-
headers:wantsFragment?{'X-STX-Router':'true','Accept':'text/html'}:{'Accept':'text/html'},
|
|
3727
|
-
credentials:'same-origin',
|
|
3728
|
-
}).then(function(r){
|
|
3729
|
-
// POST/redirect/GET. The server said where the result lives, so go there
|
|
3730
|
-
// through the normal path rather than swapping the POST's body under the
|
|
3731
|
-
// form's own URL \u2014 otherwise reloading re-submits.
|
|
3732
|
-
if(r.redirected&&r.url){
|
|
3733
|
-
var rd=new URL(r.url,location.href);
|
|
3734
|
-
if(rd.origin!==location.origin){location.href=r.url;return}
|
|
3735
|
-
return navigate(rd.pathname+rd.search+rd.hash);
|
|
3736
|
-
}
|
|
3737
|
-
return r.text().then(function(html){
|
|
3738
|
-
var isFrag=wantsFragment&&r.headers.get('X-STX-Fragment')==='true';
|
|
3739
|
-
pendingContainerAttrs=isFrag?(r.headers.get('X-STX-Container-Attrs')||''):'';
|
|
3740
|
-
pendingLayoutDecl=isFrag?null:{layout:r.headers.get('X-STX-Layout')||'',group:r.headers.get('X-STX-Layout-Group')||''};
|
|
3741
|
-
var marked=isFrag?fragmentMarker(r.headers.get('X-STX-Runtime')||'')+html:html;
|
|
3742
|
-
// Deliberately NOT cached: this body is the answer to one POST, and
|
|
3743
|
-
// serving it later for a GET of the same path would be a lie.
|
|
3744
|
-
// 'replace' so re-rendered validation errors do not stack one history
|
|
3745
|
-
// entry per attempt.
|
|
3746
|
-
return Promise.resolve(swap(marked,cacheKey(action),'replace','')).then(function(){
|
|
3747
|
-
applyTitle(r.headers.get('X-STX-Title')||'');
|
|
3748
|
-
});
|
|
3749
|
-
});
|
|
3750
|
-
}).catch(function(err){
|
|
3751
|
-
// No native re-submit on failure: that would send the POST a second time.
|
|
3752
|
-
// The app is told instead, and the form is left intact for a retry.
|
|
3753
|
-
console.error('[router] form submit failed:',err);
|
|
3754
|
-
form.dispatchEvent(new CustomEvent('stx:form-error',{bubbles:true,detail:{error:err,action:action}}));
|
|
3755
|
-
}).finally(function(){setFormPending(form,false)});
|
|
3756
|
-
}
|
|
3757
|
-
|
|
3758
|
-
document.addEventListener('submit',function(e){
|
|
3759
|
-
// Bubble phase, and defaultPrevented is honoured, so a page's own @submit
|
|
3760
|
-
// handler always wins. This is a fallback for forms nobody else claimed,
|
|
3761
|
-
// never an override of one that is already handled.
|
|
3762
|
-
if(e.defaultPrevented)return;
|
|
3763
|
-
var form=e.target;
|
|
3764
|
-
if(!form||form.tagName!=='FORM')return;
|
|
3765
|
-
if(!form.hasAttribute('data-stx-form')&&!form.hasAttribute('data-stx-link')&&!o.interceptForms)return;
|
|
3766
|
-
var submitter=e.submitter||null;
|
|
3767
|
-
if(formExcluded(form,submitter))return;
|
|
3768
|
-
// Without a container there is nothing to swap into, so a native submit is
|
|
3769
|
-
// the only thing that can work.
|
|
3770
|
-
if(!getContainer())return;
|
|
3771
|
-
// Constraint validation has already run by the time submit fires, so an
|
|
3772
|
-
// invalid form never reaches here.
|
|
3773
|
-
e.preventDefault();
|
|
3774
|
-
// A synchronous throw in here would be swallowed by dispatchEvent, and
|
|
3775
|
-
// preventDefault has already run \u2014 so the form would be silently dead with
|
|
3776
|
-
// nothing in the console. Report it and clear the pending state instead.
|
|
3777
|
-
try{submitForm(form,submitter)}
|
|
3778
|
-
catch(err){
|
|
3779
|
-
console.error('[router] form submit failed:',err);
|
|
3780
|
-
setFormPending(form,false);
|
|
3781
|
-
form.dispatchEvent(new CustomEvent('stx:form-error',{bubbles:true,detail:{error:err}}));
|
|
3782
|
-
}
|
|
3783
|
-
});
|
|
3784
|
-
|
|
3785
|
-
// \u2500\u2500 Back/forward \u2500\u2500
|
|
3786
|
-
window.addEventListener('popstate',function(){
|
|
3787
|
-
// A tab switch rewinding to the first entry: not a navigation of its own.
|
|
3788
|
-
if(rewinding){rewinding();return}
|
|
3789
|
-
var settled=swipeSettled;
|
|
3790
|
-
swipeSettled=null;
|
|
3791
|
-
// Nothing has scrolled yet (restoration is manual), so the viewport still
|
|
3792
|
-
// shows the entry being left: save it under its own token before adopting
|
|
3793
|
-
// the popped entry's, and hand the swap the position to land on.
|
|
3794
|
-
if(!settled)rememberScroll();
|
|
3795
|
-
var st=history.state;
|
|
3796
|
-
// An entry the router did not write (a page's own pushState) keeps the
|
|
3797
|
-
// depth it was written at; a lower one is a step back.
|
|
3798
|
-
var depth=st&&typeof st[DEPTH]==='number'?st[DEPTH]:histDepth;
|
|
3799
|
-
var dir=depth<histDepth?'pop':depth>histDepth?'push':'replace';
|
|
3800
|
-
histDepth=depth;
|
|
3801
|
-
var popped=st&&st[SCROLL_TOKEN];
|
|
3802
|
-
scrollToken=popped||newScrollToken();
|
|
3803
|
-
if(restoreScroll){
|
|
3804
|
-
if(!popped)stampScrollToken();
|
|
3805
|
-
pendingScroll=readScroll(scrollToken);
|
|
3806
|
-
pendingInner=readInner(scrollToken);
|
|
3807
|
-
}
|
|
3808
|
-
if(screensLive){
|
|
3809
|
-
if(st&&st[TAB]!==undefined&&st[TAB]!==curTab&&tabs[st[TAB]]){curTab=st[TAB];markTab(curTab)}
|
|
3810
|
-
var target=(tabs[curTab]||[])[depth];
|
|
3811
|
-
if(target&&target.el&&target.token===scrollToken&&target!==active){showRetained(target,dir==='replace'?'pop':dir,{settled:settled===target});return}
|
|
3812
|
-
}
|
|
3813
|
-
navCtxNext={direction:dir};
|
|
3814
|
-
navigate(location.pathname+location.search+location.hash,false);
|
|
3815
|
-
});
|
|
3816
|
-
|
|
3817
|
-
// \u2500\u2500 Offline stand-in \u2500\u2500
|
|
3818
|
-
// Without a network the offline worker answers a screen it has not kept
|
|
3819
|
-
// whole with the app's first screen, marked. A screen reached inside the
|
|
3820
|
-
// app was kept as a fragment, so ask for that and show it in place. Only
|
|
3821
|
-
// when it is there: a failed navigate reloads, and the reload would be
|
|
3822
|
-
// answered with the stand-in again.
|
|
3823
|
-
var standIn=document.querySelector('meta[name="stx-offline-fallback"]');
|
|
3824
|
-
if(standIn&&shouldUseFragmentResponse()){
|
|
3825
|
-
standIn.remove();
|
|
3826
|
-
var wanted=location.pathname+location.search;
|
|
3827
|
-
fetch(wanted,{headers:{'X-STX-Router':'true','Accept':'text/html'}}).then(function(r){
|
|
3828
|
-
if(r.ok&&r.headers.get('X-STX-Fragment')==='true'&&location.pathname+location.search===wanted)
|
|
3829
|
-
navigate(wanted+location.hash,false,true);
|
|
3830
|
-
}).catch(function(){});
|
|
3831
|
-
}
|
|
3832
|
-
|
|
3833
|
-
// \u2500\u2500 Prefetch \u2500\u2500
|
|
3834
|
-
// A link's page, fetched before it is followed into the cache navigate
|
|
3835
|
-
// reads, so following it is a swap rather than a round trip.
|
|
3836
|
-
// 'prefetching' holds each fetch in flight, so a tap that lands before its
|
|
3837
|
-
// prefetch has finished waits for that answer instead of asking again.
|
|
3838
|
-
// 'fresh' refetches a page that is cached, for an entry restored from an
|
|
3839
|
-
// earlier launch that is shown at once and replaced behind it.
|
|
3840
|
-
function prefetchLink(link,fresh){
|
|
3841
|
-
// Same opt-out set as the click path. Without it, hovering a link the
|
|
3842
|
-
// router is not allowed to claim still fired a real GET at it \u2014 a
|
|
3843
|
-
// logout or OAuth URL was requested on hover alone.
|
|
3844
|
-
if(isRouterExcluded(link))return;
|
|
3845
|
-
var href=withCurrentLocale(link.getAttribute('href'));
|
|
3846
|
-
var key=cacheKey(href);
|
|
3847
|
-
if((cache[key]&&!fresh)||prefetching[key])return;
|
|
3848
|
-
var eager=link.getAttribute('data-stx-prefetch')==='eager';
|
|
3849
|
-
var wantsFragment=shouldUseFragmentResponse();
|
|
3850
|
-
prefetching[key]=fetch(href,{headers:wantsFragment?{'X-STX-Router':'true','Accept':'text/html'}:{'Accept':'text/html'}}).then(function(r){
|
|
3851
|
-
return readPrefetchResponse(r,wantsFragment);
|
|
3852
|
-
}).then(function(result){
|
|
3853
|
-
if(result&&o.cache){
|
|
3854
|
-
setCache(key,result.html,result.layout,result.layoutGroup,result.title,result.containerAttrs);
|
|
3855
|
-
if(eager)keepPage(key,result);
|
|
3856
|
-
}
|
|
3857
|
-
if(result){
|
|
3858
|
-
loadModuleRegistries(result.html,href);
|
|
3859
|
-
// Its stylesheet too, so the tap never waits on cssLoadTimeout for a
|
|
3860
|
-
// sheet it could have had all along. Added unapplied (media=print),
|
|
3861
|
-
// as a navigation adds it; the swap makes it live.
|
|
3862
|
-
if(result.html.indexOf('<!--stx-fragment')===0)preloadGeneratedCss(fragmentCssHrefs(result.html),location.href);
|
|
3863
|
-
}
|
|
3864
|
-
}).catch(function(){}).finally(function(){delete prefetching[key]});
|
|
3865
|
-
}
|
|
3866
|
-
|
|
3867
|
-
// \u2500\u2500 Pages kept between launches \u2500\u2500
|
|
3868
|
-
// An eager link's page (a tab bar's) is kept in localStorage under the build
|
|
3869
|
-
// that rendered it, and put back into the cache when the next launch of the
|
|
3870
|
-
// same build starts. The first tap after opening the app is then a swap,
|
|
3871
|
-
// not a round trip, however far the server is; the eager prefetch replaces
|
|
3872
|
-
// the kept copy behind it. Pages of any other build are dropped, since a
|
|
3873
|
-
// runtime must never be handed another build's fragment (#1772).
|
|
3874
|
-
var KEPT_PREFIX='stx:pages:';
|
|
3875
|
-
var KEPT_MAX_BYTES=400000;
|
|
3876
|
-
function keptStore(){try{return window.localStorage||null}catch(e){return null}}
|
|
3877
|
-
function keepPage(key,result){
|
|
3878
|
-
var store=keptStore();
|
|
3879
|
-
if(!store||!loadedBuild||!result||!result.html||result.html.length>KEPT_MAX_BYTES)return;
|
|
3880
|
-
try{
|
|
3881
|
-
var all=JSON.parse(store.getItem(KEPT_PREFIX+loadedBuild)||'{}');
|
|
3882
|
-
all[key]={h:result.html,l:result.layout,g:result.layoutGroup,t:result.title,a:result.containerAttrs};
|
|
3883
|
-
store.setItem(KEPT_PREFIX+loadedBuild,JSON.stringify(all));
|
|
3884
|
-
}catch(e){}
|
|
3885
|
-
}
|
|
3886
|
-
function restoreKeptPages(){
|
|
3887
|
-
var store=keptStore();
|
|
3888
|
-
if(!store||!o.cache||!loadedBuild)return;
|
|
3889
|
-
try{
|
|
3890
|
-
for(var i=store.length-1;i>=0;i--){
|
|
3891
|
-
var name=store.key(i);
|
|
3892
|
-
if(name&&name.indexOf(KEPT_PREFIX)===0&&name!==KEPT_PREFIX+loadedBuild)store.removeItem(name);
|
|
3893
|
-
}
|
|
3894
|
-
var all=JSON.parse(store.getItem(KEPT_PREFIX+loadedBuild)||'{}');
|
|
3895
|
-
for(var key in all){
|
|
3896
|
-
var page=all[key];
|
|
3897
|
-
if(!Object.prototype.hasOwnProperty.call(all,key)||!page||!page.h||cache[key])continue;
|
|
3898
|
-
setCache(key,page.h,page.l||'',page.g||'',page.t||'',page.a||'');
|
|
3899
|
-
restored[key]=true;
|
|
3900
|
-
loadModuleRegistries(page.h,key);
|
|
3901
|
-
}
|
|
3902
|
-
}catch(e){}
|
|
3903
|
-
}
|
|
3904
|
-
var restored={};
|
|
3905
|
-
|
|
3906
|
-
// A prefetched page's module registry (the file its imports are bundled
|
|
3907
|
-
// into, one per page) is loaded with the prefetch, not on the tap. It only
|
|
3908
|
-
// registers modules, once, so loading it early changes nothing but timing,
|
|
3909
|
-
// and the tap then finds every script it needs already here: the swap runs
|
|
3910
|
-
// in one task and the first visit to a tab paints whole, like the second.
|
|
3911
|
-
function loadModuleRegistries(html,base){
|
|
3912
|
-
html.replace(new RegExp('<scr'+'ipt\\\\b([^>]*)>','gi'),function(m,attrs){
|
|
3913
|
-
if(!/(?:^|\\s)data-stx-modules(?:[\\s=]|$)/i.test(attrs))return m;
|
|
3914
|
-
var srcMatch=attrs.match(/\\bsrc\\s*=\\s*(?:"([^"]*)"|'([^']*)'|([^\\s>]+))/i);
|
|
3915
|
-
var src=srcMatch&&(srcMatch[1]||srcMatch[2]||srcMatch[3]);
|
|
3916
|
-
if(src)loadExternalScript(src,base);
|
|
3917
|
-
return m;
|
|
3918
|
-
});
|
|
3919
|
-
}
|
|
3920
|
-
|
|
3921
|
-
// Links marked data-stx-prefetch="eager", fetched once the page is idle, so
|
|
3922
|
-
// even their first tap is served from the cache: the way a phone's tab bar
|
|
3923
|
-
// switches instantly. Not on a connection that asked to save data.
|
|
3924
|
-
var eagerQueued=false;
|
|
3925
|
-
function prefetchEager(){
|
|
3926
|
-
if(!o.prefetch||!o.cache||eagerQueued)return;
|
|
3927
|
-
eagerQueued=true;
|
|
3928
|
-
var run=function(){
|
|
3929
|
-
eagerQueued=false;
|
|
3930
|
-
var connection=navigator.connection;
|
|
3931
|
-
if(connection&&(connection.saveData||/2g/.test(connection.effectiveType||'')))return;
|
|
3932
|
-
// The page it is on too: it was loaded as a whole document, so without
|
|
3933
|
-
// its fragment the way back to it went to the network.
|
|
3934
|
-
var links=document.querySelectorAll('[data-stx-link][data-stx-prefetch="eager"]');
|
|
3935
|
-
for(var i=0;i<links.length;i++){
|
|
3936
|
-
var href=links[i].getAttribute('href');
|
|
3937
|
-
if(!href)continue;
|
|
3938
|
-
var key=cacheKey(withCurrentLocale(href));
|
|
3939
|
-
prefetchLink(links[i],restored[key]);
|
|
3940
|
-
delete restored[key];
|
|
3941
|
-
}
|
|
3942
|
-
};
|
|
3943
|
-
// Soon after the first paint rather than whenever the page next idles: on
|
|
3944
|
-
// a distant server each page is a second away, and the first tap comes fast.
|
|
3945
|
-
if(window.requestIdleCallback)window.requestIdleCallback(run,{timeout:500});
|
|
3946
|
-
else setTimeout(run,100);
|
|
3947
|
-
}
|
|
3948
|
-
|
|
3949
|
-
if(o.prefetch){
|
|
3950
|
-
// A mouse hovers before it clicks. A finger does not hover, but it
|
|
3951
|
-
// touches down a tap's length before the click: start the fetch there.
|
|
3952
|
-
var onIntent=function(e){
|
|
3953
|
-
if(!e.target||!e.target.closest)return;
|
|
3954
|
-
prefetchLink(e.target.closest('[data-stx-link]'));
|
|
3955
|
-
};
|
|
3956
|
-
document.addEventListener('mouseover',onIntent,true);
|
|
3957
|
-
document.addEventListener('touchstart',onIntent,{capture:true,passive:true});
|
|
3958
|
-
window.addEventListener('stx:load',prefetchEager);
|
|
3959
|
-
window.addEventListener('stx:load',function(){setTimeout(observeLinks,0)});
|
|
3960
|
-
}
|
|
3961
|
-
|
|
3962
|
-
// \u2500\u2500 Active link management \u2500\u2500
|
|
3963
|
-
// Which links point at the page being shown. Every href is resolved against
|
|
3964
|
-
// the current URL first, because comparing the raw attribute with
|
|
3965
|
-
// location.pathname got most real links wrong:
|
|
3966
|
-
// - a query-only href (?range=24h, the shape of every range picker and tab
|
|
3967
|
-
// bar) never equalled a pathname, so the active one lost its classes and
|
|
3968
|
-
// had the server's aria-current="page" stripped on every load;
|
|
3969
|
-
// - /dashboard?site=1 was never an exact match for /dashboard;
|
|
3970
|
-
// - http://same-origin/x and /x/ were never matches for /x;
|
|
3971
|
-
// - /blog was "active" on /blogging, a prefix that is not a parent.
|
|
3972
|
-
// Null for links the router does not own (another origin, a bare #fragment,
|
|
3973
|
-
// anything unparseable), which are left exactly as the server rendered them.
|
|
3974
|
-
function normPath(p){return p.length>1&&p.charAt(p.length-1)==='/'?p.slice(0,-1):p}
|
|
3975
|
-
// Paths beyond the link's own that make it current: a tab bar's Calendar
|
|
3976
|
-
// tab stays lit on a workout opened from it (data-stx-active-match, a
|
|
3977
|
-
// space-separated list of path prefixes).
|
|
3978
|
-
function matchesAny(list,cur){
|
|
3979
|
-
if(!list)return false;
|
|
3980
|
-
return list.split(/\\s+/).some(function(p){p=normPath(p);return p&&(cur===p||cur.indexOf(p+'/')===0)});
|
|
3981
|
-
}
|
|
3982
|
-
function linkState(href,also){
|
|
3983
|
-
if(!href||href.charAt(0)==='#')return null;
|
|
3984
|
-
var u;
|
|
3985
|
-
try{u=new URL(href,location.href)}catch(e){return null}
|
|
3986
|
-
if(u.origin!==location.origin)return null;
|
|
3987
|
-
var path=normPath(u.pathname),cur=normPath(location.pathname);
|
|
3988
|
-
// A link with no query matches on its path, so a nav entry stays current
|
|
3989
|
-
// while the page adds filters. A link WITH a query is current only when
|
|
3990
|
-
// every param it names has that value here: of five range links that all
|
|
3991
|
-
// point at this path, exactly one is.
|
|
3992
|
-
var have=new URLSearchParams(location.search),q=true;
|
|
3993
|
-
u.searchParams.forEach(function(v,k){if(have.getAll(k).indexOf(v)===-1)q=false});
|
|
3994
|
-
// EXACT is the stricter half (stacksjs/stx#2017). A link with NO query
|
|
3995
|
-
// satisfied the loop above trivially, so a bare /list reported itself exact
|
|
3996
|
-
// on /list?status=up - taking exact-active-class and aria-current from the
|
|
3997
|
-
// chip the user was actually on, and telling a screen reader the unfiltered
|
|
3998
|
-
// link was the current page.
|
|
3999
|
-
//
|
|
4000
|
-
// Only a link with no query at all is held to the stricter test, which is
|
|
4001
|
-
// what keeps #1777 working: a range picker's /dashboard?site=1&range=7d is
|
|
4002
|
-
// still exact on /dashboard?site=1&range=7d&country=US, because a page
|
|
4003
|
-
// filter the link does not name should not unmark the range it IS on. The
|
|
4004
|
-
// two requests look opposed and are not - one is about a link naming MORE
|
|
4005
|
-
// than the page, the other about a link naming NOTHING.
|
|
4006
|
-
var linkHasQuery=false;
|
|
4007
|
-
u.searchParams.forEach(function(){linkHasQuery=true});
|
|
4008
|
-
var sameQuery=q&&(linkHasQuery||!location.search);
|
|
4009
|
-
var matched=matchesAny(also,cur);
|
|
4010
|
-
return {
|
|
4011
|
-
exact:sameQuery&&path===cur,
|
|
4012
|
-
active:q&&(path==='/'?cur==='/':(cur===path||cur.indexOf(path+'/')===0))||matched,
|
|
4013
|
-
matched:matched
|
|
4014
|
-
};
|
|
4015
|
-
}
|
|
4016
|
-
|
|
4017
|
-
// A nav marked data-stx-sticky-active is a tab bar: each tab owns the
|
|
4018
|
-
// screens opened from it, as on iOS. On a page that is none of its links'
|
|
4019
|
-
// own, the link that was current stays current (a workout opened from
|
|
4020
|
-
// Today keeps Today lit), and data-stx-active-match only decides a page
|
|
4021
|
-
// reached with nothing current yet, such as a cold start on a deep link.
|
|
4022
|
-
// data-stx-nav-current remembers which link that was, because
|
|
4023
|
-
// updateActiveLinks has already rewritten the classes by now.
|
|
4024
|
-
function updateNav(){
|
|
4025
|
-
var done=new Set();
|
|
4026
|
-
qsa('nav, #mobileNav, [data-stx-nav]:not(a)').forEach(function(nav){
|
|
4027
|
-
var states=[];
|
|
4028
|
-
Array.prototype.forEach.call(nav.querySelectorAll('a[href]'),function(a){
|
|
4029
|
-
if(done.has(a)||!a.hasAttribute('data-stx-link'))return;
|
|
4030
|
-
done.add(a);
|
|
4031
|
-
var st=linkState(a.getAttribute('href'),a.getAttribute('data-stx-active-match'));
|
|
4032
|
-
if(st)states.push({a:a,st:st});
|
|
4033
|
-
});
|
|
4034
|
-
var kept=null;
|
|
4035
|
-
if(nav.hasAttribute('data-stx-sticky-active')&&!states.some(function(x){return x.st.exact})){
|
|
4036
|
-
kept=states.filter(function(x){return x.a.hasAttribute('data-stx-nav-current')})[0]||null;
|
|
4037
|
-
}
|
|
4038
|
-
states.forEach(function(x){
|
|
4039
|
-
var a=x.a,st=x.st;
|
|
4040
|
-
var on=kept?a===kept.a:(st.exact||st.matched);
|
|
4041
|
-
var ac=a.getAttribute('data-stx-active-class')||'active';
|
|
4042
|
-
ac.split(' ').forEach(function(cls){if(cls){if(on)a.classList.add(cls);else a.classList.remove(cls)}});
|
|
4043
|
-
if(on)a.setAttribute('data-stx-nav-current','');else a.removeAttribute('data-stx-nav-current');
|
|
4044
|
-
});
|
|
4045
|
-
});
|
|
4046
|
-
}
|
|
4047
|
-
|
|
4048
|
-
// Both passes, always in this order. A link inside a <nav> is current only on
|
|
4049
|
-
// its own page (or one it names in data-stx-active-match), so updateNav has
|
|
4050
|
-
// the last word. Navigation used to run them the other way round, which lit
|
|
4051
|
-
// a nav's parent links by prefix after an in-app navigation but not after a
|
|
4052
|
-
// full load of the same URL.
|
|
4053
|
-
function refreshCurrentLinks(){
|
|
4054
|
-
updateActiveLinks();
|
|
4055
|
-
updateNav();
|
|
4056
|
-
}
|
|
4057
|
-
|
|
4058
|
-
function updateActiveLinks(){
|
|
4059
|
-
// Update active classes on <stx-link> elements (and legacy data-stx-link)
|
|
4060
|
-
var links=qsa('[data-stx-link]');
|
|
4061
|
-
links.forEach(function(link){
|
|
4062
|
-
var st=linkState(link.getAttribute('to')||link.getAttribute('href')||'',link.getAttribute('data-stx-active-match'));
|
|
4063
|
-
if(!st)return;
|
|
4064
|
-
var ac=link.getAttribute('active-class')||link.getAttribute('data-stx-active-class')||'active';
|
|
4065
|
-
var eac=link.getAttribute('exact-active-class')||link.getAttribute('data-stx-exact-active-class')||'exact-active';
|
|
4066
|
-
ac.split(' ').forEach(function(cls){if(cls)link.classList.remove(cls)});
|
|
4067
|
-
eac.split(' ').forEach(function(cls){if(cls)link.classList.remove(cls)});
|
|
4068
|
-
if(st.exact)eac.split(' ').forEach(function(cls){if(cls)link.classList.add(cls)});
|
|
4069
|
-
if(st.active)ac.split(' ').forEach(function(cls){if(cls)link.classList.add(cls)});
|
|
4070
|
-
markCurrent(link,st.exact);
|
|
4071
|
-
});
|
|
4072
|
-
}
|
|
4073
|
-
|
|
4074
|
-
// aria-current="page" names the page the user is ON, so it has to move when
|
|
4075
|
-
// the page does. The server stamps it on the link for the page it rendered;
|
|
4076
|
-
// after that, only a full load would ever correct it, and a fragment swap is
|
|
4077
|
-
// not a full load. Left alone, a screen reader announces the entry the user
|
|
4078
|
-
// arrived through as current for the rest of the session, however far they
|
|
4079
|
-
// navigate \u2014 silently wrong, and only to the people relying on it.
|
|
4080
|
-
//
|
|
4081
|
-
// Only ever touched on links that already carry the attribute or are the
|
|
4082
|
-
// current one, so a page using aria-current for something else \u2014 a step in a
|
|
4083
|
-
// wizard, a sort direction \u2014 is not rewritten out from under itself.
|
|
4084
|
-
//
|
|
4085
|
-
// Cleared for 'true' as well as 'page' (#2017). Both say "this is the current
|
|
4086
|
-
// one" with no further meaning, and a consumer working around a missing
|
|
4087
|
-
// aria-current by stamping aria-current="true" server-side kept it forever:
|
|
4088
|
-
// the router neither updated nor removed it, so after one navigation it named
|
|
4089
|
-
// the wrong link and the workaround was worse than the bug. The role-specific
|
|
4090
|
-
// values - step, location, date, time - are still left alone, because those
|
|
4091
|
-
// ARE the page using the attribute for something else.
|
|
4092
|
-
function markCurrent(link,isExact){
|
|
4093
|
-
var now=link.getAttribute('aria-current');
|
|
4094
|
-
if(isExact){
|
|
4095
|
-
if(now!=='page')link.setAttribute('aria-current','page');
|
|
4096
|
-
return;
|
|
4097
|
-
}
|
|
4098
|
-
if(now==='page'||now==='true')link.removeAttribute('aria-current');
|
|
4099
|
-
}
|
|
4100
|
-
|
|
4101
|
-
// \u2500\u2500 Progress bar DOM + style \u2500\u2500
|
|
4102
|
-
// Injects a fixed-position element at the top of the viewport plus the
|
|
4103
|
-
// minimal CSS for its transform/opacity transitions. Kept idempotent so
|
|
4104
|
-
// repeated init() calls (e.g. after full-body swaps) don't duplicate.
|
|
4105
|
-
// Also ships the default View Transitions fade+slide CSS when
|
|
4106
|
-
// viewTransitions is enabled and the browser supports it. Apps can
|
|
4107
|
-
// override by defining more specific ::view-transition-* rules later in
|
|
4108
|
-
// the cascade, or opt out entirely with viewTransitions:false.
|
|
4109
|
-
function injectStyles(){
|
|
4110
|
-
if(!document.getElementById('stx-r-css')){
|
|
4111
|
-
var s=ce('style');s.id='stx-r-css';
|
|
4112
|
-
// Strip characters that could escape our CSS block and inject new
|
|
4113
|
-
// declarations: ; { } ( ) " ' \\ < > plus whitespace control chars.
|
|
4114
|
-
// This keeps the value safe to concat into a stylesheet even if a
|
|
4115
|
-
// caller wires the config from an untrusted source.
|
|
4116
|
-
var sanitize=function(v){return String(v).replace(/[;{}()"'\\\\<>\\n\\r\\t]/g,'')};
|
|
4117
|
-
var pc=sanitize(o.progressColor);
|
|
4118
|
-
var ph=sanitize(o.progressHeight);
|
|
4119
|
-
// The container the router focuses after a navigation is not a
|
|
4120
|
-
// control, so it draws no focus ring: Safari drew one around the whole
|
|
4121
|
-
// page, a blue line along its bottom edge on every screen.
|
|
4122
|
-
// Links stay live while a page loads: a tap on another one is the newest
|
|
4123
|
-
// navigation and wins (navigate), where pointer-events:none used to
|
|
4124
|
-
// swallow it.
|
|
4125
|
-
var css='[data-stx-route-focus]:focus{outline:none}.stx-navigating{cursor:progress}#stx-router-progress{position:fixed;top:0;left:0;right:0;height:'+ph+';background:'+pc+';box-shadow:0 0 8px '+pc+',0 0 4px '+pc+';transform:scaleX(0);transform-origin:left;transition:transform .18s ease-out,opacity .26s ease;opacity:0;pointer-events:none;z-index:999999}html.stx-instant::view-transition-group(*),html.stx-instant::view-transition-old(*),html.stx-instant::view-transition-new(*){animation:none!important}';
|
|
4126
|
-
// One View Transitions block. There used to be two, and the second
|
|
4127
|
-
// (injected after this one) silently overrode the first: the page stays
|
|
4128
|
-
// put and only the routed content fades, which is what this now says
|
|
4129
|
-
// once. The retained screen inside it is not named, so the container
|
|
4130
|
-
// and the screen never carry the same name.
|
|
4131
|
-
if(o.viewTransitions&&'startViewTransition' in document){
|
|
4132
|
-
var dur=(o.viewTransitionDuration||220)+'ms';
|
|
4133
|
-
var ease=o.viewTransitionEasing||'cubic-bezier(0.16, 1, 0.3, 1)';
|
|
4134
|
-
css+='::view-transition-old(root),::view-transition-new(root){animation:none}::view-transition{background:transparent}';
|
|
4135
|
-
css+='main,#app-content,[data-stx-content]:not([data-stx-screen]){view-transition-name:stx-content}::view-transition-group(stx-content){overflow:hidden}';
|
|
4136
|
-
css+='::view-transition-old(stx-content){animation:stx-r-fade-out '+dur+' '+ease+' both}::view-transition-new(stx-content){animation:stx-r-fade-in '+dur+' '+ease+' both}';
|
|
4137
|
-
css+='@keyframes stx-r-fade-out{from{opacity:1;transform:translateY(0)}to{opacity:0;transform:translateY(-4px)}}';
|
|
4138
|
-
css+='@keyframes stx-r-fade-in{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:translateY(0)}}';
|
|
4139
|
-
css+='@media (prefers-reduced-motion: reduce){::view-transition-old(stx-content),::view-transition-new(stx-content){animation-duration:0s;animation-name:none}}';
|
|
4140
|
-
}
|
|
4141
|
-
// Screens: a hidden one takes no room, and a screen lays its page out
|
|
4142
|
-
// the way the container would have. Push and pop are iOS's: the new
|
|
4143
|
-
// screen slides in from the right edge while the one under it moves a
|
|
4144
|
-
// third of the way left and dims, over --stx-nav-duration on a spring
|
|
4145
|
-
// (linear() where the browser has it). Reduced motion cross-fades.
|
|
4146
|
-
css+='[data-stx-screen]{display:inherit;flex-direction:inherit;flex-wrap:inherit;gap:inherit;align-items:inherit;justify-content:inherit;grid-template-columns:inherit;grid-template-rows:inherit;grid-column:1/-1;flex:1 1 auto;align-self:stretch;min-width:0}['+HIDDEN+']{display:none!important}';
|
|
4147
|
-
css+=':root{--stx-nav-duration:'+(o.navDuration|0)+'ms;--stx-nav-ease:cubic-bezier(0.32,0.72,0,1)}@supports (animation-timing-function:linear(0,1)){:root{--stx-nav-ease:linear(0,.079,.236,.403,.551,.671,.764,.833,.883,.919,.944,.962,.974,.983,.988,.992,.995,.997,.998,.999,1)}}';
|
|
4148
|
-
css+='[data-stx-animating]{overflow-x:clip}[data-stx-screen-enter],[data-stx-screen-leave]{position:relative;will-change:transform;animation-duration:var(--stx-nav-duration);animation-timing-function:var(--stx-nav-ease);animation-fill-mode:both}';
|
|
4149
|
-
css+='html[data-nav-direction=push] [data-stx-screen-enter]{animation-name:stx-nav-in;z-index:3;box-shadow:0 0 24px rgb(0 0 0 / .12)}html[data-nav-direction=push] [data-stx-screen-leave]{animation-name:stx-nav-out;z-index:1}';
|
|
4150
|
-
css+='html[data-nav-direction=pop] [data-stx-screen-enter]{animation-name:stx-nav-back-in;z-index:1}html[data-nav-direction=pop] [data-stx-screen-leave]{animation-name:stx-nav-back-out;z-index:3;box-shadow:0 0 24px rgb(0 0 0 / .12)}';
|
|
4151
|
-
css+='[data-stx-screen-dim]{position:fixed;top:0;bottom:0;z-index:2;background:rgb(0 0 0 / .1);pointer-events:none;animation:stx-nav-dim var(--stx-nav-duration) var(--stx-nav-ease) both}html[data-nav-direction=pop] [data-stx-screen-dim]{animation-direction:reverse}[data-stx-screen-dim][data-stx-drag]{animation:none}';
|
|
4152
|
-
css+='@keyframes stx-nav-in{from{transform:translateX(100%)}to{transform:none}}@keyframes stx-nav-out{from{transform:none}to{transform:translateX(-30%)}}@keyframes stx-nav-back-in{from{transform:translateX(-30%)}to{transform:none}}@keyframes stx-nav-back-out{from{transform:none}to{transform:translateX(100%)}}@keyframes stx-nav-dim{from{opacity:0}to{opacity:1}}@keyframes stx-nav-fade-in{from{opacity:0}}@keyframes stx-nav-fade-out{to{opacity:0}}';
|
|
4153
|
-
css+='@media (prefers-reduced-motion: reduce){html[data-nav-direction] [data-stx-screen-enter]{animation-name:stx-nav-fade-in;animation-duration:200ms;box-shadow:none}html[data-nav-direction] [data-stx-screen-leave]{animation-name:stx-nav-fade-out;animation-duration:200ms;box-shadow:none}[data-stx-screen-dim]{display:none}}';
|
|
4154
|
-
// The screen a failed load leaves behind (showNavError).
|
|
4155
|
-
css+='.stx-retry{display:flex;flex-direction:column;align-items:center;gap:.5rem;padding:4rem 1.5rem;text-align:center}.stx-retry-title{margin:0;font-weight:600;font-size:1.0625rem}.stx-retry-text{margin:0;opacity:.7;max-width:20rem}.stx-retry-button{margin-top:.75rem;padding:.5rem 1.25rem;border:0;border-radius:999px;background:var(--native-accent,#2563eb);color:#fff;font:inherit;font-weight:600}';
|
|
4156
|
-
s.textContent=css;
|
|
4157
|
-
dhead().appendChild(s);
|
|
4158
|
-
}
|
|
4159
|
-
if(o.progress&&!document.getElementById('stx-router-progress')){
|
|
4160
|
-
var el=ce('div');
|
|
4161
|
-
el.id='stx-router-progress';
|
|
4162
|
-
el.setAttribute('role','progressbar');
|
|
4163
|
-
el.setAttribute('aria-hidden','true');
|
|
4164
|
-
// Append to <html>, not <body>: when the router swaps the body
|
|
4165
|
-
// (container:'body' for full-page static-site SPAs) the progress
|
|
4166
|
-
// element gets wiped along with it. <html> stays put across
|
|
4167
|
-
// swaps, so the bar survives multi-hop navigation.
|
|
4168
|
-
document.documentElement.appendChild(el);
|
|
4169
|
-
progEl=el;
|
|
4170
|
-
} else if(o.progress){
|
|
4171
|
-
progEl=document.getElementById('stx-router-progress');
|
|
4172
|
-
}
|
|
4173
|
-
}
|
|
4174
|
-
|
|
4175
|
-
|
|
4176
|
-
// \u2500\u2500 Public API \u2500\u2500
|
|
4177
|
-
var router={
|
|
4178
|
-
navigate:navigate,
|
|
4179
|
-
navigateTo:navigate,
|
|
4180
|
-
prefetch:function(url){
|
|
4181
|
-
var key=cacheKey(url);
|
|
4182
|
-
if(!cache[key]){
|
|
4183
|
-
var wantsFragment=shouldUseFragmentResponse();
|
|
4184
|
-
fetch(url,{headers:wantsFragment?{'X-STX-Router':'true'}:{'Accept':'text/html'}}).then(function(r){return readPrefetchResponse(r,wantsFragment)}).then(function(result){if(result)setCache(key,result.html,result.layout,result.layoutGroup,result.title,result.containerAttrs)}).catch(function(){});
|
|
4185
|
-
}
|
|
4186
|
-
},
|
|
4187
|
-
// Re-run the CURRENT route against the server and swap the result.
|
|
4188
|
-
//
|
|
4189
|
-
// There was no way to do this. invalidate() on a query only re-runs client
|
|
4190
|
-
// fetches, so anything the server rendered \u2014 a list the mutation just
|
|
4191
|
-
// changed, a count in the layout \u2014 could only be refreshed with a full
|
|
4192
|
-
// document load, which discards every signal on the page. That is the
|
|
4193
|
-
// exact thing the SPA exists to avoid, so apps reached for
|
|
4194
|
-
// location.reload() and lost their state (#1850, #1858).
|
|
4195
|
-
//
|
|
4196
|
-
// force:true both evicts the cache entry and defeats navigate's
|
|
4197
|
-
// same-URL early return, so this is exposing behaviour the router already
|
|
4198
|
-
// had rather than adding a second code path. 'replace' because a refresh
|
|
4199
|
-
// is not a new place \u2014 it must not add a history entry you can go Back to.
|
|
4200
|
-
refresh:function(){
|
|
4201
|
-
return navigate(location.pathname+location.search+location.hash,'replace',true);
|
|
4202
|
-
},
|
|
4203
|
-
// Expire ONE entry, so a mutation can invalidate just the page it affected
|
|
4204
|
-
// and let the next visit re-fetch, instead of throwing the whole cache away.
|
|
4205
|
-
invalidate:function(url){
|
|
4206
|
-
evictCache(cacheKey(url||(location.pathname+location.search)));
|
|
4207
|
-
},
|
|
4208
|
-
clearCache:function(){for(var k in cache)delete cache[k];for(var lk in layoutCache)delete layoutCache[lk];for(var gk in layoutGroupCache)delete layoutGroupCache[gk];cacheOrder.length=0},
|
|
4209
|
-
// Switch to the tab whose link points at href, as a tap on it would:
|
|
4210
|
-
// its retained screens and its own back stack (selectTab).
|
|
4211
|
-
selectTab:selectTab,
|
|
4212
|
-
back:function(){return history.back()},
|
|
4213
|
-
// The screens kept alive, for a page or a test that wants to look.
|
|
4214
|
-
screens:function(){var out=[];eachEntry(function(e,id){out.push({url:e.url,tab:id,depth:e.depth,live:!!e.el,active:e===active})});return out},
|
|
4215
|
-
cache:cache,
|
|
4216
|
-
swap:swap,
|
|
4217
|
-
updateNav:updateNav
|
|
4218
|
-
};
|
|
4219
|
-
|
|
4220
|
-
router.__rev=ROUTER_REV;
|
|
4221
|
-
window.__stxRouter=router;
|
|
4222
|
-
window.stxRouter=router;
|
|
4223
|
-
if(window.stx)window.stx.router=router;
|
|
4224
|
-
|
|
4225
|
-
// \u2500\u2500 Initialize \u2500\u2500
|
|
4226
|
-
function init(){
|
|
4227
|
-
injectStyles();
|
|
4228
|
-
refreshCurrentLinks();
|
|
4229
|
-
// The first screen, from the start, so the page never changes shape on
|
|
4230
|
-
// its first navigation.
|
|
4231
|
-
if(screensOn()){var host=getContainer();if(host)adoptInitial(host)}
|
|
4232
|
-
restoreKeptPages();
|
|
4233
|
-
prefetchEager();
|
|
4234
|
-
observeLinks();
|
|
4235
|
-
}
|
|
4236
|
-
|
|
4237
|
-
if(document.readyState==='loading')document.addEventListener('DOMContentLoaded',init);else init();
|
|
4238
|
-
})();
|
|
4239
|
-
`;
|
|
4240
|
-
}
|
|
4241
|
-
function getRouterScript() {
|
|
4242
|
-
if (cachedRouterScript)
|
|
4243
|
-
return cachedRouterScript;
|
|
4244
|
-
cachedRouterScript = minifyRouterScript(stripRouterLogs(routerSource()), true);
|
|
4245
|
-
return cachedRouterScript;
|
|
4246
|
-
}
|
|
4247
|
-
function getRouterScriptDev() {
|
|
4248
|
-
if (cachedRouterScriptDev)
|
|
4249
|
-
return cachedRouterScriptDev;
|
|
4250
|
-
cachedRouterScriptDev = minifyRouterScript(routerSource());
|
|
4251
|
-
return cachedRouterScriptDev;
|
|
4252
|
-
}
|
|
4253
|
-
export {
|
|
4254
|
-
setAppUrl,
|
|
4255
|
-
runMiddleware,
|
|
4256
|
-
route,
|
|
4257
|
-
resolveLayoutChain,
|
|
4258
|
-
resetRoutes,
|
|
4259
|
-
registerMiddleware,
|
|
4260
|
-
patternToRegex,
|
|
4261
|
-
navigateTo,
|
|
4262
|
-
matchRoute,
|
|
4263
|
-
loadMiddlewareFromDirectory,
|
|
4264
|
-
isNonPageRoutePattern,
|
|
4265
|
-
isNonPageRoutePath,
|
|
4266
|
-
hasMiddleware,
|
|
4267
|
-
getRouterScriptDev,
|
|
4268
|
-
getRouterScript,
|
|
4269
|
-
getMiddlewareNames,
|
|
4270
|
-
getMiddleware,
|
|
4271
|
-
generateRouteTypes,
|
|
4272
|
-
generateRouteManifest,
|
|
4273
|
-
formatRoutes,
|
|
4274
|
-
findErrorPage,
|
|
4275
|
-
filePathToPattern,
|
|
4276
|
-
extractLayoutMetadata,
|
|
4277
|
-
deriveLayoutGroup,
|
|
4278
|
-
defineRoutes,
|
|
4279
|
-
defineRoute,
|
|
4280
|
-
defineMiddleware,
|
|
4281
|
-
createStorageManager,
|
|
4282
|
-
createServerCookieManager,
|
|
4283
|
-
createRouter,
|
|
4284
|
-
createRouteLocation,
|
|
4285
|
-
createMiddlewareContext,
|
|
4286
|
-
createClientCookieManager,
|
|
4287
|
-
clearMiddleware,
|
|
4288
|
-
bracketPathToRegex,
|
|
4289
|
-
abortNavigation,
|
|
4290
|
-
Router,
|
|
4291
|
-
NON_PAGE_DIRS
|
|
4292
|
-
};
|