diff --git a/README.md b/README.md index 9600b35..a6dd8f7 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ An application or framework's centralized router can use the `navigate` event to ```js navigation.addEventListener("navigate", e => { - if (!e.canTransition || e.hashChange) { + if (!e.canTransition || e.hashChange || e.downloadRequest !== null) { return; } @@ -323,6 +323,8 @@ The event object has several useful properties: - `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. +- `downloadRequest`: a string or null, indicating whether this navigation was initiated by a `` link. If it was, then this will contain the value of the attribute (which could be the empty string). + - `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. - `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 => { return; } - // Don't intercept fragment navigations. - if (e.hashChange) { + // Don't intercept fragment navigations or downloads. + if (e.hashChange || e.downloadRequest !== null) { return; } @@ -405,7 +407,7 @@ Sometimes it's desirable to handle back/forward navigations specially, e.g. reus ```js navigation.addEventListener("navigate", e => { // As before. - if (!e.canTransition || e.hashChange) { + if (!e.canTransition || e.hashChange || e.downloadRequest !== null) { return; } diff --git a/navigation_api.d.ts b/navigation_api.d.ts index 7d2814b..c826969 100644 --- a/navigation_api.d.ts +++ b/navigation_api.d.ts @@ -118,6 +118,7 @@ declare class NavigateEvent extends Event { readonly destination: NavigationDestination; readonly signal: AbortSignal; readonly formData: FormData|null; + readonly downloadRequest: string|null; readonly info: unknown; transitionWhile(newNavigationAction: Promise, options?: NavigationTransitionWhileOptions): void; @@ -131,6 +132,7 @@ interface NavigateEventInit extends EventInit { destination: NavigationDestination; signal: AbortSignal; formData?: FormData|null; + downloadRequest?: string|null; info?: unknown; } diff --git a/spec.bs b/spec.bs index c2333e8..1a81196 100644 --- a/spec.bs +++ b/spec.bs @@ -21,6 +21,7 @@ Assume Explicit For: yes
 spec: html; urlPrefix: https://html.spec.whatwg.org/multipage/
@@ -131,6 +132,13 @@ table {
   top: -0.8em;
   left: -0.8em;
 }
+
+/* .XXX from https://resources.whatwg.org/standard.css */
+.XXX {
+  color: #D50606;
+  background: white;
+  border: solid #D50606;
+}
 
 
 
@@ -1026,6 +1034,7 @@ interface NavigateEvent : Event {
   readonly attribute boolean hashChange;
   readonly attribute AbortSignal signal;
   readonly attribute FormData? formData;
+  readonly attribute DOMString? downloadRequest;
   readonly attribute any info;
 
   undefined transitionWhile(Promise newNavigationAction);
@@ -1039,6 +1048,7 @@ dictionary NavigateEventInit : EventInit {
   boolean hashChange = false;
   required AbortSignal signal;
   FormData? formData = null;
+  DOMString? downloadRequest = null;
   any info;
 };
 
@@ -1092,6 +1102,20 @@ enum NavigationNavigationType {
     

(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.) +

event.{{NavigateEvent/downloadRequest}} +
+

Represents whether or not this navigation was requested to be a download, by using an <{a}> or <{area}> element's <{a/download}> attribute: + + * If a download was not requested, then this property is null. + * If a download was requested, returns the filename that was supplied, via ``. (This could be the empty string, as in the case of ``.) + +

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 unspecified reasons. + +

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. + +

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. +

+
event.{{NavigateEvent/info}}

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 {

-The navigationType, destination, canTransition, userInitiated, hashChange, signal, formData, and info getter steps are to return the value that the corresponding attribute was initialized to. +The navigationType, destination, canTransition, userInitiated, hashChange, signal, formData, downloadRequest, and info getter steps are to return the value that the corresponding attribute was initialized to. A {{NavigateEvent}} has the following associated values which are only conditionally used: @@ -1216,6 +1240,7 @@ The sameDocument getter steps a To fire a traversal `navigate` event at a {{Navigation}} |navigation| given a [=session history entry=] |destinationEntry|, and an optional [=user navigation involvement=] |userInvolvement| (default "[=user navigation involvement/none=]"): 1. Let |event| be the result of [=creating an event=] given {{NavigateEvent}}, in |navigation|'s [=relevant Realm=]. + 1. Set |event|'s [=NavigateEvent/classic history API serialized data=] to null. 1. Let |destination| be a [=new=] {{NavigationDestination}} created in |navigation|'s [=relevant Realm=]. 1. Set |destination|'s [=NavigationDestination/URL=] to |destinationEntry|'s [=session history entry/URL=]. 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 sameDocument getter steps a 1. Set |destination|'s [=NavigationDestination/index=] to −1. 1. Set |destination|'s [=NavigationDestination/state=] to null. 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. - 1. Let |result| be the result of performing the [=inner navigate event firing algorithm=] given |navigation|, "{{NavigationNavigationType/traverse}}", |event|, |destination|, |userInvolvement|, and null. + 1. Let |result| be the result of performing the [=inner navigate event firing algorithm=] given |navigation|, "{{NavigationNavigationType/traverse}}", |event|, |destination|, |userInvolvement|, null, and null. 1. [=Assert=]: |result| is true (traversals are never cancelable). @@ -1245,11 +1270,26 @@ The sameDocument getter steps a 1. Set |destination|'s [=NavigationDestination/index=] to −1. 1. Set |destination|'s [=NavigationDestination/state=] to |state|. 1. Set |destination|'s [=NavigationDestination/is same document=] to |isSameDocument|. - 1. Return the result of performing the [=inner navigate event firing algorithm=] given |navigation|, |navigationType|, |event|, |destination|, |userInvolvement|, and |formDataEntryList|. + 1. Return the result of performing the [=inner navigate event firing algorithm=] given |navigation|, |navigationType|, |event|, |destination|, |userInvolvement|, |formDataEntryList|, and null. + + +
+ To fire a download-requested `navigate` event at a {{Navigation}} |navigation| given a [=URL=] |destinationURL|, a [=user navigation involvement=] |userInvolvement|, and a string |filename|: + + 1. Let |event| be the result of [=creating an event=] given {{NavigateEvent}}, in |navigation|'s [=relevant Realm=]. + 1. Set |event|'s [=NavigateEvent/classic history API serialized data=] to null. + 1. Let |destination| be a [=new=] {{NavigationDestination}} created in |navigation|'s [=relevant Realm=]. + 1. Set |destination|'s [=NavigationDestination/URL=] to |destinationURL|. + 1. Set |destination|'s [=NavigationDestination/key=] to null. + 1. Set |destination|'s [=NavigationDestination/id=] to null. + 1. Set |destination|'s [=NavigationDestination/index=] to −1. + 1. Set |destination|'s [=NavigationDestination/state=] to null. + 1. Set |destination|'s [=NavigationDestination/is same document=] to false. + 1. Return the result of performing the [=inner navigate event firing algorithm=] given |navigation|, "{{NavigationNavigationType/push}}", |event|, |destination|, |userInvolvement|, null, and |filename|.
- The inner `navigate` event firing algorithm 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|: + The inner `navigate` event firing algorithm 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|: 1. [=Navigation/Promote the upcoming navigation to ongoing=] given |navigation| and |destination|'s [=NavigationDestination/key=]. 1. Let |ongoingNavigation| be |navigation|'s [=Navigation/ongoing navigation=]. @@ -1266,6 +1306,7 @@ The sameDocument getter steps a 1. Initialize |event|'s {{Event/type}} to "{{Navigation/navigate}}". 1. Initialize |event|'s {{NavigateEvent/navigationType}} to |navigationType|. 1. Initialize |event|'s {{NavigateEvent/destination}} to |destination|. + 1. Initialize |event|'s {{NavigateEvent/downloadRequest}} to |downloadRequestFilename|. 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.

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=]. 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 1. [=Fire a traversal navigate event=] at |previousDocument|'s [=relevant global object=]'s [=Window/navigation API=] with [=fire a traversal navigate event/destinationEntry=] set to |targetEntry| and [=fire a traversal navigate event/userInvolvement=] set to userInvolvement.

+ + +The current specification for downloading a hyperlink has several known issues, most notably whatwg/html#5548 which indicates that the specification should probably be merged into the general navigation algorithm. + +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 downloading a hyperlink algorithm, we describe here the expected behavior in a less-formal fashion. We believe this is still enough to get interoperability. + +
+ +
+

Patches to session history

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