oficina / slides Public

Clone
git clone https://amadan.net/oficina/slides

Plain git — no account needed to clone.

Download

Download this file

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