@zenithbuild/router 0.7.2 → 0.7.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +1 -1
- package/package.json +3 -2
- package/template-core.js +2 -1
- package/template-document.js +49 -0
- package/template-form.js +184 -0
- package/template-navigation.js +119 -144
- package/template.js +4 -2
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Zenith Team
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zenithbuild/router",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.4",
|
|
4
4
|
"description": "File-based SPA router for Zenith framework with deterministic, compile-time route resolution",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -9,9 +9,10 @@
|
|
|
9
9
|
"dist",
|
|
10
10
|
"template.js",
|
|
11
11
|
"template-core.js",
|
|
12
|
+
"template-document.js",
|
|
13
|
+
"template-form.js",
|
|
12
14
|
"template-lifecycle.js",
|
|
13
15
|
"template-navigation.js",
|
|
14
|
-
"index.js",
|
|
15
16
|
"index.d.ts",
|
|
16
17
|
"README.md",
|
|
17
18
|
"dist/**",
|
package/template-core.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export function renderRouterCoreSource({ manifest, runtimeSpec, coreSpec }) {
|
|
1
|
+
export function renderRouterCoreSource({ manifest, runtimeSpec, coreSpec, routeCheck = false }) {
|
|
2
2
|
return `import { hydrate as __zenithHydrate } from '${runtimeSpec}';
|
|
3
3
|
import { zenOnMount as __zenithOnMount } from '${coreSpec}';
|
|
4
4
|
|
|
@@ -6,6 +6,7 @@ void __zenithHydrate;
|
|
|
6
6
|
void __zenithOnMount;
|
|
7
7
|
|
|
8
8
|
const __ZENITH_MANIFEST__ = ${manifest};
|
|
9
|
+
const __ZENITH_ROUTE_CHECK_ENABLED__ = ${routeCheck ? 'true' : 'false'};
|
|
9
10
|
const __ZENITH_BASE_PATH__ = normalizeBasePath(
|
|
10
11
|
typeof __ZENITH_MANIFEST__.base_path === "string" ? __ZENITH_MANIFEST__.base_path : "/"
|
|
11
12
|
);
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
export function renderRouterDocumentSource() {
|
|
2
|
+
return `function extractSsrData(parsed) {
|
|
3
|
+
if (!parsed || typeof parsed.getElementById !== "function") return {};
|
|
4
|
+
const ssrScript = parsed.getElementById("zenith-ssr-data");
|
|
5
|
+
if (!ssrScript) return {};
|
|
6
|
+
const source = typeof ssrScript.textContent === "string" ? ssrScript.textContent : "";
|
|
7
|
+
const marker = "window.__zenith_ssr_data =";
|
|
8
|
+
const markerIndex = source.indexOf(marker);
|
|
9
|
+
if (markerIndex === -1) return {};
|
|
10
|
+
const jsonText = source.slice(markerIndex + marker.length).trim().replace(/;$/, "");
|
|
11
|
+
try {
|
|
12
|
+
return JSON.parse(jsonText);
|
|
13
|
+
} catch {
|
|
14
|
+
return {};
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function parseDocumentPayload(html) {
|
|
19
|
+
if (typeof DOMParser === "undefined") return null;
|
|
20
|
+
const parsed = new DOMParser().parseFromString(html, "text/html");
|
|
21
|
+
return {
|
|
22
|
+
html,
|
|
23
|
+
title: parsed.title || "",
|
|
24
|
+
ssrData: extractSsrData(parsed)
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function isHtmlResponse(response) {
|
|
29
|
+
const contentType = String(response.headers.get("content-type") || "");
|
|
30
|
+
return /text\\/html|application\\/xhtml\\+xml/i.test(contentType);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function createDocumentDetail(payload, response) {
|
|
34
|
+
return {
|
|
35
|
+
title: payload && typeof payload.title === "string" ? payload.title : "",
|
|
36
|
+
hasSsrData: !!(payload && payload.ssrData && typeof payload.ssrData === "object"),
|
|
37
|
+
status: response && typeof response.status === "number" ? response.status : 200
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function createScrollDetail(targetUrl, scrollTarget) {
|
|
42
|
+
return {
|
|
43
|
+
mode: scrollTarget.mode,
|
|
44
|
+
x: scrollTarget.x,
|
|
45
|
+
y: scrollTarget.y,
|
|
46
|
+
hash: targetUrl.hash || ""
|
|
47
|
+
};
|
|
48
|
+
}`;
|
|
49
|
+
}
|
package/template-form.js
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
export function renderRouterFormSource() {
|
|
2
|
+
return `function normalizeFormMethod(method) {
|
|
3
|
+
const value = typeof method === "string" ? method.trim().toUpperCase() : "";
|
|
4
|
+
return value || "GET";
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function readSubmitOverride(submitter, attributeName, propertyName) {
|
|
8
|
+
if (!submitter) return "";
|
|
9
|
+
if (typeof submitter.getAttribute === "function") {
|
|
10
|
+
const attrValue = submitter.getAttribute(attributeName);
|
|
11
|
+
if (typeof attrValue === "string" && attrValue.length > 0) {
|
|
12
|
+
return attrValue;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
const propertyValue = submitter[propertyName];
|
|
16
|
+
return typeof propertyValue === "string" ? propertyValue : "";
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function resolveFormTargetUrl(form, submitter) {
|
|
20
|
+
const action =
|
|
21
|
+
readSubmitOverride(submitter, "formaction", "formAction") ||
|
|
22
|
+
form.getAttribute("action") ||
|
|
23
|
+
form.action ||
|
|
24
|
+
window.location.href;
|
|
25
|
+
return new URL(action, window.location.href);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function resolveFormTargetValue(form, submitter) {
|
|
29
|
+
const target = readSubmitOverride(submitter, "formtarget", "formTarget") || form.target || "";
|
|
30
|
+
return String(target || "").trim();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function resolveFormMethod(form, submitter) {
|
|
34
|
+
return normalizeFormMethod(
|
|
35
|
+
readSubmitOverride(submitter, "formmethod", "formMethod") || form.getAttribute("method") || form.method || "GET"
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function resolveFormEnctype(form, submitter) {
|
|
40
|
+
return String(
|
|
41
|
+
readSubmitOverride(submitter, "formenctype", "formEnctype") ||
|
|
42
|
+
form.getAttribute("enctype") ||
|
|
43
|
+
form.enctype ||
|
|
44
|
+
"application/x-www-form-urlencoded"
|
|
45
|
+
).toLowerCase();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function createFormSubmissionPayload(form, submitter) {
|
|
49
|
+
try {
|
|
50
|
+
return submitter ? new FormData(form, submitter) : new FormData(form);
|
|
51
|
+
} catch {
|
|
52
|
+
const formData = new FormData(form);
|
|
53
|
+
if (submitter && submitter.name) {
|
|
54
|
+
formData.append(submitter.name, submitter.value || "");
|
|
55
|
+
}
|
|
56
|
+
return formData;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function shouldEnhanceForm(form, submitter) {
|
|
61
|
+
if (!form || typeof form.getAttribute !== "function") return false;
|
|
62
|
+
if (!form.hasAttribute("data-zen-form")) return false;
|
|
63
|
+
const target = resolveFormTargetValue(form, submitter);
|
|
64
|
+
if (target && target !== "_self") return false;
|
|
65
|
+
if (resolveFormMethod(form, submitter) !== "POST") return false;
|
|
66
|
+
if (resolveFormEnctype(form, submitter).includes("multipart/form-data")) return false;
|
|
67
|
+
|
|
68
|
+
const targetUrl = resolveFormTargetUrl(form, submitter);
|
|
69
|
+
if (targetUrl.origin !== window.location.origin) return false;
|
|
70
|
+
const resolved = resolveRoute(targetUrl.pathname);
|
|
71
|
+
return !!resolved && requiresServerReload(resolved.route);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function performEnhancedFormSubmission(form, submitter) {
|
|
75
|
+
const targetUrl = resolveFormTargetUrl(form, submitter);
|
|
76
|
+
const resolved = resolveRoute(targetUrl.pathname);
|
|
77
|
+
if (!resolved) {
|
|
78
|
+
navigateViaBrowser(targetUrl, false);
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const context = beginNavigation(targetUrl, resolved, "push");
|
|
83
|
+
let historyCommitted = false;
|
|
84
|
+
let documentDetail = null;
|
|
85
|
+
try {
|
|
86
|
+
dispatchRouteEvent("navigation:request", buildNavigationPayload(context));
|
|
87
|
+
context.stage = "fetch";
|
|
88
|
+
const response = await fetch(targetUrl.href, {
|
|
89
|
+
method: "POST",
|
|
90
|
+
body: createFormSubmissionPayload(form, submitter),
|
|
91
|
+
credentials: "include",
|
|
92
|
+
headers: { Accept: "text/html,application/xhtml+xml" },
|
|
93
|
+
redirect: "manual",
|
|
94
|
+
signal: context.signal
|
|
95
|
+
});
|
|
96
|
+
if (!ensureCurrentNavigation(context)) return false;
|
|
97
|
+
|
|
98
|
+
if (response.type === "opaqueredirect" || (response.status >= 300 && response.status < 400)) {
|
|
99
|
+
const redirectUrl = resolveRedirectUrl(response.headers.get("location"), targetUrl);
|
|
100
|
+
dispatchNavigationFallback(context, {
|
|
101
|
+
reason: "server-redirect",
|
|
102
|
+
location: redirectUrl.href,
|
|
103
|
+
status: response.status
|
|
104
|
+
});
|
|
105
|
+
navigateViaBrowser(redirectUrl, false);
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (!isHtmlResponse(response) || (response.status !== 200 && response.status !== 400 && response.status !== 422)) {
|
|
110
|
+
dispatchNavigationFallback(context, {
|
|
111
|
+
reason: "http-status",
|
|
112
|
+
status: response.status
|
|
113
|
+
});
|
|
114
|
+
navigateViaBrowser(targetUrl, false);
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const html = await response.text();
|
|
119
|
+
if (!ensureCurrentNavigation(context)) return false;
|
|
120
|
+
const payload = parseDocumentPayload(html);
|
|
121
|
+
if (!payload) {
|
|
122
|
+
dispatchNavigationFallback(context, {
|
|
123
|
+
reason: "document-parse"
|
|
124
|
+
});
|
|
125
|
+
navigateViaBrowser(targetUrl, false);
|
|
126
|
+
return true;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const committed = await commitNavigationDocument(
|
|
130
|
+
context,
|
|
131
|
+
resolved,
|
|
132
|
+
targetUrl,
|
|
133
|
+
"push",
|
|
134
|
+
null,
|
|
135
|
+
payload,
|
|
136
|
+
response
|
|
137
|
+
);
|
|
138
|
+
documentDetail = committed.documentDetail;
|
|
139
|
+
historyCommitted = committed.historyCommitted;
|
|
140
|
+
if (!committed.committed) return false;
|
|
141
|
+
return true;
|
|
142
|
+
} catch (error) {
|
|
143
|
+
if (!isAbortError(error)) {
|
|
144
|
+
emitNavigationError(context, {
|
|
145
|
+
reason: "runtime-failure",
|
|
146
|
+
error,
|
|
147
|
+
historyCommitted,
|
|
148
|
+
document: documentDetail
|
|
149
|
+
});
|
|
150
|
+
console.error("[Zenith Router] form submission failed", error);
|
|
151
|
+
dispatchNavigationFallback(context, {
|
|
152
|
+
reason: "runtime-failure",
|
|
153
|
+
historyCommitted
|
|
154
|
+
});
|
|
155
|
+
navigateViaBrowser(targetUrl, false);
|
|
156
|
+
return true;
|
|
157
|
+
}
|
|
158
|
+
dispatchNavigationFallback(context, context.abortReason || {
|
|
159
|
+
reason: "superseded",
|
|
160
|
+
abortedStage: context.stage
|
|
161
|
+
});
|
|
162
|
+
return false;
|
|
163
|
+
} finally {
|
|
164
|
+
completeNavigation(context);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function installEnhancedFormHandling() {
|
|
169
|
+
document.addEventListener("submit", function(event) {
|
|
170
|
+
if (event.defaultPrevented) return;
|
|
171
|
+
const form = event.target;
|
|
172
|
+
const submitter = event.submitter || null;
|
|
173
|
+
if (!shouldEnhanceForm(form, submitter)) return;
|
|
174
|
+
|
|
175
|
+
event.preventDefault();
|
|
176
|
+
performEnhancedFormSubmission(form, submitter).catch(function(error) {
|
|
177
|
+
if (!isAbortError(error)) {
|
|
178
|
+
console.error("[Zenith Router] enhanced form submission failed", error);
|
|
179
|
+
navigateViaBrowser(resolveFormTargetUrl(form, submitter), false);
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
}`;
|
|
184
|
+
}
|
package/template-navigation.js
CHANGED
|
@@ -1,53 +1,8 @@
|
|
|
1
1
|
export function renderRouterNavigationSource() {
|
|
2
|
-
return `function
|
|
3
|
-
if (!
|
|
4
|
-
|
|
5
|
-
if (!ssrScript) return {};
|
|
6
|
-
const source = typeof ssrScript.textContent === "string" ? ssrScript.textContent : "";
|
|
7
|
-
const marker = "window.__zenith_ssr_data =";
|
|
8
|
-
const markerIndex = source.indexOf(marker);
|
|
9
|
-
if (markerIndex === -1) return {};
|
|
10
|
-
const jsonText = source.slice(markerIndex + marker.length).trim().replace(/;$/, "");
|
|
11
|
-
try {
|
|
12
|
-
return JSON.parse(jsonText);
|
|
13
|
-
} catch {
|
|
14
|
-
return {};
|
|
2
|
+
return `async function requestRouteCheck(context, resolved, targetUrl, signal) {
|
|
3
|
+
if (!__ZENITH_ROUTE_CHECK_ENABLED__) {
|
|
4
|
+
return { kind: "allow" };
|
|
15
5
|
}
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function parseDocumentPayload(html) {
|
|
19
|
-
if (typeof DOMParser === "undefined") return null;
|
|
20
|
-
const parsed = new DOMParser().parseFromString(html, "text/html");
|
|
21
|
-
return {
|
|
22
|
-
html,
|
|
23
|
-
title: parsed.title || "",
|
|
24
|
-
ssrData: extractSsrData(parsed)
|
|
25
|
-
};
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function isHtmlResponse(response) {
|
|
29
|
-
const contentType = String(response.headers.get("content-type") || "");
|
|
30
|
-
return /text\\/html|application\\/xhtml\\+xml/i.test(contentType);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function createDocumentDetail(payload, response) {
|
|
34
|
-
return {
|
|
35
|
-
title: payload && typeof payload.title === "string" ? payload.title : "",
|
|
36
|
-
hasSsrData: !!(payload && payload.ssrData && typeof payload.ssrData === "object"),
|
|
37
|
-
status: response && typeof response.status === "number" ? response.status : 200
|
|
38
|
-
};
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function createScrollDetail(targetUrl, scrollTarget) {
|
|
42
|
-
return {
|
|
43
|
-
mode: scrollTarget.mode,
|
|
44
|
-
x: scrollTarget.x,
|
|
45
|
-
y: scrollTarget.y,
|
|
46
|
-
hash: targetUrl.hash || ""
|
|
47
|
-
};
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
async function requestRouteCheck(context, resolved, targetUrl, signal) {
|
|
51
6
|
if (!requiresServerReload(resolved.route)) {
|
|
52
7
|
return { kind: "allow" };
|
|
53
8
|
}
|
|
@@ -147,6 +102,109 @@ function dispatchNavigationFallback(context, detail) {
|
|
|
147
102
|
emitNavigationAbort(context, detail);
|
|
148
103
|
}
|
|
149
104
|
|
|
105
|
+
async function commitNavigationDocument(context, resolved, targetUrl, historyMode, popstateState, payload, response) {
|
|
106
|
+
const documentDetail = createDocumentDetail(payload, response);
|
|
107
|
+
let historyCommitted = false;
|
|
108
|
+
|
|
109
|
+
context.stage = "data-ready";
|
|
110
|
+
emitNavigationEvent(context, "navigation:data-ready", {
|
|
111
|
+
document: documentDetail
|
|
112
|
+
}, false);
|
|
113
|
+
|
|
114
|
+
dispatchScrollEvent("before", {
|
|
115
|
+
navigationType: historyMode,
|
|
116
|
+
to: targetUrl.href,
|
|
117
|
+
from: context.fromUrl ? context.fromUrl.href : window.location.href
|
|
118
|
+
}, false);
|
|
119
|
+
|
|
120
|
+
context.stage = "before-leave";
|
|
121
|
+
await emitNavigationEvent(context, "navigation:before-leave", {
|
|
122
|
+
document: documentDetail
|
|
123
|
+
}, true);
|
|
124
|
+
if (!ensureCurrentNavigation(context)) return { committed: false, documentDetail, historyCommitted };
|
|
125
|
+
|
|
126
|
+
context.stage = "leave-complete";
|
|
127
|
+
emitNavigationEvent(context, "navigation:leave-complete", {
|
|
128
|
+
document: documentDetail
|
|
129
|
+
}, false);
|
|
130
|
+
|
|
131
|
+
context.stage = "before-swap";
|
|
132
|
+
await emitNavigationEvent(context, "navigation:before-swap", {
|
|
133
|
+
document: documentDetail
|
|
134
|
+
}, true);
|
|
135
|
+
if (!ensureCurrentNavigation(context)) return { committed: false, documentDetail, historyCommitted };
|
|
136
|
+
|
|
137
|
+
if (historyMode === "push") {
|
|
138
|
+
rememberScrollForKey(currentHistoryKey);
|
|
139
|
+
pushHistoryEntry(targetUrl);
|
|
140
|
+
historyCommitted = true;
|
|
141
|
+
} else if (historyMode === "pop") {
|
|
142
|
+
syncHistoryEntry(popstateState);
|
|
143
|
+
historyCommitted = true;
|
|
144
|
+
}
|
|
145
|
+
currentUrl = new URL(targetUrl.href);
|
|
146
|
+
|
|
147
|
+
if (payload.title) {
|
|
148
|
+
document.title = payload.title;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const mounted = await mountRoute(resolved.route, resolved.params, context.token, payload);
|
|
152
|
+
if (!mounted || !ensureCurrentNavigation(context)) {
|
|
153
|
+
return { committed: false, documentDetail, historyCommitted };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
context.stage = "content-swapped";
|
|
157
|
+
emitNavigationEvent(context, "navigation:content-swapped", {
|
|
158
|
+
document: documentDetail,
|
|
159
|
+
historyCommitted
|
|
160
|
+
}, false);
|
|
161
|
+
|
|
162
|
+
await nextFrame();
|
|
163
|
+
if (!ensureCurrentNavigation(context)) return { committed: false, documentDetail, historyCommitted };
|
|
164
|
+
|
|
165
|
+
const scrollTarget = resolveScrollTarget(targetUrl, historyMode, popstateState);
|
|
166
|
+
const scrollDetail = createScrollDetail(targetUrl, scrollTarget);
|
|
167
|
+
const defaultScrollAllowed = dispatchScrollEvent("apply", {
|
|
168
|
+
navigationType: historyMode,
|
|
169
|
+
mode: scrollDetail.mode,
|
|
170
|
+
x: scrollDetail.x,
|
|
171
|
+
y: scrollDetail.y,
|
|
172
|
+
hash: scrollDetail.hash
|
|
173
|
+
}, true);
|
|
174
|
+
if (defaultScrollAllowed) {
|
|
175
|
+
applyNativeScroll(scrollTarget);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
focusAfterNavigation(scrollTarget);
|
|
179
|
+
rememberScrollForKey(currentHistoryKey, { x: scrollTarget.x, y: scrollTarget.y });
|
|
180
|
+
|
|
181
|
+
context.stage = "before-enter";
|
|
182
|
+
await emitNavigationEvent(context, "navigation:before-enter", {
|
|
183
|
+
document: documentDetail,
|
|
184
|
+
scroll: scrollDetail
|
|
185
|
+
}, true);
|
|
186
|
+
if (!ensureCurrentNavigation(context)) return { committed: false, documentDetail, historyCommitted };
|
|
187
|
+
|
|
188
|
+
dispatchScrollEvent("after", {
|
|
189
|
+
navigationType: historyMode,
|
|
190
|
+
mode: scrollDetail.mode,
|
|
191
|
+
x: scrollDetail.x,
|
|
192
|
+
y: scrollDetail.y,
|
|
193
|
+
hash: scrollDetail.hash
|
|
194
|
+
}, false);
|
|
195
|
+
|
|
196
|
+
await nextFrame();
|
|
197
|
+
if (!ensureCurrentNavigation(context)) return { committed: false, documentDetail, historyCommitted };
|
|
198
|
+
|
|
199
|
+
context.stage = "enter-complete";
|
|
200
|
+
emitNavigationEvent(context, "navigation:enter-complete", {
|
|
201
|
+
document: documentDetail,
|
|
202
|
+
scroll: scrollDetail
|
|
203
|
+
}, false);
|
|
204
|
+
|
|
205
|
+
return { committed: true, documentDetail, historyCommitted };
|
|
206
|
+
}
|
|
207
|
+
|
|
150
208
|
async function performNavigation(targetUrl, historyMode, popstateState) {
|
|
151
209
|
const resolved = resolveRoute(targetUrl.pathname);
|
|
152
210
|
if (!resolved) {
|
|
@@ -232,102 +290,18 @@ async function performNavigation(targetUrl, historyMode, popstateState) {
|
|
|
232
290
|
navigateViaBrowser(targetUrl, historyMode === "pop");
|
|
233
291
|
return true;
|
|
234
292
|
}
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
}, false);
|
|
248
|
-
|
|
249
|
-
context.stage = "before-leave";
|
|
250
|
-
await emitNavigationEvent(context, "navigation:before-leave", {
|
|
251
|
-
document: documentDetail
|
|
252
|
-
}, true);
|
|
253
|
-
if (!ensureCurrentNavigation(context)) return false;
|
|
254
|
-
|
|
255
|
-
context.stage = "leave-complete";
|
|
256
|
-
emitNavigationEvent(context, "navigation:leave-complete", {
|
|
257
|
-
document: documentDetail
|
|
258
|
-
}, false);
|
|
259
|
-
|
|
260
|
-
context.stage = "before-swap";
|
|
261
|
-
await emitNavigationEvent(context, "navigation:before-swap", {
|
|
262
|
-
document: documentDetail
|
|
263
|
-
}, true);
|
|
264
|
-
if (!ensureCurrentNavigation(context)) return false;
|
|
265
|
-
|
|
266
|
-
if (historyMode === "push") {
|
|
267
|
-
rememberScrollForKey(currentHistoryKey);
|
|
268
|
-
pushHistoryEntry(targetUrl);
|
|
269
|
-
historyCommitted = true;
|
|
270
|
-
} else if (historyMode === "pop") {
|
|
271
|
-
syncHistoryEntry(popstateState);
|
|
272
|
-
historyCommitted = true;
|
|
273
|
-
}
|
|
274
|
-
currentUrl = new URL(targetUrl.href);
|
|
275
|
-
|
|
276
|
-
if (payload.title) {
|
|
277
|
-
document.title = payload.title;
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
const mounted = await mountRoute(resolved.route, resolved.params, context.token, payload);
|
|
281
|
-
if (!mounted || !ensureCurrentNavigation(context)) return false;
|
|
282
|
-
|
|
283
|
-
context.stage = "content-swapped";
|
|
284
|
-
emitNavigationEvent(context, "navigation:content-swapped", {
|
|
285
|
-
document: documentDetail,
|
|
286
|
-
historyCommitted
|
|
287
|
-
}, false);
|
|
288
|
-
|
|
289
|
-
await nextFrame();
|
|
290
|
-
if (!ensureCurrentNavigation(context)) return false;
|
|
291
|
-
|
|
292
|
-
const scrollTarget = resolveScrollTarget(targetUrl, historyMode, popstateState);
|
|
293
|
-
const scrollDetail = createScrollDetail(targetUrl, scrollTarget);
|
|
294
|
-
const defaultScrollAllowed = dispatchScrollEvent("apply", {
|
|
295
|
-
navigationType: historyMode,
|
|
296
|
-
mode: scrollDetail.mode,
|
|
297
|
-
x: scrollDetail.x,
|
|
298
|
-
y: scrollDetail.y,
|
|
299
|
-
hash: scrollDetail.hash
|
|
300
|
-
}, true);
|
|
301
|
-
if (defaultScrollAllowed) {
|
|
302
|
-
applyNativeScroll(scrollTarget);
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
focusAfterNavigation(scrollTarget);
|
|
306
|
-
rememberScrollForKey(currentHistoryKey, { x: scrollTarget.x, y: scrollTarget.y });
|
|
307
|
-
|
|
308
|
-
context.stage = "before-enter";
|
|
309
|
-
await emitNavigationEvent(context, "navigation:before-enter", {
|
|
310
|
-
document: documentDetail,
|
|
311
|
-
scroll: scrollDetail
|
|
312
|
-
}, true);
|
|
313
|
-
if (!ensureCurrentNavigation(context)) return false;
|
|
314
|
-
|
|
315
|
-
dispatchScrollEvent("after", {
|
|
316
|
-
navigationType: historyMode,
|
|
317
|
-
mode: scrollDetail.mode,
|
|
318
|
-
x: scrollDetail.x,
|
|
319
|
-
y: scrollDetail.y,
|
|
320
|
-
hash: scrollDetail.hash
|
|
321
|
-
}, false);
|
|
322
|
-
|
|
323
|
-
await nextFrame();
|
|
324
|
-
if (!ensureCurrentNavigation(context)) return false;
|
|
325
|
-
|
|
326
|
-
context.stage = "enter-complete";
|
|
327
|
-
emitNavigationEvent(context, "navigation:enter-complete", {
|
|
328
|
-
document: documentDetail,
|
|
329
|
-
scroll: scrollDetail
|
|
330
|
-
}, false);
|
|
293
|
+
const committed = await commitNavigationDocument(
|
|
294
|
+
context,
|
|
295
|
+
resolved,
|
|
296
|
+
targetUrl,
|
|
297
|
+
historyMode,
|
|
298
|
+
popstateState,
|
|
299
|
+
payload,
|
|
300
|
+
response
|
|
301
|
+
);
|
|
302
|
+
documentDetail = committed.documentDetail;
|
|
303
|
+
historyCommitted = committed.historyCommitted;
|
|
304
|
+
if (!committed.committed) return false;
|
|
331
305
|
|
|
332
306
|
return true;
|
|
333
307
|
} catch (error) {
|
|
@@ -387,6 +361,7 @@ function start() {
|
|
|
387
361
|
currentUrl = new URL(window.location.href);
|
|
388
362
|
queueScrollSnapshot();
|
|
389
363
|
});
|
|
364
|
+
installEnhancedFormHandling();
|
|
390
365
|
|
|
391
366
|
document.addEventListener("click", function(event) {
|
|
392
367
|
if (event.defaultPrevented || event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
|
package/template.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { renderRouterCoreSource } from './template-core.js';
|
|
2
|
+
import { renderRouterDocumentSource } from './template-document.js';
|
|
3
|
+
import { renderRouterFormSource } from './template-form.js';
|
|
2
4
|
import { renderRouterLifecycleSource } from './template-lifecycle.js';
|
|
3
5
|
import { renderRouterNavigationSource } from './template-navigation.js';
|
|
4
6
|
|
|
@@ -19,7 +21,7 @@ export function renderRouterModule(opts) {
|
|
|
19
21
|
throw new Error('renderRouterModule(opts) requires an options object');
|
|
20
22
|
}
|
|
21
23
|
|
|
22
|
-
const { manifestJson, runtimeImport, coreImport } = opts;
|
|
24
|
+
const { manifestJson, runtimeImport, coreImport, routeCheck = false } = opts;
|
|
23
25
|
if (typeof manifestJson !== 'string' || manifestJson.length === 0) {
|
|
24
26
|
throw new Error('renderRouterModule(opts) requires opts.manifestJson string');
|
|
25
27
|
}
|
|
@@ -34,5 +36,5 @@ export function renderRouterModule(opts) {
|
|
|
34
36
|
const runtimeSpec = sanitizeImportSpecifier(runtimeImport);
|
|
35
37
|
const coreSpec = sanitizeImportSpecifier(coreImport);
|
|
36
38
|
|
|
37
|
-
return `${renderRouterCoreSource({ manifest, runtimeSpec, coreSpec })}\n\n${renderRouterLifecycleSource()}\n\n${renderRouterNavigationSource()}\n`;
|
|
39
|
+
return `${renderRouterCoreSource({ manifest, runtimeSpec, coreSpec, routeCheck })}\n\n${renderRouterDocumentSource()}\n\n${renderRouterLifecycleSource()}\n\n${renderRouterNavigationSource()}\n\n${renderRouterFormSource()}\n`;
|
|
38
40
|
}
|