From 55d086ed756cf3c2c669025d72f22a1de9e1e0a7 Mon Sep 17 00:00:00 2001 From: Domenic Denicola Date: Wed, 23 Mar 2022 18:03:21 -0400 Subject: [PATCH 1/4] Specify navigateEvent.requestDownload Closes #76. --- README.md | 10 +++--- navigation_api.d.ts | 2 ++ spec.bs | 76 ++++++++++++++++++++++++++++++++++++++++++--- 3 files changed, 80 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 9600b35..e683fea 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.requestDownload !== 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. +- `requestDownload`: 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.requestDownload !== 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.requestDownload !== null) { return; } diff --git a/navigation_api.d.ts b/navigation_api.d.ts index 7d2814b..443833c 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 requestDownlaod: 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; + requestDownload?: string|null; info?: unknown; } diff --git a/spec.bs b/spec.bs index c2333e8..527222b 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? requestDownload;
   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? requestDownload = 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/requestDownload}} +
+

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 for 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, requestDownload, 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/requestDownload}} 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/requestDownload}} 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. + +
+
    +
  • Ensure that the algorithm gets an appropriate [=user navigation involvement=] value, |userInvolvement|, passed to it. This is similar to the modifications for the follow the hyperlink 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 "[=user navigation involvement/browser UI=]", even if it is triggered as part of [=EventTarget/activation behavior=]. + +

  • Separate out the sandboxing checks in allowed to download 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=]. + +

  • +

    Before we reach the point at which it's time to actually go in parallel and fetch content from the server, and after the cannot navigate check, the synchronously-possible part of the allowed to download check, the URL parsing step, and the hyperlink suffix appending step, run the equivalent of the following: + + 1. If |userInvolvement| is not "[=user navigation involvement/browser UI=]", then: + 1. Let |navigation| be |subject|'s [=relevant global object=]'s [=Window/navigation API=]. + 1. Let |filename| be the value of |subject|'s <{a/download}> attribute. + 1. Let |continue| be the result of [=firing a download-requested navigate event=] at |navigation| with [=fire a download-requested navigate event/destinationURL=] set to |URL|, [=fire a download-requested navigate event/userInvolvement=] set to |userInvolvement|, and [=fire a download-requested navigate event/filename=] set to |filename|. + 1. If |continue| is false, then return. + +

    Here the variables |subject| and |URL| refer to the same things they currently do in the download the hyperlink algorithm, i.e. the <{a}> or <{area}> element in question, and the parsed [=URL=]. + +

    If we end up triggering the navigate algorithm from the download the hyperlink algorithm, then these steps won't be directly incorporated into the download the hyperlink 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. +

+
+

Patches to session history

This section details monkeypatches to [[!HTML]] to track appropriate data for associating a {{Navigation}} with a [=session history entry=]. From b3bfa3a0d276181df2f3ada43fc2176690998036 Mon Sep 17 00:00:00 2001 From: Domenic Denicola Date: Thu, 24 Mar 2022 11:04:48 -0400 Subject: [PATCH 2/4] Anne loves those commas after e.g. Co-authored-by: Anne van Kesteren --- spec.bs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec.bs b/spec.bs index 527222b..6162d8d 100644 --- a/spec.bs +++ b/spec.bs @@ -1113,7 +1113,7 @@ enum NavigationNavigationType {

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

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}} From 1dea15bfee46df7313d9a53730ea0ace6f4cdb47 Mon Sep 17 00:00:00 2001 From: Domenic Denicola Date: Thu, 24 Mar 2022 11:06:16 -0400 Subject: [PATCH 3/4] requestDownload -> downloadRequest --- README.md | 8 ++++---- navigation_api.d.ts | 2 +- spec.bs | 12 ++++++------ 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index e683fea..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 || e.requestDownload !== null) { + if (!e.canTransition || e.hashChange || e.downloadRequest !== null) { return; } @@ -323,7 +323,7 @@ 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. -- `requestDownload`: 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). +- `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. @@ -361,7 +361,7 @@ navigation.addEventListener("navigate", e => { } // Don't intercept fragment navigations or downloads. - if (e.hashChange || e.requestDownload !== null) { + if (e.hashChange || e.downloadRequest !== null) { return; } @@ -407,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 || e.requestDownload !== null) { + if (!e.canTransition || e.hashChange || e.downloadRequest !== null) { return; } diff --git a/navigation_api.d.ts b/navigation_api.d.ts index 443833c..55f0a8e 100644 --- a/navigation_api.d.ts +++ b/navigation_api.d.ts @@ -132,7 +132,7 @@ interface NavigateEventInit extends EventInit { destination: NavigationDestination; signal: AbortSignal; formData?: FormData|null; - requestDownload?: string|null; + downloadRequest?: string|null; info?: unknown; } diff --git a/spec.bs b/spec.bs index 6162d8d..1a81196 100644 --- a/spec.bs +++ b/spec.bs @@ -1034,7 +1034,7 @@ interface NavigateEvent : Event { readonly attribute boolean hashChange; readonly attribute AbortSignal signal; readonly attribute FormData? formData; - readonly attribute DOMString? requestDownload; + readonly attribute DOMString? downloadRequest; readonly attribute any info; undefined transitionWhile(Promise newNavigationAction); @@ -1048,7 +1048,7 @@ dictionary NavigateEventInit : EventInit { boolean hashChange = false; required AbortSignal signal; FormData? formData = null; - DOMString? requestDownload = null; + DOMString? downloadRequest = null; any info; }; @@ -1102,7 +1102,7 @@ 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/requestDownload}} +
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: @@ -1131,7 +1131,7 @@ enum NavigationNavigationType {

-The navigationType, destination, canTransition, userInitiated, hashChange, signal, formData, requestDownload, 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: @@ -1306,7 +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/requestDownload}} to |downloadRequestFilename|. + 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=]. @@ -1728,7 +1728,7 @@ Expand the section of the navigation/traversal response handling which deals wit 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/requestDownload}} 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. +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.

    From aaff8351a599dfba455a26d6e4b9325372ed00af Mon Sep 17 00:00:00 2001 From: Domenic Denicola Date: Fri, 25 Mar 2022 12:04:59 -0400 Subject: [PATCH 4/4] Fix missed property name --- navigation_api.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/navigation_api.d.ts b/navigation_api.d.ts index 55f0a8e..c826969 100644 --- a/navigation_api.d.ts +++ b/navigation_api.d.ts @@ -118,7 +118,7 @@ declare class NavigateEvent extends Event { readonly destination: NavigationDestination; readonly signal: AbortSignal; readonly formData: FormData|null; - readonly requestDownlaod: string|null; + readonly downloadRequest: string|null; readonly info: unknown; transitionWhile(newNavigationAction: Promise, options?: NavigationTransitionWhileOptions): void;