rastrillo / fiesta Public

Clone
git clone https://amadan.net/rastrillo/fiesta

Plain git — no account needed to clone.

Download

Download this file

1/* shell.js — transition observation and phone shell navigation.
2 First-party, dependency-free, CSP-clean, and optional: delete it (and
3 its <script> tag) and every page still works, because the index and
4 the back control are ordinary server-rendered pages and links. It is
5 app-owned from the moment it is scaffolded.
6
7 What it reads, all written by the layouts, in either spelling:
8 [rst-shell-sidebar~="index"|"page"], [rst-shell-console~=…]
9 which view the server rendered (the "view"
10 block): .rst-shell-sidebar--index and so on
11 [rst-shell-back] > a the back control, the strip's own link; its
12 href is the "up" block. Only the direct child:
13 a page may put a menu in the strip too, and
14 its links are not a way back
15 [rst-shell-nav] a the index's rows
16
17 What it adds, three things:
18 1. Direction. In pagereveal it types the view transition "back" or
19 "forward", which shell.css turns into a slide.
20 2. Back reuses history. A plain click on the back control calls
21 history.back() when, and only when, the Navigation API proves the
22 entry behind this one is the up page, in another document; the
23 index then comes back from the back/forward cache as it was left.
24 3. Focus returns to the section you left. Leaving a content page
25 records its path in sessionStorage; revealing the index at phone
26 width focuses the nav link to that path, or else the one the URL's
27 #fragment names.
28
29 Sidebar and console use defer blocking="render" for focus and
30 direction after parsing. Other layouts use transition.js before
31 styles so incoming transitions skipped before pagereveal are observed. */
32(function () {
33 "use strict";
34
35 var ROOT = "[rst-shell-sidebar],[rst-shell-console],.rst-shell-sidebar,.rst-shell-console";
36 var INDEX = '[rst-shell-sidebar~="index"],[rst-shell-console~="index"],.rst-shell-sidebar--index,.rst-shell-console--index';
37 var PAGE = '[rst-shell-sidebar~="page"],[rst-shell-console~="page"],.rst-shell-sidebar--page,.rst-shell-console--page';
38 var BACK = "[rst-shell-back] > a[href],.rst-shell__back > a[href]";
39 var NAV = "[rst-shell-nav] a[href],.rst-shell__nav a[href]";
40 var NARROW = "(max-width: 799.98px)";
41 var RETURN = "rst-shell-return", WENT_BACK = "rst-shell-back";
42
43 // sessionStorage can throw: storage disabled, a quota, a sandboxed
44 // frame. Any throw reads as "no record", so the fragment rule below
45 // still works.
46 function store(fn) {
47 try { return fn(window.sessionStorage); } catch (e) { return null; }
48 }
49 // One-shot: read and remove, so a record is consumed at most once.
50 function take(key) {
51 return store(function (s) { var v = s.getItem(key); s.removeItem(key); return v; });
52 }
53 // A URL as this script compares URLs: resolved against the page, path
54 // and query, fragment ignored; null for another origin.
55 function place(href) {
56 var u = new URL(href, location.href);
57 return u.origin === location.origin ? u.pathname + u.search : null;
58 }
59 function view(sel) {
60 var r = document.querySelector(ROOT);
61 return !!r && r.matches(sel);
62 }
63
64 // 3. Focus return, read when an index is revealed and never while
65 // prerendering: a prerendered index runs its first scripts with a copy
66 // of storage taken before the reader left the page, and pagereveal at
67 // activation is the first moment the fresh record is there. The record
68 // wins over the fragment: after going back to /#nav-invoices and then
69 // visiting Orders, a history return restores the old fragment. The
70 // record is taken before the width test: an index revealed wide would
71 // otherwise leave it behind, and a later phone-width index reached some
72 // other way would focus a link the reader left long ago.
73 function returnFocus() {
74 if (document.prerendering || !view(INDEX)) return;
75 var rec = take(RETURN), links, hit = null, i, id;
76 if (!matchMedia(NARROW).matches) return;
77 links = document.querySelectorAll(NAV);
78 for (i = 0; rec && !hit && i < links.length; i++) {
79 if (place(links[i].href) === rec) hit = links[i];
80 }
81 if (!hit && location.hash) {
82 try { id = decodeURIComponent(location.hash.slice(1)); } catch (e) { id = location.hash.slice(1); }
83 hit = document.getElementById(id);
84 if (hit && !hit.matches(NAV)) hit = null;
85 }
86 if (hit) hit.focus();
87 }
88
89 // A skipped browser transition rejects even though navigation succeeds.
90 // Startup and pagereveal can expose the same object; observe it once.
91 var observed = new WeakSet();
92 function observeTransition(transition) {
93 if (!transition || observed.has(transition)) return;
94 observed.add(transition);
95 ["ready", "finished", "updateCallbackDone"].forEach(function (key) {
96 if (transition[key]) transition[key].catch(function (err) {
97 if (!(err instanceof DOMException) ||
98 ["AbortError", "InvalidStateError", "TimeoutError"].indexOf(err.name) < 0) throw err;
99 });
100 });
101 }
102
103 observeTransition(document.activeViewTransition);
104
105 // 1. Direction. The back record is scoped to its destination because
106 // a cancelled leave-page prompt can leave it behind. Consume it on
107 // every reveal so it cannot affect a later visit. Browser Back uses
108 // the Navigation API's entry order; URL patterns belong to the app.
109 function reveal(e) {
110 observeTransition(e.viewTransition);
111 var back = take(WENT_BACK) === location.pathname + location.search, a = window.navigation && navigation.activation;
112 if (!back && a && a.navigationType === "traverse" && a.from && a.entry) back = a.entry.index < a.from.index;
113 // Focus first, and the type only where there is a set to add it to:
114 // an engine with cross-document transitions but no types must not
115 // turn a cosmetic step into a throw that skips the focus return.
116 returnFocus();
117 if (e.viewTransition && e.viewTransition.types) e.viewTransition.types.add(back ? "back" : "forward");
118 }
119 if ("onpagereveal" in window) window.addEventListener("pagereveal", reveal);
120 else window.addEventListener("pageshow", returnFocus);
121
122 // The record is written when a content page is LEFT, which covers
123 // every way back: the back control, the browser's Back, a deep link.
124 // pageswap fires on the outgoing document for every navigation, the
125 // activation of a prerendered page included, before the new one is
126 // revealed; pagehide is the fallback where there is no pageswap.
127 // Never both: a pagehide landing after the index took the record
128 // would leave a stale one behind.
129 function leave(e) {
130 observeTransition(e.viewTransition);
131 if (view(PAGE)) store(function (s) { s.setItem(RETURN, location.pathname + location.search); });
132 }
133 window.addEventListener("onpageswap" in window ? "pageswap" : "pagehide", leave);
134
135 // 2. Back reuses history, only when it can prove what is behind: a
136 // plain primary click, the Navigation API, and the entry before this
137 // one a DIFFERENT document whose path and query are the back link's
138 // (index, page, #section on the same page: Back must not stop at the
139 // fragment). A redirect behind "up" fails the test and the link is
140 // followed. Anything unproven follows the link and history grows by
141 // one, which is the scriptless behaviour, not a failure. The referrer
142 // is not consulted: it names the document's referrer, not the entry
143 // behind this one.
144 document.addEventListener("click", function (e) {
145 var a = e.target.closest && e.target.closest(BACK), nav = window.navigation, cur, prev, want;
146 if (!a || e.defaultPrevented || e.button !== 0 || e.ctrlKey || e.metaKey || e.shiftKey || e.altKey) return;
147 want = place(a.href);
148 if (want !== null) store(function (s) { s.setItem(WENT_BACK, want); });
149 cur = nav && nav.currentEntry;
150 if (!cur || cur.index < 1) return;
151 prev = nav.entries()[cur.index - 1];
152 if (want !== null && prev && !prev.sameDocument && prev.url && place(prev.url) === want) {
153 e.preventDefault();
154 history.back();
155 }
156 });
157})();
158