Skip to content

Commit c302264

Browse files
authored
Specify navigateEvent.downloadRequest
Closes #76.
1 parent 4a9fdaa commit c302264

File tree

3 files changed

+80
-8
lines changed

3 files changed

+80
-8
lines changed

README.md

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ An application or framework's centralized router can use the `navigate` event to
2626

2727
```js
2828
navigation.addEventListener("navigate", e => {
29-
if (!e.canTransition || e.hashChange) {
29+
if (!e.canTransition || e.hashChange || e.downloadRequest !== null) {
3030
return;
3131
}
3232

@@ -323,6 +323,8 @@ The event object has several useful properties:
323323
324324
- `formData`: a [`FormData`](https://developer.mozilla.org/en-US/docs/Web/API/FormData) object containing form submission data, or `null` if the navigation is not a form submission.
325325
326+
- `downloadRequest`: a string or null, indicating whether this navigation was initiated by a `<a href="..." download>` link. If it was, then this will contain the value of the attribute (which could be the empty string).
327+
326328
- `info`: any value passed by `navigation.navigate(url, { state, info })`, `navigation.back({ info })`, or similar, if the navigation was initiated by one of those methods and the `info` option was supplied. Otherwise, undefined. See [the example below](#example-using-info) for more.
327329
328330
- `signal`: an [`AbortSignal`](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal) which can be monitored for when the navigation gets aborted.
@@ -358,8 +360,8 @@ navigation.addEventListener("navigate", e => {
358360
return;
359361
}
360362

361-
// Don't intercept fragment navigations.
362-
if (e.hashChange) {
363+
// Don't intercept fragment navigations or downloads.
364+
if (e.hashChange || e.downloadRequest !== null) {
363365
return;
364366
}
365367

@@ -405,7 +407,7 @@ Sometimes it's desirable to handle back/forward navigations specially, e.g. reus
405407
```js
406408
navigation.addEventListener("navigate", e => {
407409
// As before.
408-
if (!e.canTransition || e.hashChange) {
410+
if (!e.canTransition || e.hashChange || e.downloadRequest !== null) {
409411
return;
410412
}
411413

navigation_api.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ declare class NavigateEvent extends Event {
118118
readonly destination: NavigationDestination;
119119
readonly signal: AbortSignal;
120120
readonly formData: FormData|null;
121+
readonly downloadRequest: string|null;
121122
readonly info: unknown;
122123

123124
transitionWhile(newNavigationAction: Promise<any>, options?: NavigationTransitionWhileOptions): void;
@@ -131,6 +132,7 @@ interface NavigateEventInit extends EventInit {
131132
destination: NavigationDestination;
132133
signal: AbortSignal;
133134
formData?: FormData|null;
135+
downloadRequest?: string|null;
134136
info?: unknown;
135137
}
136138

spec.bs

Lines changed: 72 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ Assume Explicit For: yes
2121

2222
<pre class="link-defaults">
2323
spec: html; type: element; text: a
24+
spec: html; type: element-attr; for: a; text: download
2425
</pre>
2526
<pre class="anchors">
2627
spec: html; urlPrefix: https://html.spec.whatwg.org/multipage/
@@ -131,6 +132,13 @@ table {
131132
top: -0.8em;
132133
left: -0.8em;
133134
}
135+
136+
/* .XXX from https://resources.whatwg.org/standard.css */
137+
.XXX {
138+
color: #D50606;
139+
background: white;
140+
border: solid #D50606;
141+
}
134142
</style>
135143

136144
<script src="https://resources.whatwg.org/file-issue.js" async></script>
@@ -1026,6 +1034,7 @@ interface NavigateEvent : Event {
10261034
readonly attribute boolean hashChange;
10271035
readonly attribute AbortSignal signal;
10281036
readonly attribute FormData? formData;
1037+
readonly attribute DOMString? downloadRequest;
10291038
readonly attribute any info;
10301039

10311040
undefined transitionWhile(Promise<undefined> newNavigationAction);
@@ -1039,6 +1048,7 @@ dictionary NavigateEventInit : EventInit {
10391048
boolean hashChange = false;
10401049
required AbortSignal signal;
10411050
FormData? formData = null;
1051+
DOMString? downloadRequest = null;
10421052
any info;
10431053
};
10441054

@@ -1092,6 +1102,20 @@ enum NavigationNavigationType {
10921102
<p>(Notably, this will be null even for "{{NavigationNavigationType/reload}}" and "{{NavigationNavigationType/traverse}}" navigations that are revisiting a session history entry that was originally created from a form submission.)
10931103
</dd>
10941104

1105+
<dt><code><var ignore>event</var>.{{NavigateEvent/downloadRequest}}</code>
1106+
<dd>
1107+
<p>Represents whether or not this navigation was requested to be a download, by using an <{a}> or <{area}> element's <{a/download}> attribute:
1108+
1109+
* If a download was not requested, then this property is null.
1110+
* If a download was requested, returns the filename that was supplied, via `<a download="filename" href="...">`. (This could be the empty string, as in the case of `<a download href="...">`.)
1111+
1112+
<p>Note that a download being requested does not always mean that a download will happen: for example, the download might be blocked by browser security policies, or end up being treated as a push navigation for <a href="https://github.com/whatwg/html/issues/7718" class="XXX">unspecified reasons</a>.
1113+
1114+
<p>Similarly, a navigation might end up being a download even if it was not requested to be one, due to the destination server responding with a `Content-Disposition: attachment` header.
1115+
1116+
<p>Finally, note that the {{Navigation/navigate}} event will not fire at all for downloads initiated using browser UI affordances, e.g., those created by right-clicking and choosing to save the target of the link.
1117+
</dd>
1118+
10951119
<dt><code><var ignore>event</var>.{{NavigateEvent/info}}</code>
10961120
<dd>
10971121
<p>An arbitrary JavaScript value passed via {{Window/navigation}} APIs that initiated this navigation, or null if the navigation was initiated by the user or via a non-{{Window/navigation}} API.
@@ -1107,7 +1131,7 @@ enum NavigationNavigationType {
11071131
</dd>
11081132
</dl>
11091133

1110-
The <dfn attribute for="NavigateEvent">navigationType</dfn>, <dfn attribute for="NavigateEvent">destination</dfn>, <dfn attribute for="NavigateEvent">canTransition</dfn>, <dfn attribute for="NavigateEvent">userInitiated</dfn>, <dfn attribute for="NavigateEvent">hashChange</dfn>, <dfn attribute for="NavigateEvent">signal</dfn>, <dfn attribute for="NavigateEvent">formData</dfn>, and <dfn attribute for="NavigateEvent">info</dfn> getter steps are to return the value that the corresponding attribute was initialized to.
1134+
The <dfn attribute for="NavigateEvent">navigationType</dfn>, <dfn attribute for="NavigateEvent">destination</dfn>, <dfn attribute for="NavigateEvent">canTransition</dfn>, <dfn attribute for="NavigateEvent">userInitiated</dfn>, <dfn attribute for="NavigateEvent">hashChange</dfn>, <dfn attribute for="NavigateEvent">signal</dfn>, <dfn attribute for="NavigateEvent">formData</dfn>, <dfn attribute for="NavigateEvent">downloadRequest</dfn>, and <dfn attribute for="NavigateEvent">info</dfn> getter steps are to return the value that the corresponding attribute was initialized to.
11111135

11121136
A {{NavigateEvent}} has the following associated values which are only conditionally used:
11131137

@@ -1216,6 +1240,7 @@ The <dfn attribute for="NavigationDestination">sameDocument</dfn> getter steps a
12161240
To <dfn>fire a traversal `navigate` event</dfn> at a {{Navigation}} |navigation| given a [=session history entry=] <dfn for="fire a traversal navigate event">|destinationEntry|</dfn>, and an optional [=user navigation involvement=] <dfn for="fire a traversal navigate event">|userInvolvement|</dfn> (default "<code>[=user navigation involvement/none=]</code>"):
12171241

12181242
1. Let |event| be the result of [=creating an event=] given {{NavigateEvent}}, in |navigation|'s [=relevant Realm=].
1243+
1. Set |event|'s [=NavigateEvent/classic history API serialized data=] to null.
12191244
1. Let |destination| be a [=new=] {{NavigationDestination}} created in |navigation|'s [=relevant Realm=].
12201245
1. Set |destination|'s [=NavigationDestination/URL=] to |destinationEntry|'s [=session history entry/URL=].
12211246
1. If |destinationEntry|'s [=session history entry/origin=] is [=same origin=] with |navigation|'s [=relevant settings object=]'s [=environment settings object/origin=], then:
@@ -1229,7 +1254,7 @@ The <dfn attribute for="NavigationDestination">sameDocument</dfn> getter steps a
12291254
1. Set |destination|'s [=NavigationDestination/index=] to &minus;1.
12301255
1. Set |destination|'s [=NavigationDestination/state=] to null.
12311256
1. Set |destination|'s [=NavigationDestination/is same document=] to true if |destinationEntry|'s [=session history entry/document=] is equal to |navigation|'s [=relevant global object=]'s [=associated Document=]; otherwise false.
1232-
1. Let |result| be the result of performing the [=inner navigate event firing algorithm=] given |navigation|, "{{NavigationNavigationType/traverse}}", |event|, |destination|, |userInvolvement|, and null.
1257+
1. Let |result| be the result of performing the [=inner navigate event firing algorithm=] given |navigation|, "{{NavigationNavigationType/traverse}}", |event|, |destination|, |userInvolvement|, null, and null.
12331258
1. [=Assert=]: |result| is true (traversals are never cancelable).
12341259
</div>
12351260

@@ -1245,11 +1270,26 @@ The <dfn attribute for="NavigationDestination">sameDocument</dfn> getter steps a
12451270
1. Set |destination|'s [=NavigationDestination/index=] to &minus;1.
12461271
1. Set |destination|'s [=NavigationDestination/state=] to |state|.
12471272
1. Set |destination|'s [=NavigationDestination/is same document=] to |isSameDocument|.
1248-
1. Return the result of performing the [=inner navigate event firing algorithm=] given |navigation|, |navigationType|, |event|, |destination|, |userInvolvement|, and |formDataEntryList|.
1273+
1. Return the result of performing the [=inner navigate event firing algorithm=] given |navigation|, |navigationType|, |event|, |destination|, |userInvolvement|, |formDataEntryList|, and null.
1274+
</div>
1275+
1276+
<div algorithm="fire a download-requested navigate event">
1277+
To <dfn>fire a download-requested `navigate` event</dfn> at a {{Navigation}} |navigation| given a [=URL=] <dfn for="fire a download-requested navigate event">|destinationURL|</dfn>, a [=user navigation involvement=] <dfn for="fire a download-requested navigate event">|userInvolvement|</dfn>, and a string <dfn for="fire a download-requested navigate event">|filename|</dfn>:
1278+
1279+
1. Let |event| be the result of [=creating an event=] given {{NavigateEvent}}, in |navigation|'s [=relevant Realm=].
1280+
1. Set |event|'s [=NavigateEvent/classic history API serialized data=] to null.
1281+
1. Let |destination| be a [=new=] {{NavigationDestination}} created in |navigation|'s [=relevant Realm=].
1282+
1. Set |destination|'s [=NavigationDestination/URL=] to |destinationURL|.
1283+
1. Set |destination|'s [=NavigationDestination/key=] to null.
1284+
1. Set |destination|'s [=NavigationDestination/id=] to null.
1285+
1. Set |destination|'s [=NavigationDestination/index=] to &minus;1.
1286+
1. Set |destination|'s [=NavigationDestination/state=] to null.
1287+
1. Set |destination|'s [=NavigationDestination/is same document=] to false.
1288+
1. Return the result of performing the [=inner navigate event firing algorithm=] given |navigation|, "{{NavigationNavigationType/push}}", |event|, |destination|, |userInvolvement|, null, and |filename|.
12491289
</div>
12501290

12511291
<div algorithm>
1252-
The <dfn>inner `navigate` event firing algorithm</dfn> is the following steps, given a {{Navigation}} |navigation|, a {{NavigationNavigationType}} |navigationType|, a {{NavigateEvent}} |event|, a {{NavigationDestination}} |destination|, a [=user navigation involvement=] |userInvolvement|, and an [=entry list=] or null |formDataEntryList|:
1292+
The <dfn>inner `navigate` event firing algorithm</dfn> is the following steps, given a {{Navigation}} |navigation|, a {{NavigationNavigationType}} |navigationType|, a {{NavigateEvent}} |event|, a {{NavigationDestination}} |destination|, a [=user navigation involvement=] |userInvolvement|, an [=entry list=] or null |formDataEntryList|, and a string or null |downloadRequestFilename|:
12531293

12541294
1. [=Navigation/Promote the upcoming navigation to ongoing=] given |navigation| and |destination|'s [=NavigationDestination/key=].
12551295
1. Let |ongoingNavigation| be |navigation|'s [=Navigation/ongoing navigation=].
@@ -1266,6 +1306,7 @@ The <dfn attribute for="NavigationDestination">sameDocument</dfn> getter steps a
12661306
1. Initialize |event|'s {{Event/type}} to "{{Navigation/navigate}}".
12671307
1. Initialize |event|'s {{NavigateEvent/navigationType}} to |navigationType|.
12681308
1. Initialize |event|'s {{NavigateEvent/destination}} to |destination|.
1309+
1. Initialize |event|'s {{NavigateEvent/downloadRequest}} to |downloadRequestFilename|.
12691310
1. If |ongoingNavigation| is not null, then initialize |event|'s {{NavigateEvent/info}} to |ongoingNavigation|'s [=navigation API method navigation/info=]. Otherwise, initialize it to undefined.
12701311
<p class="note">At this point |ongoingNavigation|'s [=navigation API method navigation/info=] is no longer needed and can be nulled out instead of keeping it alive for the lifetime of the [=navigation API method navigation=].
12711312
1. Initialize |event|'s {{NavigateEvent/signal}} to a [=new=] {{AbortSignal}} created in |navigation|'s [=relevant Realm=].
@@ -1683,6 +1724,33 @@ Expand the section of the navigation/traversal response handling which deals wit
16831724
1. [=Fire a traversal navigate event=] at |previousDocument|'s [=relevant global object=]'s [=Window/navigation API=] with <i>[=fire a traversal navigate event/destinationEntry=]</i> set to |targetEntry| and <i>[=fire a traversal navigate event/userInvolvement=]</i> set to <var ignore>userInvolvement</var>.
16841725
</div>
16851726

1727+
<h3 id="navigate-event-download-patches">Download a hyperlink updates</h3>
1728+
1729+
The current specification for <a spec="HTML" lt="download the hyperlink">downloading a hyperlink</a> has several known issues, most notably <a href="https://github.com/whatwg/html/issues/5548">whatwg/html#5548</a> which indicates that the specification should probably be merged into the general <a spec="HTML" lt="navigate">navigation</a> algorithm.
1730+
1731+
For the purposes of the navigation API, we need to fire the appropriate {{Navigation/navigate}} event, with {{NavigateEvent/downloadRequest}} set to the correct value. We could rigorously detail the ways to modify the current spec to accomplish this. But, given that the current spec will be rewritten anyway, this is probably not very useful. So until such a time as we can properly investigate and rewrite the <a spec="HTML" lt="download the hyperlink">downloading a hyperlink</a> algorithm, we describe here the expected behavior in a less-formal fashion. We believe this is still enough to get interoperability.
1732+
1733+
<div algorithm="download the hyperlink">
1734+
<ul>
1735+
<li><p>Ensure that the algorithm gets an appropriate [=user navigation involvement=] value, |userInvolvement|, passed to it. This is similar to the modifications for the <a spec="HTML">follow the hyperlink</a> algorithm described in [[#user-initiated-patches]]. One key difference is that, for the case where the user indicates a preference for downloading, |userInvolvement| must be "<code>[=user navigation involvement/browser UI=]</code>", even if it is triggered as part of [=EventTarget/activation behavior=].
1736+
1737+
<li><p>Separate out the sandboxing checks in <a spec="HTML">allowed to download</a> from the user-safeguarding checks. If the sandboxing checks fail, then the user agent must not fire a {{Navigation/navigate}} event. Whereas, the user-safeguarding checks generally happen later, probably [=in parallel=].
1738+
1739+
<li>
1740+
<p>Before we reach the point at which it's time to actually go in parallel and fetch content from the server, and after the <a spec="HTML">cannot navigate</a> check, the synchronously-possible part of the <a spec="HTML">allowed to download</a> check, the URL parsing step, and the hyperlink suffix appending step, run the equivalent of the following:
1741+
1742+
1. If |userInvolvement| is not "<code>[=user navigation involvement/browser UI=]</code>", then:
1743+
1. Let |navigation| be |subject|'s [=relevant global object=]'s [=Window/navigation API=].
1744+
1. Let |filename| be the value of |subject|'s <{a/download}> attribute.
1745+
1. Let |continue| be the result of [=firing a download-requested navigate event=] at |navigation| with <i>[=fire a download-requested navigate event/destinationURL=]</i> set to |URL|, <i>[=fire a download-requested navigate event/userInvolvement=]</i> set to |userInvolvement|, and <i>[=fire a download-requested navigate event/filename=]</i> set to |filename|.
1746+
1. If |continue| is false, then return.
1747+
1748+
<p>Here the variables |subject| and |URL| refer to the same things they currently do in the <a spec="HTML">download the hyperlink</a> algorithm, i.e. the <{a}> or <{area}> element in question, and the parsed [=URL=].
1749+
1750+
<p>If we end up triggering the <a spec="HTML">navigate</a> algorithm from the <a spec="HTML">download the hyperlink</a> algorithm, then these steps won't be directly incorporated into the <a spec="HTML">download the hyperlink</a> algorithm. Instead, the modifications in [[#navigate-algorithm-patches]] will get a bit more complicated, so as to use [=fire a download-requested navigate event=] with the above arguments, instead of [=fire a non-traversal navigate event=], for downloads.
1751+
</ul>
1752+
</div>
1753+
16861754
<h2 id="session-history-patches">Patches to session history</h2>
16871755

16881756
This section details monkeypatches to [[!HTML]] to track appropriate data for associating a {{Navigation}} with a [=session history entry=].

0 commit comments

Comments
 (0)