Skip to content

Commit

Permalink
Specify goTo()/back()/forward()
Browse files Browse the repository at this point in the history
  • Loading branch information
domenic committed Jun 30, 2021
1 parent a99db09 commit c202c9a
Show file tree
Hide file tree
Showing 2 changed files with 142 additions and 9 deletions.
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1337,9 +1337,9 @@ interface AppHistory : EventTarget {
Promise<undefined> navigate(USVString url, optional AppHistoryNavigateOptions options = {});
Promise<undefined> reload(optional AppHistoryReloadOptions options = {});
Promise<undefined> goTo(DOMString key, optional AppHistoryNavigationOptions = {});
Promise<undefined> back(optional AppHistoryNavigationOptions = {});
Promise<undefined> forward(optional AppHistoryNavigationOptions = {});
Promise<undefined> goTo(DOMString key, optional AppHistoryNavigationOptions options = {});
Promise<undefined> back(optional AppHistoryNavigationOptions options = {});
Promise<undefined> forward(optional AppHistoryNavigationOptions options = {});
attribute EventHandler onnavigate;
attribute EventHandler onnavigatesuccess;
Expand Down
145 changes: 139 additions & 6 deletions spec.bs
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,7 @@ spec: html; urlPrefix: https://whatpr.org/html/6315/
text: get all history steps; for: traversable navigable; url: history.html#getting-all-history-steps
text: step; for: session history entry; url: history.html#she-step
text: apply the history step; url: history.html#apply-the-history-step
text: checkForUserCancelation; for: apply the history step; url: history.html#apply-the-history-step
text: initiatorToCheck; for: apply the history step; url: history.html#apply-the-history-step
text: containing navigable; for: browsing context; url: browsers.html#bc-navigable
text: session; for: browsing context group; url: browsers.html#bcg-session
text: active document; for: navigable; url: history.html#nav-document
text: navigable; url: history.html#navigable
spec: uuid; type: dfn; urlPrefix: https://wicg.github.io/uuid/
Expand Down Expand Up @@ -157,6 +154,10 @@ interface AppHistory : EventTarget {
Promise<undefined> navigate(USVString url, optional AppHistoryNavigateOptions options = {});
Promise<undefined> reload(optional AppHistoryReloadOptions options = {});

Promise<undefined> goTo(DOMString key, optional AppHistoryNavigationOptions options = {});
Promise<undefined> back(optional AppHistoryNavigationOptions options = {});
Promise<undefined> forward(optional AppHistoryNavigationOptions options = {});

attribute EventHandler onnavigate;
attribute EventHandler onnavigatesuccess;
attribute EventHandler onnavigateerror;
Expand Down Expand Up @@ -436,6 +437,139 @@ Each {{AppHistory}} object has an associated <dfn for="AppHistory">navigate meth

<p class="note">Unlike {{Location/assign()|location.assign()}} and friends, which are exposed across [=same origin-domain|origin-domain=] boundaries, {{AppHistory/navigate()|appHistory.navigate()}} and {{AppHistory/reload()|appHistory.reload()}} can only be accessed by code with direct synchronous access to the {{Window/appHistory}} property. Thus, we avoid the complications around tracking <a spec="HTML">source browsing contexts</a>, and we don't need to deal with the <a spec="HTML">allowed to navigate</a> check and its accompanying <i>[=navigate/exceptionsEnabled=]</i> flag. We just treat all navigations as being initiated by the {{AppHistory}} object itself.

<h3 id="global-traversing">Traversing</h3>

<dl class="domintro non-normative">
<dt><code>await {{Window/appHistory}} . {{AppHistory/goTo(key)|goTo}}(<var ignore>key</var>)</code>
<dt><code>await {{Window/appHistory}} . {{AppHistory/goTo(key, options)|goTo}}(<var ignore>key</var>, { {{AppHistoryNavigationOptions/navigateInfo}} })</code>
<dd>
<p>Traverses the <a spec=HTML>joint session history</a> to the closest joint session history entry that matches the {{AppHistoryEntry}} with the given key. {{AppHistoryNavigationOptions/navigateInfo}} can be set to any value; it will populate the {{AppHistoryNavigateEvent/info}} property of the corresponding {{AppHistory/navigate}} event.

<p>The returned promise will behave as follows:

* If there is no {{AppHistoryEntry}} in {{AppHistory/entries|appHistory.entries}} with the given key, it will reject with an "{{InvalidStateError}}" {{DOMException}}.
* For same-document traversals intercepted by the {{AppHistory/navigate}} event's {{AppHistoryNavigateEvent/respondWith()}} method, it will fulfill or reject according to the promise passed to {{AppHistoryNavigateEvent/respondWith()}}.
* For non-intercepted same-document traversals, it will fulfill after the traversal completes.
* For cross-document traversals, it will never settle.
</dd>

<dt><code>await {{Window/appHistory}} . {{AppHistory/back()|back}}()</code>
<dt><code>await {{Window/appHistory}} . {{AppHistory/back(options)|back}}({ {{AppHistoryNavigationOptions/navigateInfo}} })</code>
<dd>
<p>Traverse the <a spec=HTML>joint session history</a> to the closest previous joint session history entry which results in this frame navigating, i.e. results in {{AppHistory/current|appHistory.current}} updating. {{AppHistoryNavigationOptions/navigateInfo}} can be set to any value; it will populate the {{AppHistoryNavigateEvent/info}} property of the corresponding {{AppHistory/navigate}} event.

<p>The returned promise will behave as follows:

* If {{AppHistory/canGoBack|appHistory.canGoBack}} is false, it will reject with an "{{InvalidStateError}}" {{DOMException}}.
* For same-document traversals intercepted by the {{AppHistory/navigate}} event's {{AppHistoryNavigateEvent/respondWith()}} method, it will fulfill or reject according to the promise passed to {{AppHistoryNavigateEvent/respondWith()}}.
* For non-intercepted same-document traversals, it will fulfill after the traversal completes.
* For cross-document traversals, it will never settle.
</dd>

<dt><code>await {{Window/appHistory}} . {{AppHistory/forward()|forward}}()</code>
<dt><code>await {{Window/appHistory}} . {{AppHistory/forward(options)|forward}}({ {{AppHistoryNavigationOptions/navigateInfo}} })</code>
<dd>
<p>Traverse the <a spec=HTML>joint session history</a> to the closest forward joint session history entry which results in this frame navigating, i.e. results in {{AppHistory/current|appHistory.current}} updating. {{AppHistoryNavigationOptions/navigateInfo}} can be set to any value; it will populate the {{AppHistoryNavigateEvent/info}} property of the corresponding {{AppHistory/navigate}} event.

<p>The returned promise will behave as follows:

* If {{AppHistory/canGoBack|appHistory.canGoForward}} is false, it will reject with an "{{InvalidStateError}}" {{DOMException}}.
* For same-document traversals intercepted by the {{AppHistory/navigate}} event's {{AppHistoryNavigateEvent/respondWith()}} method, it will fulfill or reject according to the promise passed to {{AppHistoryNavigateEvent/respondWith()}}.
* For non-intercepted same-document traversals, it will fulfill after the traversal completes.
* For cross-document traversals, it will never settle.
</dd>
</dl>

<div algorithm>
The <dfn method for="AppHistory">goTo(|key|, |options|)</dfn> method steps are:

1. If [=this=]'s [=AppHistory/current index=] is &minus;1, then return [=a promise rejected with=] an "{{InvalidStateError}}" {{DOMException}}.

1. If [=this=]'s [=AppHistory/entry list=] does not contain any {{AppHistoryEntry}} whose [=AppHistoryEntry/session history entry=]'s [=session history entry/app history key=] equals |key|, then return [=a promise rejected with=] an "{{InvalidStateError}}" {{DOMException}}.

1. Return the result of [=performing an app history traversal=] given [=this=], |key|, and |options|.
</div>

<div algorithm>
The <dfn method for="AppHistory">back(|options|)</dfn> method steps are:

1. If [=this=]'s [=AppHistory/current index=] is &minus;1 or 0, then return [=a promise rejected with=] an "{{InvalidStateError}}" {{DOMException}}.

1. Let |key| be [=this=]'s [=AppHistory/entry list=][[=this=]'s [=AppHistory/current index=] &minus; 1]'s [=AppHistoryEntry/session history entry=]'s [=session history entry/app history key=].

1. Return the result of [=performing an app history traversal=] given [=this=], |key|, and |options|.
</div>

<div algorithm>
The <dfn method for="AppHistory">forward(|options|)</dfn> method steps are:

1. If [=this=]'s [=AppHistory/current index=] is &minus;1 or is equal to [=this=]'s [=AppHistory/entry list=]'s [=list/size=] &minus; 1, then return [=a promise rejected with=] an "{{InvalidStateError}}" {{DOMException}}.

1. Let |key| be [=this=]'s [=AppHistory/entry list=][[=this=]'s [=AppHistory/current index=] + 1]'s [=AppHistoryEntry/session history entry=]'s [=session history entry/app history key=].

1. Return the result of [=performing an app history traversal=] given [=this=], |key|, and |options|.
</div>

<div algorithm>
<p class="advisement">The following algorithm is specified in terms of the <a href="https://github.com/whatwg/html/pull/6315">session history rewrite pull request</a> against the HTML Standard, because the existing session history traversal infrastructure is broken enough that it's hard to build on. It is expected to track that work as it continues.</p>

To <dfn>perform an app history traversal</dfn> given an {{AppHistory}} object |appHistory|, a string |key|, and an {{AppHistoryNavigationOptions}} |options|:

1. If |appHistory|'s [=relevant global object=]'s [=associated Document=] is not [=Document/fully active=], then return [=a promise rejected with=] an "{{InvalidStateError}}" {{DOMException}}.

1. If |appHistory|'s [=relevant global object=]'s [=associated Document=]'s <a spec="HTML">unload counter</a> is greater than 0, then return [=a promise rejected with=] an "{{InvalidStateError}}" {{DOMException}}.

1. If |appHistory|'s [=AppHistory/entry list=][|appHistory|'s [=AppHistory/current index=]]'s [=AppHistoryEntry/session history entry=]'s [=session history entry/app history key=] equals |key|, then return [=a promise resolved with=] undefined.

1. Let |navigateInfo| be |options|["{{AppHistoryNavigationOptions/navigateInfo}}"] if it [=map/exists=], or undefined otherwise.

1. Let |navigable| be |appHistory|'s [=relevant global object=]'s [=Window/browsing context=]'s [=browsing context/containing navigable=].

1. Let |traversable| be |navigable|'s [=navigable/traversable navigable=].

1. Let |initiatorBC| be |appHistory|'s [=relevant global object=]'s [=Window/browsing context=].

1. Let |promise| be [=a new promise=] created in |appHistory|'s [=relevant Realm=].

1. [=parallel queue/Enqueue the following steps=] on |traversable|'s [=traversable navigable/session history traversal queue=]:

1. Let |navigableEntries| be the result of [=navigable/getting the session history entries=] given |navigable|.

1. Let |targetEntry| be the [=session history entry=] in |navigableEntries| whose [=session history entry/app history key=] equals |key|. If no such entry exists, then:

1. [=Reject=] |promise| with an "{{InvalidStateError}}" {{DOMException}}.

1. Abort these steps.

<p class="note">This can occur if the |appHistory| object's view of session history is outdated, which can happen for brief periods while all the relevant threads and processes are being synchronized in reaction to a history change (such as the user clearing their history).

1. Assert: |targetEntry| is not |navigable|'s [=navigable/active session history entry=].

1. Let |targetStep| be null.

1. If |targetEntry|'s [=session history entry/step=] is greater than |traversable|'s [=traversable navigable/current session history step=], then set |targetStep| to |targetEntry|'s [=session history entry/step=].

1. Otherwise:

1. Let |afterTarget| be the [=session history entry=] after |targetEntry| in |navigableEntries|.

1. Let |allSteps| be the result of [=traversable navigable/getting all history steps=] that are part of the target session TODO.

1. Set |targetStep| to the greatest number in |allSteps| that is less than |afterTarget|'s [=session history entry/step=].

1. [=Apply the history step=] |targetStep| to |traversable|, with true, |initiatorBC|, "<code>[=user navigation involvement/none=]</code>", |navigateInfo|.

- If this aborts due to user-canceled unloading or the navigate event being canceled, then [=reject=] |promise| with an "{{AbortError}}" {{DOMException}}.

- If this aborts due to the initiator allowed-to-navigate check, then [=reject=] |promise| with a "{{SecurityError}}" {{DOMException}}.

- TODO: deal with another traversal aborting this one. (The HTML spec PR doesn't support this yet.) Should result in "{{AbortError}}" {{DOMException}}.

<p class="advisement">Eventually [=apply the history step=] will have well-specified hooks for communicating these conditions back to its caller.</p>
</div>

TODO need to stash info and then use it appropriately.

<h3 id="global-events">Event handlers</h3>

The following are the [=event handlers=] (and their corresponding [=event handler event types=]) that must be supported, as [=event handler IDL attributes=], by objects implementing the {{AppHistory}} interface:
Expand Down Expand Up @@ -626,7 +760,7 @@ The <dfn attribute for="AppHistoryDestination">sameDocument</dfn> getter steps a
<h3 id="navigate-event-firing">Firing the event</h3>

<div algorithm="fire a traversal navigate event">
To <dfn>fire a traversal `navigate` event</dfn> at an {{AppHistory}} |appHistory| given a [=session history entry=] <dfn for="fire a traversal navigate event">|destinationEntry|</dfn>, an optional [=user navigation involvement=] <dfn for="fire a traversal navigate event">|userInvolvement|</dfn> (default "<code>[=user navigation involvement/none=]</code>"), and an optional JavaScript value |info| (default undefined):
To <dfn>fire a traversal `navigate` event</dfn> at an {{AppHistory}} |appHistory| given a [=session history entry=] <dfn for="fire a traversal navigate event">|destinationEntry|</dfn>, an optional [=user navigation involvement=] <dfn for="fire a traversal navigate event">|userInvolvement|</dfn> (default "<code>[=user navigation involvement/none=]</code>"), and an optional JavaScript value <dfn for="fire a traversal navigate event">|info|</dfn> (default undefined):

1. Let |destinationURL| be |destinationEntry|'s [=session history entry/URL=].
1. Let |destinationState| be |destinationEntry|'s [=session history entry/app history state=].
Expand Down Expand Up @@ -760,8 +894,7 @@ interface AppHistoryEntry : EventTarget {
<dd>
<p>A [=user agent=]-generated random UUID string representing this app history entry's place in the app history list. This value will be reused by other {{AppHistoryEntry}} instances that replace this one due to replace-style navigations. This value will survive session restores.

<!-- TODO proper cross-link -->
<p>This is useful for navigating back to this location in the app history entry list, using `appHistory.goTo(key)`.
<p>This is useful for navigating back to this location in the app history entry list, using {{AppHistory/goTo(key)|appHistory.goTo(key)}}.
</dd>

<dt><code>entry . {{AppHistoryEntry/id}}</code>
Expand Down

0 comments on commit c202c9a

Please sign in to comment.