| 1 | /* shell.js — phone navigation for the sidebar and console shells. |
| 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; its href is the "up" block |
| 12 | [rst-shell-nav] a the index's rows |
| 13 | |
| 14 | What it adds, three things: |
| 15 | 1. Direction. In pagereveal it types the view transition "back" or |
| 16 | "forward", which shell.css turns into a slide. |
| 17 | 2. Back reuses history. A plain click on the back control calls |
| 18 | history.back() when, and only when, the Navigation API proves the |
| 19 | entry behind this one is the up page, in another document; the |
| 20 | index then comes back from the back/forward cache as it was left. |
| 21 | 3. Focus returns to the section you left. Leaving a content page |
| 22 | records its path in sessionStorage; revealing the index at phone |
| 23 | width focuses the nav link to that path, or else the one the URL's |
| 24 | #fragment names. |
| 25 | |
| 26 | The layouts load it with blocking="render": the direction is set in |
| 27 | pagereveal, at the first render, and a deferred script can run after |
| 28 | it. An engine that ignores blocking gets an untyped transition, which |
| 29 | shell.css draws as a cross-fade. */ |
| 30 | (function () { |
| 31 | "use strict"; |
| 32 | |
| 33 | var ROOT = "[rst-shell-sidebar],[rst-shell-console],.rst-shell-sidebar,.rst-shell-console"; |
| 34 | var INDEX = '[rst-shell-sidebar~="index"],[rst-shell-console~="index"],.rst-shell-sidebar--index,.rst-shell-console--index'; |
| 35 | var PAGE = '[rst-shell-sidebar~="page"],[rst-shell-console~="page"],.rst-shell-sidebar--page,.rst-shell-console--page'; |
| 36 | var BACK = "[rst-shell-back] a[href],.rst-shell__back a[href]"; |
| 37 | var NAV = "[rst-shell-nav] a[href],.rst-shell__nav a[href]"; |
| 38 | var NARROW = "(max-width: 799.98px)"; |
| 39 | var RETURN = "rst-shell-return", WENT_BACK = "rst-shell-back"; |
| 40 | |
| 41 | // sessionStorage can throw: storage disabled, a quota, a sandboxed |
| 42 | // frame. Any throw reads as "no record", so the fragment rule below |
| 43 | // still works. |
| 44 | function store(fn) { |
| 45 | try { return fn(window.sessionStorage); } catch (e) { return null; } |
| 46 | } |
| 47 | // One-shot: read and remove, so a record is consumed at most once. |
| 48 | function take(key) { |
| 49 | return store(function (s) { var v = s.getItem(key); s.removeItem(key); return v; }); |
| 50 | } |
| 51 | // A URL as this script compares URLs: resolved against the page, path |
| 52 | // and query, fragment ignored; null for another origin. |
| 53 | function place(href) { |
| 54 | var u = new URL(href, location.href); |
| 55 | return u.origin === location.origin ? u.pathname + u.search : null; |
| 56 | } |
| 57 | function view(sel) { |
| 58 | var r = document.querySelector(ROOT); |
| 59 | return !!r && r.matches(sel); |
| 60 | } |
| 61 | |
| 62 | // 3. Focus return, read when an index is revealed and never while |
| 63 | // prerendering: a prerendered index runs its first scripts with a copy |
| 64 | // of storage taken before the reader left the page, and pagereveal at |
| 65 | // activation is the first moment the fresh record is there. The record |
| 66 | // wins over the fragment: after going back to /#nav-invoices and then |
| 67 | // visiting Orders, a history return restores the old fragment. The |
| 68 | // record is taken before the width test: an index revealed wide would |
| 69 | // otherwise leave it behind, and a later phone-width index reached some |
| 70 | // other way would focus a link the reader left long ago. |
| 71 | function returnFocus() { |
| 72 | if (document.prerendering || !view(INDEX)) return; |
| 73 | var rec = take(RETURN), links, hit = null, i, id; |
| 74 | if (!matchMedia(NARROW).matches) return; |
| 75 | links = document.querySelectorAll(NAV); |
| 76 | for (i = 0; rec && !hit && i < links.length; i++) { |
| 77 | if (place(links[i].href) === rec) hit = links[i]; |
| 78 | } |
| 79 | if (!hit && location.hash) { |
| 80 | try { id = decodeURIComponent(location.hash.slice(1)); } catch (e) { id = location.hash.slice(1); } |
| 81 | hit = document.getElementById(id); |
| 82 | if (hit && !hit.matches(NAV)) hit = null; |
| 83 | } |
| 84 | if (hit) hit.focus(); |
| 85 | } |
| 86 | |
| 87 | // 1. Direction. Back when the page being left recorded that its back |
| 88 | // control was used AND this page is where it pointed, or when the |
| 89 | // Navigation API reports a traverse to an earlier entry (the browser's |
| 90 | // own Back); forward otherwise. The record is written on the click, |
| 91 | // before anything commits, so a Back whose leave-page prompt was |
| 92 | // cancelled leaves it behind: scoped to its destination, the next link |
| 93 | // followed still slides forward. Taken on every reveal, matched or not, |
| 94 | // so a stale one never waits for its URL to come round. A URL pattern |
| 95 | // cannot decide it: the new page cannot know which URL is the index, |
| 96 | // and the pattern is the app's. |
| 97 | function reveal(e) { |
| 98 | var back = take(WENT_BACK) === location.pathname + location.search, a = window.navigation && navigation.activation; |
| 99 | if (!back && a && a.navigationType === "traverse" && a.from && a.entry) back = a.entry.index < a.from.index; |
| 100 | // Focus first, and the type only where there is a set to add it to: |
| 101 | // an engine with cross-document transitions but no types must not |
| 102 | // turn a cosmetic step into a throw that skips the focus return. |
| 103 | returnFocus(); |
| 104 | if (e.viewTransition && e.viewTransition.types) e.viewTransition.types.add(back ? "back" : "forward"); |
| 105 | } |
| 106 | if ("onpagereveal" in window) window.addEventListener("pagereveal", reveal); |
| 107 | else window.addEventListener("pageshow", returnFocus); |
| 108 | |
| 109 | // The record is written when a content page is LEFT, which covers |
| 110 | // every way back: the back control, the browser's Back, a deep link. |
| 111 | // pageswap fires on the outgoing document for every navigation, the |
| 112 | // activation of a prerendered page included, before the new one is |
| 113 | // revealed; pagehide is the fallback where there is no pageswap. |
| 114 | // Never both: a pagehide landing after the index took the record |
| 115 | // would leave a stale one behind. |
| 116 | function leave() { |
| 117 | if (view(PAGE)) store(function (s) { s.setItem(RETURN, location.pathname + location.search); }); |
| 118 | } |
| 119 | window.addEventListener("onpageswap" in window ? "pageswap" : "pagehide", leave); |
| 120 | |
| 121 | // 2. Back reuses history, only when it can prove what is behind: a |
| 122 | // plain primary click, the Navigation API, and the entry before this |
| 123 | // one a DIFFERENT document whose path and query are the back link's |
| 124 | // (index, page, #section on the same page: Back must not stop at the |
| 125 | // fragment). A redirect behind "up" fails the test and the link is |
| 126 | // followed. Anything unproven follows the link and history grows by |
| 127 | // one, which is the scriptless behaviour, not a failure. The referrer |
| 128 | // is not consulted: it names the document's referrer, not the entry |
| 129 | // behind this one. |
| 130 | document.addEventListener("click", function (e) { |
| 131 | var a = e.target.closest && e.target.closest(BACK), nav = window.navigation, cur, prev, want; |
| 132 | if (!a || e.defaultPrevented || e.button !== 0 || e.ctrlKey || e.metaKey || e.shiftKey || e.altKey) return; |
| 133 | want = place(a.href); |
| 134 | if (want !== null) store(function (s) { s.setItem(WENT_BACK, want); }); |
| 135 | cur = nav && nav.currentEntry; |
| 136 | if (!cur || cur.index < 1) return; |
| 137 | prev = nav.entries()[cur.index - 1]; |
| 138 | if (want !== null && prev && !prev.sameDocument && prev.url && place(prev.url) === want) { |
| 139 | e.preventDefault(); |
| 140 | history.back(); |
| 141 | } |
| 142 | }); |
| 143 | })(); |
| 144 | |