@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,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client router: dom-parse.
|
|
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
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Parse a navigation response into a Document, PRESERVING COMMENTS.
|
|
12
|
+
*
|
|
13
|
+
* Comments are load-bearing here, not incidental: the partial swap pairs on
|
|
14
|
+
* `<!--wj:children:<path>-->` markers and hydration keys off `<!--webjs-hydrate-->`.
|
|
15
|
+
* `Document.parseHTMLUnsafe` would be the natural choice (it is the only
|
|
16
|
+
* single-pass API that also processes Declarative Shadow DOM) but it STRIPS
|
|
17
|
+
* EVERY COMMENT in Chromium 150 (#1007), which deletes both. So it is used only
|
|
18
|
+
* when a one-time probe proves it lossless on this engine, and otherwise we parse
|
|
19
|
+
* with DOMParser (comments preserved; DSD is left unprocessed, see
|
|
20
|
+
* `parseDocumentPreservingComments` for why that gap beats both ways of closing it).
|
|
21
|
+
*
|
|
22
|
+
* A partial-nav response (#936) is an INNER fragment that BEGINS with the
|
|
23
|
+
* `<!--wj:children:<segment>:<route-key>-->` boundary open and carries no
|
|
24
|
+
* `<!doctype>`/`<html>`. Parsing such a fragment as a DOCUMENT hoists that
|
|
25
|
+
* leading comment OUT of `<body>` (the HTML parser's "before html" insertion
|
|
26
|
+
* mode makes a leading comment a child of the document, before `<html>`), so
|
|
27
|
+
* `collectBoundaries(doc.body)` never sees the opening boundary, finds no
|
|
28
|
+
* shared segment, and `applySwap` degrades to a full load. So a fragment is
|
|
29
|
+
* parsed in BODY (fragment) context instead, keeping the boundary with its
|
|
30
|
+
* content.
|
|
31
|
+
* `body.setHTMLUnsafe` also processes Declarative Shadow DOM, so a shadow
|
|
32
|
+
* component inside the swapped content still re-attaches its root; the
|
|
33
|
+
* `<template>` path is the fallback for browsers without it (markers preserved,
|
|
34
|
+
* DSD not, which matches the pre-`setHTMLUnsafe` baseline).
|
|
35
|
+
*
|
|
36
|
+
* @param {string} html
|
|
37
|
+
* @returns {Document | null}
|
|
38
|
+
*/
|
|
39
|
+
export function parseHTML(html) {
|
|
40
|
+
const isFragment = !/^\s*(?:<!doctype|<html)/i.test(html);
|
|
41
|
+
if (isFragment && typeof document !== 'undefined' && document.implementation) {
|
|
42
|
+
try {
|
|
43
|
+
const doc = document.implementation.createHTMLDocument();
|
|
44
|
+
if (typeof doc.body.setHTMLUnsafe === 'function') {
|
|
45
|
+
doc.body.setHTMLUnsafe(html);
|
|
46
|
+
} else {
|
|
47
|
+
const t = doc.createElement('template');
|
|
48
|
+
t.innerHTML = html;
|
|
49
|
+
doc.body.appendChild(t.content);
|
|
50
|
+
}
|
|
51
|
+
return doc;
|
|
52
|
+
} catch {
|
|
53
|
+
// Fall through to a document parse (still functional, just the #936 path).
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (
|
|
57
|
+
typeof Document !== 'undefined' &&
|
|
58
|
+
typeof Document.parseHTMLUnsafe === 'function' &&
|
|
59
|
+
parseHTMLUnsafePreservesComments()
|
|
60
|
+
) {
|
|
61
|
+
return Document.parseHTMLUnsafe(html);
|
|
62
|
+
}
|
|
63
|
+
return parseDocumentPreservingComments(html);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Is `Document.parseHTMLUnsafe` lossless for comments?
|
|
68
|
+
*
|
|
69
|
+
* Chromium 150 strips EVERY comment from `Document.parseHTMLUnsafe` output
|
|
70
|
+
* (#1007). No other parse API does: `DOMParser`, `setHTMLUnsafe`,
|
|
71
|
+
* `template.innerHTML`, and plain `innerHTML` all preserve them, and the
|
|
72
|
+
* document's own navigation parser preserves them (which is why a hard refresh
|
|
73
|
+
* always looked fine and only soft nav broke). MDN documents parseHTMLUnsafe as
|
|
74
|
+
* the parse-WITHOUT-sanitization entry point, so this reads as a browser defect
|
|
75
|
+
* rather than intent, but the whole router rides on comments (`wj:children`
|
|
76
|
+
* layout markers) and so does hydration (`webjs-hydrate`), so we cannot take
|
|
77
|
+
* the risk either way.
|
|
78
|
+
*
|
|
79
|
+
* Probed once, lazily, rather than version-sniffed: when the browser is fixed
|
|
80
|
+
* we silently return to the fast single-pass native path, and a future browser
|
|
81
|
+
* that regresses the same way is caught with no code change.
|
|
82
|
+
*
|
|
83
|
+
* @returns {boolean}
|
|
84
|
+
*/
|
|
85
|
+
let _parseUnsafeLossless = null;
|
|
86
|
+
|
|
87
|
+
function parseHTMLUnsafePreservesComments() {
|
|
88
|
+
if (_parseUnsafeLossless !== null) return _parseUnsafeLossless;
|
|
89
|
+
try {
|
|
90
|
+
const probe = Document.parseHTMLUnsafe('<!doctype html><body><!--c--><i></i>');
|
|
91
|
+
_parseUnsafeLossless = probe?.body?.firstChild?.nodeType === 8;
|
|
92
|
+
} catch {
|
|
93
|
+
_parseUnsafeLossless = false;
|
|
94
|
+
}
|
|
95
|
+
return _parseUnsafeLossless;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Clear the memoized losslessness probe. Test-only: a browser cannot change
|
|
100
|
+
* mid-session, so nothing in the runtime needs this. Tests SIMULATE a stripping
|
|
101
|
+
* parser (rather than depending on the runner's browser actually being an
|
|
102
|
+
* affected version, which the Chromium web-test-runner currently resolves is not) and reset the
|
|
103
|
+
* memo around that stub.
|
|
104
|
+
*/
|
|
105
|
+
export function resetParseProbe() {
|
|
106
|
+
_parseUnsafeLossless = null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Parse a FULL document while preserving comments.
|
|
111
|
+
*
|
|
112
|
+
* `DOMParser` keeps comments but does NOT process Declarative Shadow DOM, so a
|
|
113
|
+
* `<template shadowrootmode>` stays an inert template here. That is a DELIBERATE
|
|
114
|
+
* limitation on this path, and both obvious ways to "fix" it are worse than the
|
|
115
|
+
* gap (measured on Chromium 150, not reasoned about):
|
|
116
|
+
*
|
|
117
|
+
* - `body.setHTMLUnsafe(body.innerHTML)` re-serializes, and that round-trip is
|
|
118
|
+
* not idempotent: Chromium omits the spec's LF-compensation rule (append
|
|
119
|
+
* U+000A when a `pre` / `textarea` / `listing` element's first Text child
|
|
120
|
+
* starts with one), so `<textarea>\n\nfoo</textarea>` parses to `"\nfoo"`
|
|
121
|
+
* natively but `"foo"` after a round-trip. In a `<textarea>` that is silent
|
|
122
|
+
* form-data corruption: a soft nav would submit different bytes than a hard
|
|
123
|
+
* refresh.
|
|
124
|
+
* - Attaching each root by hand (`host.attachShadow()` + move the template's
|
|
125
|
+
* nodes) is USELESS on the common path and HARMFUL on the other. Useless
|
|
126
|
+
* because the marker swap imports with `document.importNode(n, true)`, which
|
|
127
|
+
* drops a shadow root unless it is `clonable`, and SSR emits a bare
|
|
128
|
+
* `<template shadowrootmode="open">`, so the root never survives the import
|
|
129
|
+
* and `component.js` re-attaches from scratch exactly as it always did.
|
|
130
|
+
* Harmful because the full-body-swap path ADOPTS instead, so a script-created
|
|
131
|
+
* root does survive, and a script-created root is not `declarative`: the spec
|
|
132
|
+
* only permits a second `attachShadow()` over an existing root when that root
|
|
133
|
+
* is declarative, so any element whose constructor unconditionally calls
|
|
134
|
+
* `attachShadow()` then throws `NotSupportedError` on upgrade, where the
|
|
135
|
+
* native parse it replaced worked fine.
|
|
136
|
+
*
|
|
137
|
+
* The gap this leaves is narrow and strictly better than the bug it replaces: on
|
|
138
|
+
* a comment-stripping browser only, an element that depends on DSD content and
|
|
139
|
+
* ships NO JavaScript loses that content on a full-body-swap navigation. Every
|
|
140
|
+
* WebJs `static shadow = true` component is unaffected, because it attaches and
|
|
141
|
+
* renders its own root on upgrade (`component.js`, guarded by `if
|
|
142
|
+
* (!this.shadowRoot)`), and a soft nav runs JS by definition. Tracked separately.
|
|
143
|
+
*
|
|
144
|
+
* @param {string} html
|
|
145
|
+
* @returns {Document | null}
|
|
146
|
+
*/
|
|
147
|
+
function parseDocumentPreservingComments(html) {
|
|
148
|
+
if (typeof DOMParser === 'undefined') return null;
|
|
149
|
+
return new DOMParser().parseFromString(html, 'text/html');
|
|
150
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client router: events.
|
|
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 { findAnchorInPath } from './anchors.js';
|
|
10
|
+
import { NON_HTML_EXTENSIONS } from './constants.js';
|
|
11
|
+
import { enabled, markFragmentNav } from './state.js';
|
|
12
|
+
import { warnIfActionSubmissionCannotDeliver } from './diagnostics.js';
|
|
13
|
+
import { buildSubmitFormData, encodeSubmitBody, getSubmitAction, getSubmitEnctype, getSubmitMethod } from './form-encoder.js';
|
|
14
|
+
import { resolveTargetFrameId } from './frames.js';
|
|
15
|
+
import { resolvePreserveScroll } from './scroll.js';
|
|
16
|
+
import { absorbFragmentClickPopState, performNavigation, performSubmission } from './navigator.js';
|
|
17
|
+
|
|
18
|
+
/** @param {MouseEvent} e */
|
|
19
|
+
export function onClick(e) {
|
|
20
|
+
if (!enabled) return;
|
|
21
|
+
if (e.defaultPrevented || e.button !== 0) return;
|
|
22
|
+
if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
|
|
23
|
+
|
|
24
|
+
const anchor = findAnchorInPath(e);
|
|
25
|
+
if (!anchor) return;
|
|
26
|
+
if (anchor.hasAttribute('download')) return;
|
|
27
|
+
if (anchor.target && anchor.target !== '_self') return;
|
|
28
|
+
|
|
29
|
+
const href = anchor.href;
|
|
30
|
+
if (!href) return;
|
|
31
|
+
|
|
32
|
+
const url = new URL(href);
|
|
33
|
+
if (url.origin !== location.origin) return;
|
|
34
|
+
// `href.includes('#')` rather than `url.hash`, because the URL serializer
|
|
35
|
+
// reports BOTH a null fragment and an empty one as `''`, and only the second
|
|
36
|
+
// is a fragment navigation. `href="#"` keeps its `#` in `href` and per the
|
|
37
|
+
// spec navigates to the document element (the back-to-top idiom), while
|
|
38
|
+
// `href=""` resolves to the current url with the fragment REMOVED, which the
|
|
39
|
+
// spec reloads rather than jumping, so it must stay a router navigation. A
|
|
40
|
+
// `#` cannot appear anywhere else in a serialized url: the parser encodes it
|
|
41
|
+
// in the path and starts the fragment at it in the query (#1437).
|
|
42
|
+
if (url.pathname === location.pathname && url.search === location.search && url.href.includes('#')) {
|
|
43
|
+
// Leave a mark for the popstate this jump is about to fire. A REPEAT click
|
|
44
|
+
// replaces its history entry rather than pushing, so that popstate arrives
|
|
45
|
+
// with an unchanged url and is indistinguishable by comparison from a real
|
|
46
|
+
// Back between two entries that share one. Provenance is the only thing
|
|
47
|
+
// that separates them, and this is where the router has it (#1437).
|
|
48
|
+
markFragmentNav(url.href);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
// Checked AFTER the fragment bow-out on purpose. `data-no-router` opts out of
|
|
52
|
+
// ROUTING, and the bow-out above routes nothing either way, but the browser
|
|
53
|
+
// still performs the native jump and still fires the popstate that has to be
|
|
54
|
+
// recognised. Returning here first would leave EVERY click of a
|
|
55
|
+
// `data-no-router` in-page anchor unmarked, first and repeat alike, since the
|
|
56
|
+
// mark is the only thing that absorbs one, so each would be re-navigated
|
|
57
|
+
// destructively (#1437).
|
|
58
|
+
if (anchor.hasAttribute('data-no-router')) return;
|
|
59
|
+
if (NON_HTML_EXTENSIONS.test(url.pathname)) return;
|
|
60
|
+
|
|
61
|
+
e.preventDefault();
|
|
62
|
+
// Resolve the target frame. An explicit `data-webjs-frame` on (or above)
|
|
63
|
+
// the anchor drives a frame by id from anywhere in the document (an
|
|
64
|
+
// external sidebar/nav link), `_top` breaks out to a full-page nav, and
|
|
65
|
+
// absence falls back to the closest enclosing frame (today's default).
|
|
66
|
+
const frameId = resolveTargetFrameId(anchor);
|
|
67
|
+
// #1436: `data-preserve-scroll` on the anchor or an ancestor keeps the reader
|
|
68
|
+
// where they are instead of scrolling to top. Read here, beside the other
|
|
69
|
+
// per-link opt-outs, and carried on the navigation's opts bag. Inert on a
|
|
70
|
+
// frame-targeted link, which already writes no scroll (#1427).
|
|
71
|
+
performNavigation(href, false, frameId, { preserveScroll: resolvePreserveScroll(anchor) });
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** @param {PopStateEvent} _e */
|
|
75
|
+
export function onPopState(_e) {
|
|
76
|
+
// The popstate an in-page fragment CLICK produces is not a navigation: the
|
|
77
|
+
// browser has already done the jump, so re-fetching and re-swapping would
|
|
78
|
+
// destroy live DOM identity and undo it (#1437). Absorbed only when the
|
|
79
|
+
// router MARKED that click on its way out (the bow-out above); a popstate
|
|
80
|
+
// with no mark behind it is a traversal and stays on the normal path,
|
|
81
|
+
// whatever its url. The callee's docstring has the full reasoning, including
|
|
82
|
+
// why no url comparison can stand in for the mark.
|
|
83
|
+
if (absorbFragmentClickPopState(location.href)) return;
|
|
84
|
+
// popstate has no DOM anchor, so no frame context: restore via cache or
|
|
85
|
+
// refetch the whole document.
|
|
86
|
+
performNavigation(location.href, true, null);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Intercept form submissions. BUBBLE phase (see enableClientRouter) so we run
|
|
91
|
+
* AFTER a component's per-element `@submit` handler, which is bound at-target.
|
|
92
|
+
* That ordering is what makes the `if (e.defaultPrevented) return` guard below
|
|
93
|
+
* work: a component that calls `e.preventDefault()` (the chat / comments forms,
|
|
94
|
+
* or any JS-handled form) has already run, so we see the prevented default and
|
|
95
|
+
* leave the form alone. A capture listener would fire us first, before the
|
|
96
|
+
* component, defeating the guard and wrongly navigating the page out from under
|
|
97
|
+
* a JS-handled form.
|
|
98
|
+
*
|
|
99
|
+
* Filtering mirrors Turbo's `form_submit_observer.js`:
|
|
100
|
+
* - `data-no-router` on form or submitter → full browser submit.
|
|
101
|
+
* - `formmethod="dialog"` → native <dialog> dismissal, never routed.
|
|
102
|
+
* - `target` / `formtarget` that isn't `_self` → iframe / popup target.
|
|
103
|
+
* - Cross-origin or non-HTML-extension action → let the browser handle.
|
|
104
|
+
*
|
|
105
|
+
* Submitter attributes (`formmethod`, `formaction`, `formenctype`) take
|
|
106
|
+
* precedence over the form's own: HTML5 form-submission algorithm.
|
|
107
|
+
*
|
|
108
|
+
* @param {SubmitEvent} e
|
|
109
|
+
*/
|
|
110
|
+
export function onSubmit(e) {
|
|
111
|
+
if (!enabled) return;
|
|
112
|
+
if (e.defaultPrevented) return;
|
|
113
|
+
|
|
114
|
+
const form = /** @type {HTMLFormElement | null} */ (e.target);
|
|
115
|
+
// Duck-type check rather than `instanceof HTMLFormElement`: linkedom
|
|
116
|
+
// and other non-browser DOMs don't always mark form elements as
|
|
117
|
+
// instances of the window's HTMLFormElement class.
|
|
118
|
+
if (!form || form.nodeType !== 1 || form.tagName !== 'FORM') return;
|
|
119
|
+
if (form.hasAttribute('data-no-router')) return;
|
|
120
|
+
|
|
121
|
+
const submitter = /** @type {HTMLElement | null} */ (e.submitter ?? null);
|
|
122
|
+
if (submitter && submitter.hasAttribute('data-no-router')) return;
|
|
123
|
+
|
|
124
|
+
// Presence, not truthiness, exactly as `getSubmitAction` resolves the action
|
|
125
|
+
// (#1322). The form-submission algorithm asks whether the submitter HAS a
|
|
126
|
+
// `formtarget`, so a present-but-empty one overrides the form and then means
|
|
127
|
+
// the current browsing context, per the rules for choosing a navigable.
|
|
128
|
+
const target = (submitter && submitter.hasAttribute('formtarget'))
|
|
129
|
+
? (submitter.getAttribute('formtarget') || '')
|
|
130
|
+
: (form.getAttribute('target') || '');
|
|
131
|
+
if (target && target !== '_self') return;
|
|
132
|
+
|
|
133
|
+
const method = getSubmitMethod(form, submitter);
|
|
134
|
+
if (method === 'dialog') return;
|
|
135
|
+
|
|
136
|
+
const enctype = getSubmitEnctype(form, submitter);
|
|
137
|
+
const isSafeMethod = method === 'get' || method === 'head';
|
|
138
|
+
|
|
139
|
+
const action = getSubmitAction(form, submitter);
|
|
140
|
+
/** @type {URL} */ let url;
|
|
141
|
+
try { url = new URL(action, location.href); }
|
|
142
|
+
catch { return; }
|
|
143
|
+
if (url.origin !== location.origin) return;
|
|
144
|
+
if (NON_HTML_EXTENSIONS.test(url.pathname)) return;
|
|
145
|
+
|
|
146
|
+
// Built once, after the cheap bails (a submission the router ignores should
|
|
147
|
+
// not pay for a FormData) and BEFORE the text/plain bail below. That order
|
|
148
|
+
// matters for the dev report: `text/plain` is precisely the case where the
|
|
149
|
+
// router declines the submission, so reporting after the bail would be dead
|
|
150
|
+
// code for the one shape that most needs it, since both paths are then
|
|
151
|
+
// answered with a 405 and the author gets no other signal.
|
|
152
|
+
const rawBody = buildSubmitFormData(form, submitter);
|
|
153
|
+
// Observational, and silent in production. Runs before `preventDefault`.
|
|
154
|
+
warnIfActionSubmissionCannotDeliver(form, submitter, method, rawBody);
|
|
155
|
+
|
|
156
|
+
// #1307: `text/plain` is a legal native encoding the server cannot parse
|
|
157
|
+
// (`looksLikeFormSubmission` accepts multipart and urlencoded only), and
|
|
158
|
+
// there is no honest way to send it over `fetch` and have the response mean
|
|
159
|
+
// anything. Bail to the browser so BOTH paths do the same thing, rather than
|
|
160
|
+
// silently sending multipart, which is what made the same form behave one
|
|
161
|
+
// way with JS and another way without it. Turbo enumerates this encoding and
|
|
162
|
+
// then sends FormData anyway, which is the divergence being avoided here. A
|
|
163
|
+
// safe method ignores the enctype entirely, per the submission algorithm.
|
|
164
|
+
if (!isSafeMethod && enctype === 'text/plain') return;
|
|
165
|
+
|
|
166
|
+
const body = encodeSubmitBody(rawBody, enctype);
|
|
167
|
+
|
|
168
|
+
e.preventDefault();
|
|
169
|
+
// Resolve the target frame for the submit, same precedence as a link:
|
|
170
|
+
// an explicit `data-webjs-frame` on (or above) the form or its submitter
|
|
171
|
+
// wins, `_top` breaks out, absence falls back to the enclosing frame.
|
|
172
|
+
const frameId = resolveTargetFrameId(submitter || form);
|
|
173
|
+
// Same trigger precedence as the frame line above. The form is passed as a
|
|
174
|
+
// FALLBACK rather than relied on through the submitter's ancestors, because a
|
|
175
|
+
// submitter is form-associated by `form="id"` and may sit outside the form
|
|
176
|
+
// entirely, where `closest()` would never reach it. The trigger still wins
|
|
177
|
+
// when it resolves a carrier, so `data-preserve-scroll="false"` on a button
|
|
178
|
+
// inside a marked form still opts that button out.
|
|
179
|
+
const preserveScroll = resolvePreserveScroll(submitter || form, form);
|
|
180
|
+
performSubmission(url.href, method, body, frameId, form, { preserveScroll });
|
|
181
|
+
}
|
|
182
|
+
|