@webjsdev/core 0.7.50 → 0.7.53
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/webjs-core-browser.js +5 -5
- package/dist/webjs-core-browser.js.map +3 -3
- package/dist/webjs-core-testing.js +20 -20
- package/dist/webjs-core-testing.js.map +3 -3
- package/dist/webjs-core.js +7 -7
- package/dist/webjs-core.js.map +3 -3
- package/index-browser.js +1 -1
- package/index.d.ts +3 -3
- package/index.js +1 -1
- package/package.json +1 -1
- package/src/component/lifecycle.js +1481 -0
- package/src/component/reactive.js +171 -0
- package/src/component/server-element.js +248 -0
- package/src/component.js +2 -1896
- package/src/css.d.ts +9 -0
- package/src/escape.d.ts +2 -0
- package/src/html.d.ts +9 -0
- package/src/render-client/parts.js +2039 -0
- package/src/render-client/reconciler.js +388 -0
- package/src/render-client/template-compiler.js +525 -0
- package/src/render-client.js +1 -2938
- package/src/render-server/attrs.js +160 -0
- package/src/render-server/dsd.js +445 -0
- package/src/render-server/html-scan.js +402 -0
- package/src/render-server/slots.js +213 -0
- package/src/render-server/stream.js +163 -0
- package/src/render-server/template-renderer.js +938 -0
- package/src/render-server/text.js +163 -0
- package/src/render-server.js +6 -2405
- package/src/repeat.d.ts +15 -0
- package/src/rich-fetch.d.ts +4 -0
- package/src/router-client/anchors.js +44 -0
- package/src/router-client/boundaries.js +221 -0
- package/src/router-client/constants.js +206 -0
- package/src/router-client/diagnostics.js +377 -0
- package/src/router-client/dom-differ.js +709 -0
- package/src/router-client/dom-parse.js +150 -0
- package/src/router-client/events.js +182 -0
- package/src/router-client/fetch-apply.js +432 -0
- package/src/router-client/form-encoder.js +135 -0
- package/src/router-client/frames.js +329 -0
- package/src/router-client/head-merge.js +344 -0
- package/src/router-client/nav-error.js +105 -0
- package/src/router-client/navigator.js +952 -0
- package/src/router-client/prefetch.js +831 -0
- package/src/router-client/scroll.js +308 -0
- package/src/router-client/snapshot-cache.js +113 -0
- package/src/router-client/state.js +155 -0
- package/src/router-client/stream.js +206 -0
- package/src/router-client/swap.js +454 -0
- package/src/router-client/upgrade.js +261 -0
- package/src/router-client/view-transition.js +223 -0
- package/src/router-client.d.ts +11 -2
- package/src/router-client.js +78 -5345
- package/src/slot/assignment.js +304 -0
- package/src/slot/interception.js +558 -0
- package/src/slot/polyfills.js +365 -0
- package/src/slot/project.js +432 -0
- package/src/slot/sensors.js +303 -0
- package/src/slot/state.js +245 -0
- package/src/slot/symbols.js +85 -0
- package/src/slot.js +7 -2199
- package/src/suspense.d.ts +9 -0
- package/src/websocket-client.d.ts +22 -0
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client router: diagnostics.
|
|
3
|
+
*
|
|
4
|
+
* Moved verbatim out of the pre-split `router-client.js`; that barrel holds
|
|
5
|
+
* the router's full contract and the public entry points.
|
|
6
|
+
*
|
|
7
|
+
* @module
|
|
8
|
+
*/
|
|
9
|
+
import { FORM_ACTION_FIELD } from '../form-action.js';
|
|
10
|
+
import { FALLBACK_MARKER_KEY } from './constants.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Emit a `console.warn` at most once per `key` for the lifetime of the
|
|
14
|
+
* page, so a repeated misconfiguration (a stale `data-webjs-frame` clicked
|
|
15
|
+
* many times) does not spam the console.
|
|
16
|
+
*
|
|
17
|
+
* @type {Set<string>}
|
|
18
|
+
*/
|
|
19
|
+
const warnedKeys = new Set();
|
|
20
|
+
|
|
21
|
+
/** @param {string} key @param {string} message */
|
|
22
|
+
export function warnOnce(key, message, level = 'warn') {
|
|
23
|
+
if (warnedKeys.has(key)) return;
|
|
24
|
+
warnedKeys.add(key);
|
|
25
|
+
if (typeof console === 'undefined') return;
|
|
26
|
+
const fn = level === 'error' ? console.error : console.warn;
|
|
27
|
+
if (fn) fn.call(console, message);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* DEV-ONLY: report at submit time when a submission is carrying a bound
|
|
32
|
+
* action's identity it cannot actually deliver (#1307).
|
|
33
|
+
*
|
|
34
|
+
* This is the backstop for the shapes the renderer deliberately stopped
|
|
35
|
+
* refusing. A PLAIN `<button formmethod="get">` inside a bound form is a legal
|
|
36
|
+
* native override, so it renders, and the form's action then simply does not
|
|
37
|
+
* run. That is what the author asked for, but it is also what a mistake looks
|
|
38
|
+
* like, and submit time is the only moment the whole picture (the resolved
|
|
39
|
+
* method, the resolved enctype, and whether a bound identity is actually in
|
|
40
|
+
* the body) exists in one place.
|
|
41
|
+
*
|
|
42
|
+
* Observational: it runs BEFORE `preventDefault` and changes nothing about the
|
|
43
|
+
* submission. Silent in production, where a console error would be noise the
|
|
44
|
+
* visitor cannot act on; the server-side `onError` telemetry covers that side.
|
|
45
|
+
*
|
|
46
|
+
* @param {HTMLFormElement} form
|
|
47
|
+
* @param {HTMLElement | null} submitter
|
|
48
|
+
* @param {string} method lowercased, already resolved with native precedence
|
|
49
|
+
* @param {FormData} body
|
|
50
|
+
*/
|
|
51
|
+
export function warnIfActionSubmissionCannotDeliver(form, submitter, method, body) {
|
|
52
|
+
if (typeof process !== 'undefined' && process.env && process.env.NODE_ENV === 'production') return;
|
|
53
|
+
// No identity in the body: an ordinary form with nothing to deliver.
|
|
54
|
+
if (!body.has(FORM_ACTION_FIELD)) return;
|
|
55
|
+
let path = '';
|
|
56
|
+
try { path = new URL(form.getAttribute('action') || location.href, location.href).pathname; }
|
|
57
|
+
catch { path = location.pathname; }
|
|
58
|
+
if (method !== 'post') {
|
|
59
|
+
warnOnce(
|
|
60
|
+
`submit-nowhere:${path}:${method}`,
|
|
61
|
+
`[webjs] this submission carries a bound server action's identity but submits as ${method.toUpperCase()}, which sends no body, so the identity rides the query string and the action never runs. A submitter's own formmethod overrides the form's, and WebJs honours it rather than refusing it, so check for a formmethod on the button that was pressed.`,
|
|
62
|
+
'error',
|
|
63
|
+
);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
const enctype = (submitter && submitter.getAttribute('formenctype'))
|
|
67
|
+
|| form.getAttribute('enctype')
|
|
68
|
+
|| 'application/x-www-form-urlencoded';
|
|
69
|
+
// `text/plain` ONLY, not the renderer's parseable-enctype allowlist.
|
|
70
|
+
// `enctype` is an enumerated attribute whose missing AND invalid value
|
|
71
|
+
// defaults are both `application/x-www-form-urlencoded`, so
|
|
72
|
+
// `enctype="nonsense"` submits a perfectly parseable body and the action
|
|
73
|
+
// runs. Testing against the allowlist would report that working form as
|
|
74
|
+
// broken.
|
|
75
|
+
if (enctype.toLowerCase() === 'text/plain') {
|
|
76
|
+
warnOnce(
|
|
77
|
+
`submit-nowhere:${path}:${enctype}`,
|
|
78
|
+
`[webjs] this submission carries a bound server action's identity but declares enctype="${enctype}", which the server cannot parse. The router declines to send it so both paths behave the same way, and both are answered with a 405. Drop the enctype and let the binding supply it.`,
|
|
79
|
+
'error',
|
|
80
|
+
);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
// The identity is going somewhere OTHER than this page (#1307). A bound
|
|
84
|
+
// submitter emits no `formaction` url, so the submission targets whatever the
|
|
85
|
+
// FORM targets, and a form declaring its own `action="/x"` sends its buttons
|
|
86
|
+
// to `/x` by ordinary native precedence.
|
|
87
|
+
//
|
|
88
|
+
// This is the one shape the redesign left both unrefused and, until here,
|
|
89
|
+
// unreported. The renderer used to throw for it, but only where it could SEE
|
|
90
|
+
// the form, which is exactly the cross-element judgement that could not be
|
|
91
|
+
// made from inside a component. So it is reported at submit time instead,
|
|
92
|
+
// where the resolved target is a fact rather than an inference.
|
|
93
|
+
//
|
|
94
|
+
// A warning rather than an error, because it is not necessarily wrong: if
|
|
95
|
+
// `/x` is a PAGE route the action really does run there, and the 422
|
|
96
|
+
// re-render simply lands on that page. It is only dead if `/x` is a
|
|
97
|
+
// `route.ts`, another origin, or nothing at all, and the client cannot tell
|
|
98
|
+
// which from here.
|
|
99
|
+
if (path && path !== location.pathname) {
|
|
100
|
+
warnOnce(
|
|
101
|
+
`submit-elsewhere:${path}`,
|
|
102
|
+
`[webjs] this submission carries a bound server action's identity but posts to "${path}" rather than this page, because the enclosing <form> declares its own action. A bound submitter emits no formaction, so the form's target wins, which is what native HTML does. The action runs only if "${path}" is a page route; against a route.ts or another origin the identity is ignored and nothing runs. Drop the form's action attribute to keep the submission on this page.`,
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* True when a nav must degrade to a full page load because the document is
|
|
109
|
+
* still parsing. A forward, main-document nav fired at `readyState: 'loading'`
|
|
110
|
+
* races the DOM: the leaving page's closing layout markers may not be attached
|
|
111
|
+
* yet, so a soft swap would snapshot an incomplete tree and corrupt the DOM
|
|
112
|
+
* (#1008 / #936). Scoped to frameless forward navs (popstate is browser-driven,
|
|
113
|
+
* a frame nav carries its own boundary element).
|
|
114
|
+
*
|
|
115
|
+
* @param {boolean} isPopState
|
|
116
|
+
* @param {string | null | undefined} frameId
|
|
117
|
+
* @returns {boolean}
|
|
118
|
+
*/
|
|
119
|
+
export function shouldFullLoadDuringParse(isPopState, frameId) {
|
|
120
|
+
return (
|
|
121
|
+
!isPopState &&
|
|
122
|
+
!frameId &&
|
|
123
|
+
typeof document !== 'undefined' &&
|
|
124
|
+
document.readyState === 'loading'
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Has the pre-boot check already run for THIS document (#1118)? Module scope,
|
|
130
|
+
* so it resets with the document, which is the lifetime the report is about.
|
|
131
|
+
*/
|
|
132
|
+
let reportedPreBoot = false;
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Was THIS document load a same-origin navigation the client router never saw?
|
|
136
|
+
*
|
|
137
|
+
* Pure so the branch logic is testable without driving a real navigation
|
|
138
|
+
* (#1118). Every argument is read from the environment by the one caller.
|
|
139
|
+
*
|
|
140
|
+
* @param {string} navType `performance.getEntriesByType('navigation')[0].type`.
|
|
141
|
+
* Only `'navigate'` qualifies: a `'reload'` and a `'back_forward'` restore are
|
|
142
|
+
* things the browser does, not clicks the router could have intercepted.
|
|
143
|
+
* @param {string} referrer `document.referrer`. Must parse to the same origin as
|
|
144
|
+
* `href`: an empty referrer means a typed URL or an external entry (no router
|
|
145
|
+
* was running to miss the click), and a cross-origin one means the previous
|
|
146
|
+
* page was not ours.
|
|
147
|
+
* @param {string} href `location.href` of the document that just loaded.
|
|
148
|
+
* @param {string | null} marker the consumed `FALLBACK_MARKER_KEY` value. When
|
|
149
|
+
* it equals `href` the router already reported this load under its own cause,
|
|
150
|
+
* so counting it again would double-count a known degradation as an unknown.
|
|
151
|
+
* @returns {boolean}
|
|
152
|
+
*/
|
|
153
|
+
export function isPreBootNavigation(navType, referrer, href, marker) {
|
|
154
|
+
if (navType !== 'navigate') return false;
|
|
155
|
+
if (!referrer) return false;
|
|
156
|
+
if (marker && marker === href) return false;
|
|
157
|
+
try {
|
|
158
|
+
return new URL(referrer).origin === new URL(href).origin;
|
|
159
|
+
} catch {
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Report a document load that reached us by a same-origin navigation the router
|
|
166
|
+
* did not soft-navigate (#1118).
|
|
167
|
+
*
|
|
168
|
+
* A module script is deferred by spec, so it runs only after HTML parsing
|
|
169
|
+
* completes, while the links it will intercept are clickable from first paint.
|
|
170
|
+
* That window cannot be closed from inside the router (see #1118 for why an
|
|
171
|
+
* inline capture shim was rejected), so it is MEASURED instead: this turns the
|
|
172
|
+
* frequency into a production number a deployed app can read off the existing
|
|
173
|
+
* `webjs:navigation-fallback` channel, rather than folklore.
|
|
174
|
+
*
|
|
175
|
+
* Deliberately imprecise, and the docs say so: a `data-no-router` link, a
|
|
176
|
+
* cross-document form post, and an app that opted out of the client router all
|
|
177
|
+
* land here too. The signal is the RATE, not any single event.
|
|
178
|
+
*
|
|
179
|
+
* `willReload` is false because the document load has already happened. That is
|
|
180
|
+
* exactly the distinction the flag was added for.
|
|
181
|
+
*/
|
|
182
|
+
export function reportPreBootNavigation() {
|
|
183
|
+
// Same guard the scroll/current-page seeding above uses: a DOM shim without a
|
|
184
|
+
// `location` (linkedom under the unit runner) is not a document load to
|
|
185
|
+
// report on, and reading through would throw inside the boot.
|
|
186
|
+
if (typeof location === 'undefined') return;
|
|
187
|
+
// Once per DOCUMENT, not once per enable. `enableClientRouter` is re-callable
|
|
188
|
+
// after `disableClientRouter()` (the documented per-moment opt-out), and this
|
|
189
|
+
// reports on the load that produced the document, which does not happen again
|
|
190
|
+
// when the router is toggled back on. Without this, an app that toggles would
|
|
191
|
+
// emit a duplicate for a single load and inflate the very rate the report
|
|
192
|
+
// exists to measure. The marker is already consumed by then, so it cannot
|
|
193
|
+
// suppress the duplicate on its own.
|
|
194
|
+
if (reportedPreBoot) return;
|
|
195
|
+
reportedPreBoot = true;
|
|
196
|
+
/** @type {string | null} */
|
|
197
|
+
let marker = null;
|
|
198
|
+
try {
|
|
199
|
+
marker = sessionStorage.getItem(FALLBACK_MARKER_KEY);
|
|
200
|
+
// Consume unconditionally, even when it does not match: a stale marker left
|
|
201
|
+
// by an earlier navigation must never suppress a later real one.
|
|
202
|
+
sessionStorage.removeItem(FALLBACK_MARKER_KEY);
|
|
203
|
+
} catch {
|
|
204
|
+
// No marker available. Treated as absent, which can only over-report.
|
|
205
|
+
}
|
|
206
|
+
let navType = '';
|
|
207
|
+
try {
|
|
208
|
+
const nav = performance.getEntriesByType('navigation')[0];
|
|
209
|
+
navType = nav ? /** @type {PerformanceNavigationTiming} */ (nav).type : '';
|
|
210
|
+
} catch {
|
|
211
|
+
// No Navigation Timing Level 2 entry. Without a nav type the check cannot
|
|
212
|
+
// exclude a reload, so it reports nothing rather than guessing.
|
|
213
|
+
}
|
|
214
|
+
if (isPreBootNavigation(navType, document.referrer, location.href, marker)) {
|
|
215
|
+
reportFallback('pre-boot-navigation', location.href, false);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* The client router degraded a soft navigation. Records WHY (the `cause`), so
|
|
221
|
+
* "why did my SPA nav do a full reload?" is answerable instead of guessed at.
|
|
222
|
+
*
|
|
223
|
+
* Two channels, deliberately different in reach:
|
|
224
|
+
*
|
|
225
|
+
* - A **`webjs:navigation-fallback` event on `document`, in EVERY environment**,
|
|
226
|
+
* detail `{ cause, href, willReload }`. Dispatch convention matches
|
|
227
|
+
* `webjs:navigate` / `webjs:prefetch` / `webjs:navigation-error`.
|
|
228
|
+
* - A dev-only console warning, deduped per cause so a repeat does not spam.
|
|
229
|
+
*
|
|
230
|
+
* The event exists because the console warning alone made this class of bug
|
|
231
|
+
* UNDIAGNOSABLE in production (#1114). A degradation is correct behaviour, not
|
|
232
|
+
* an error, so nothing was logged and nothing was thrown, and a deployed app had
|
|
233
|
+
* no way to observe that a click had turned into a full document load. The
|
|
234
|
+
* user-visible symptoms (a loading spinner in the browser tab, a whole-document
|
|
235
|
+
* flash including preserved chrome) were then attributed to a styling problem
|
|
236
|
+
* for a full investigation cycle, because the actual cause emitted no signal.
|
|
237
|
+
*
|
|
238
|
+
* An event costs nothing when nobody listens, is greppable from a page console,
|
|
239
|
+
* and lets a deployed app wire this to analytics. It is NOT cancelable: by the
|
|
240
|
+
* time this fires the decision to degrade is already made and is the only safe
|
|
241
|
+
* option (#1015 chose a bounded full load over a heuristic recovery that could
|
|
242
|
+
* corrupt the DOM silently), so there is nothing for a listener to veto.
|
|
243
|
+
*
|
|
244
|
+
* @param {string} cause a short stable slug for the degradation reason
|
|
245
|
+
* @param {string} href the destination the router fell back to loading
|
|
246
|
+
* @param {boolean} [willReload] true when a full document load follows (the
|
|
247
|
+
* default). False for a degradation that does NOT reload, so a listener can
|
|
248
|
+
* tell "this click became a document load" from "this background op was
|
|
249
|
+
* dropped", which are very different user-visible events.
|
|
250
|
+
*/
|
|
251
|
+
export function reportFallback(cause, href, willReload = true) {
|
|
252
|
+
if (willReload) {
|
|
253
|
+
// Leave a marker naming the destination this full load is going to
|
|
254
|
+
// (#1118). The next document's boot reads it to tell "the router itself
|
|
255
|
+
// chose this full load, and already reported it under its own cause" from
|
|
256
|
+
// "a same-origin navigation the router never saw", which is the pre-boot
|
|
257
|
+
// click window. Best-effort: `sessionStorage` throws in some privacy modes
|
|
258
|
+
// and partitioned contexts, and a diagnostic must never break a navigation.
|
|
259
|
+
try {
|
|
260
|
+
sessionStorage.setItem(FALLBACK_MARKER_KEY, href);
|
|
261
|
+
} catch {
|
|
262
|
+
// Without the marker the next boot may attribute this load to the
|
|
263
|
+
// pre-boot window. That is a false positive in a diagnostic, which is
|
|
264
|
+
// strictly better than a thrown navigation.
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
if (typeof document !== 'undefined' && typeof CustomEvent !== 'undefined') {
|
|
268
|
+
try {
|
|
269
|
+
document.dispatchEvent(new CustomEvent('webjs:navigation-fallback', {
|
|
270
|
+
detail: { cause, href, willReload },
|
|
271
|
+
}));
|
|
272
|
+
} catch {
|
|
273
|
+
// A listener that throws must never turn a correct degradation into a
|
|
274
|
+
// broken navigation. Diagnostics are strictly best-effort.
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
if (typeof process !== 'undefined' && process.env && process.env.NODE_ENV === 'production') return;
|
|
278
|
+
// Word the warning from `willReload`: several causes deliberately do NOT
|
|
279
|
+
// reload (a suppressed deploy reload, a discarded background revalidation),
|
|
280
|
+
// and claiming a full page load for those sends a reader hunting the wrong
|
|
281
|
+
// symptom.
|
|
282
|
+
warnOnce(
|
|
283
|
+
`fallback:${cause}`,
|
|
284
|
+
willReload
|
|
285
|
+
? `[webjs] client router fell back to a full page load (${cause}) navigating to ${href}. This is correct (no DOM corruption), just not a soft nav.`
|
|
286
|
+
: `[webjs] client router degraded a soft navigation (${cause}) for ${href}, without a full page load.`
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Dev-only, fire-once-per-id hint: a streamed Suspense resolution arrived but
|
|
292
|
+
* its boundary placeholder was not in the DOM, so it was dropped (#1051). This
|
|
293
|
+
* is benign when the navigation was superseded, degraded to a full load, or
|
|
294
|
+
* discarded, but a stuck skeleton that is NONE of those has no other signal (it
|
|
295
|
+
* is what made the #1048 view-transition race hard to diagnose). Never warns in
|
|
296
|
+
* production, never throws.
|
|
297
|
+
*
|
|
298
|
+
* @param {string} id the streamed boundary id that could not be applied
|
|
299
|
+
*/
|
|
300
|
+
export function warnDropped(id) {
|
|
301
|
+
if (typeof process !== 'undefined' && process.env && process.env.NODE_ENV === 'production') return;
|
|
302
|
+
warnOnce(
|
|
303
|
+
`stream-drop:${id}`,
|
|
304
|
+
`[webjs] dropped a streamed Suspense resolve for "${id}": no #${id} boundary in the DOM. Benign if this navigation was superseded or degraded. A stuck skeleton here means the shell swap did not place the boundary.`
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Dev-only, fire-once hint: the router forces an INSTANT scroll-to-top on a
|
|
310
|
+
* forward navigation (matching a native page load), so an app-level
|
|
311
|
+
* `scroll-behavior: smooth` on <html> does not affect route transitions (it
|
|
312
|
+
* still applies to in-page #anchor links via `scrollIntoView`). A developer
|
|
313
|
+
* who set smooth expecting smooth nav scrolling would otherwise be puzzled.
|
|
314
|
+
* Also flags the iOS sticky-`backdrop-filter` flash this combination can
|
|
315
|
+
* cause (#610). Never warns in production, never throws.
|
|
316
|
+
*
|
|
317
|
+
* The `smoothScrollChecked` flag gates the `getComputedStyle` read (a forced
|
|
318
|
+
* style flush) to AT MOST ONCE per page, so a dev session does not pay a
|
|
319
|
+
* per-navigation reflow after the first forward nav.
|
|
320
|
+
*/
|
|
321
|
+
let smoothScrollChecked = false;
|
|
322
|
+
|
|
323
|
+
export function warnIfSmoothScrollOnHtml() {
|
|
324
|
+
if (typeof process !== 'undefined' && process.env && process.env.NODE_ENV === 'production') return;
|
|
325
|
+
if (smoothScrollChecked) return;
|
|
326
|
+
if (typeof document === 'undefined' || typeof getComputedStyle !== 'function') return;
|
|
327
|
+
const root = document.documentElement;
|
|
328
|
+
if (!root) return;
|
|
329
|
+
smoothScrollChecked = true;
|
|
330
|
+
let behavior;
|
|
331
|
+
try { behavior = getComputedStyle(root).scrollBehavior; } catch { return; }
|
|
332
|
+
if (behavior !== 'smooth') return;
|
|
333
|
+
warnOnce(
|
|
334
|
+
'scroll-behavior-smooth-html',
|
|
335
|
+
'[webjs] Detected `scroll-behavior: smooth` on <html>. The client router scrolls ' +
|
|
336
|
+
'to the top instantly on navigation (like a native page load), so route transitions ' +
|
|
337
|
+
'are not affected by it. It still applies to in-page #anchor links. Pairing it with a ' +
|
|
338
|
+
'sticky `backdrop-filter` header can also flash on iOS during navigation.'
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Nav-in-flight signalling. The router can expose `data-navigating` on <html>
|
|
344
|
+
* so an app may style a loading indicator with `html[data-navigating] { … }`.
|
|
345
|
+
*
|
|
346
|
+
* This is OPT-IN, set only when the app marks `<html data-webjs-nav-progress>`.
|
|
347
|
+
* The reason it is not unconditional: toggling ANY attribute on the root
|
|
348
|
+
* re-runs global style resolution, and on WebKit (so every iOS browser, since
|
|
349
|
+
* they all use it) that re-resolves `oklch()` / `color-mix(in oklch, …)` token
|
|
350
|
+
* values to an equivalent oklab representation and repaints them for one frame.
|
|
351
|
+
* On a token-driven theme that is a visible background flash on navigation
|
|
352
|
+
* (#610). The flash only shows on a nav slow enough to reach the deferred set
|
|
353
|
+
* below, which a desktop nav rarely is but a mobile forward fetch routinely is,
|
|
354
|
+
* so the symptom is iOS-and-forward-only. With no opt-in the attribute is never
|
|
355
|
+
* written, so the re-resolution never happens and the flash cannot occur.
|
|
356
|
+
*/
|
|
357
|
+
export function setNavigating(on) {
|
|
358
|
+
if (typeof document === 'undefined') return;
|
|
359
|
+
const root = document.documentElement;
|
|
360
|
+
if (!root || !root.hasAttribute('data-webjs-nav-progress')) return;
|
|
361
|
+
try {
|
|
362
|
+
if (on) root.setAttribute('data-navigating', '');
|
|
363
|
+
else root.removeAttribute('data-navigating');
|
|
364
|
+
} catch { /* non-DOM environment */ }
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/* ====================================================================
|
|
368
|
+
* Boundary discovery (the heart of the partial-swap mechanism, #1015)
|
|
369
|
+
* ==================================================================== */
|
|
370
|
+
|
|
371
|
+
/** Test-only: clear the fire-once warning guards so a case can be re-exercised. */
|
|
372
|
+
export function _resetWarnOnce() { warnedKeys.clear(); smoothScrollChecked = false; }
|
|
373
|
+
|
|
374
|
+
/** Test-only: the readyState-loading full-load degradation predicate (#1008). */
|
|
375
|
+
export function _shouldFullLoadDuringParse(isPopState, frameId) {
|
|
376
|
+
return shouldFullLoadDuringParse(isPopState, frameId);
|
|
377
|
+
}
|