]>
Commit | Line | Data |
---|---|---|
923072b8 FG |
1 | // storage.js is loaded in the `<head>` of all rustdoc pages and doesn't |
2 | // use `async` or `defer`. That means it blocks further parsing and rendering | |
3 | // of the page: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script. | |
4 | // This makes it the correct place to act on settings that affect the display of | |
5 | // the page, so we don't see major layout changes during the load of the page. | |
04454e1e FG |
6 | "use strict"; |
7 | ||
781aab86 | 8 | const builtinThemes = ["light", "dark", "ayu"]; |
04454e1e | 9 | const darkThemes = ["dark", "ayu"]; |
cdc7bbd5 | 10 | window.currentTheme = document.getElementById("themeStyle"); |
064997fb | 11 | |
923072b8 | 12 | const settingsDataset = (function() { |
04454e1e | 13 | const settingsElement = document.getElementById("default-settings"); |
353b0b11 | 14 | return settingsElement && settingsElement.dataset ? settingsElement.dataset : null; |
29967ef6 XL |
15 | })(); |
16 | ||
17 | function getSettingValue(settingName) { | |
04454e1e | 18 | const current = getCurrentValue(settingName); |
353b0b11 | 19 | if (current === null && settingsDataset !== null) { |
136023e0 XL |
20 | // See the comment for `default_settings.into_iter()` etc. in |
21 | // `Options::from_matches` in `librustdoc/config.rs`. | |
04454e1e | 22 | const def = settingsDataset[settingName.replace(/-/g,"_")]; |
29967ef6 XL |
23 | if (def !== undefined) { |
24 | return def; | |
25 | } | |
26 | } | |
353b0b11 | 27 | return current; |
29967ef6 XL |
28 | } |
29 | ||
04454e1e | 30 | const localStoredTheme = getSettingValue("theme"); |
29967ef6 | 31 | |
5869c6ff | 32 | // eslint-disable-next-line no-unused-vars |
a1dfa0c6 | 33 | function hasClass(elem, className) { |
0731742a | 34 | return elem && elem.classList && elem.classList.contains(className); |
a1dfa0c6 XL |
35 | } |
36 | ||
37 | function addClass(elem, className) { | |
353b0b11 FG |
38 | if (elem && elem.classList) { |
39 | elem.classList.add(className); | |
a1dfa0c6 XL |
40 | } |
41 | } | |
42 | ||
5869c6ff | 43 | // eslint-disable-next-line no-unused-vars |
a1dfa0c6 | 44 | function removeClass(elem, className) { |
353b0b11 FG |
45 | if (elem && elem.classList) { |
46 | elem.classList.remove(className); | |
a1dfa0c6 XL |
47 | } |
48 | } | |
49 | ||
a2a8927a XL |
50 | /** |
51 | * Run a callback for every element of an Array. | |
52 | * @param {Array<?>} arr - The array to iterate over | |
53 | * @param {function(?)} func - The callback | |
a2a8927a | 54 | */ |
4b012472 FG |
55 | function onEach(arr, func) { |
56 | for (const elem of arr) { | |
57 | if (func(elem)) { | |
58 | return true; | |
0531ce1d XL |
59 | } |
60 | } | |
83c7162d | 61 | return false; |
0531ce1d XL |
62 | } |
63 | ||
a2a8927a XL |
64 | /** |
65 | * Turn an HTMLCollection or a NodeList into an Array, then run a callback | |
66 | * for every element. This is useful because iterating over an HTMLCollection | |
67 | * or a "live" NodeList while modifying it can be very slow. | |
68 | * https://developer.mozilla.org/en-US/docs/Web/API/HTMLCollection | |
69 | * https://developer.mozilla.org/en-US/docs/Web/API/NodeList | |
70 | * @param {NodeList<?>|HTMLCollection<?>} lazyArray - An array to iterate over | |
71 | * @param {function(?)} func - The callback | |
a2a8927a | 72 | */ |
353b0b11 | 73 | // eslint-disable-next-line no-unused-vars |
4b012472 | 74 | function onEachLazy(lazyArray, func) { |
0731742a XL |
75 | return onEach( |
76 | Array.prototype.slice.call(lazyArray), | |
4b012472 | 77 | func); |
0731742a XL |
78 | } |
79 | ||
2c00a5a8 | 80 | function updateLocalStorage(name, value) { |
6a06907d | 81 | try { |
5099ac24 | 82 | window.localStorage.setItem("rustdoc-" + name, value); |
923072b8 | 83 | } catch (e) { |
6a06907d | 84 | // localStorage is not accessible, do nothing |
2c00a5a8 XL |
85 | } |
86 | } | |
87 | ||
88 | function getCurrentValue(name) { | |
6a06907d | 89 | try { |
5099ac24 | 90 | return window.localStorage.getItem("rustdoc-" + name); |
923072b8 | 91 | } catch (e) { |
6a06907d | 92 | return null; |
2c00a5a8 | 93 | } |
2c00a5a8 XL |
94 | } |
95 | ||
353b0b11 FG |
96 | // Get a value from the rustdoc-vars div, which is used to convey data from |
97 | // Rust to the JS. If there is no such element, return null. | |
98 | const getVar = (function getVar(name) { | |
fe692bf9 | 99 | const el = document.querySelector("head > meta[name='rustdoc-vars']"); |
353b0b11 FG |
100 | return el ? el.attributes["data-" + name].value : null; |
101 | }); | |
102 | ||
103 | function switchTheme(newThemeName, saveTheme) { | |
c0240ec0 FG |
104 | const themeNames = getVar("themes").split(",").filter(t => t); |
105 | themeNames.push(...builtinThemes); | |
106 | ||
107 | // Ensure that the new theme name is among the defined themes | |
108 | if (themeNames.indexOf(newThemeName) === -1) { | |
109 | return; | |
110 | } | |
111 | ||
29967ef6 XL |
112 | // If this new value comes from a system setting or from the previously |
113 | // saved theme, no need to save it. | |
17df50a5 | 114 | if (saveTheme) { |
487cf647 | 115 | updateLocalStorage("theme", newThemeName); |
0531ce1d XL |
116 | } |
117 | ||
781aab86 | 118 | document.documentElement.setAttribute("data-theme", newThemeName); |
353b0b11 | 119 | |
781aab86 FG |
120 | if (builtinThemes.indexOf(newThemeName) !== -1) { |
121 | if (window.currentTheme) { | |
122 | window.currentTheme.parentNode.removeChild(window.currentTheme); | |
123 | window.currentTheme = null; | |
124 | } | |
353b0b11 | 125 | } else { |
c0240ec0 | 126 | const newHref = getVar("root-path") + encodeURIComponent(newThemeName) + |
781aab86 FG |
127 | getVar("resource-suffix") + ".css"; |
128 | if (!window.currentTheme) { | |
129 | // If we're in the middle of loading, document.write blocks | |
130 | // rendering, but if we are done, it would blank the page. | |
131 | if (document.readyState === "loading") { | |
132 | document.write(`<link rel="stylesheet" id="themeStyle" href="${newHref}">`); | |
133 | window.currentTheme = document.getElementById("themeStyle"); | |
134 | } else { | |
135 | window.currentTheme = document.createElement("link"); | |
136 | window.currentTheme.rel = "stylesheet"; | |
137 | window.currentTheme.id = "themeStyle"; | |
138 | window.currentTheme.href = newHref; | |
139 | document.documentElement.appendChild(window.currentTheme); | |
140 | } | |
141 | } else if (newHref !== window.currentTheme.href) { | |
142 | window.currentTheme.href = newHref; | |
143 | } | |
0531ce1d | 144 | } |
2c00a5a8 XL |
145 | } |
146 | ||
9ffffee4 | 147 | const updateTheme = (function() { |
353b0b11 FG |
148 | // only listen to (prefers-color-scheme: dark) because light is the default |
149 | const mql = window.matchMedia("(prefers-color-scheme: dark)"); | |
150 | ||
9ffffee4 FG |
151 | /** |
152 | * Update the current theme to match whatever the current combination of | |
153 | * * the preference for using the system theme | |
154 | * (if this is the case, the value of preferred-light-theme, if the | |
155 | * system theme is light, otherwise if dark, the value of | |
156 | * preferred-dark-theme.) | |
157 | * * the preferred theme | |
158 | * … dictates that it should be. | |
159 | */ | |
160 | function updateTheme() { | |
29967ef6 XL |
161 | // maybe the user has disabled the setting in the meantime! |
162 | if (getSettingValue("use-system-theme") !== "false") { | |
04454e1e FG |
163 | const lightTheme = getSettingValue("preferred-light-theme") || "light"; |
164 | const darkTheme = getSettingValue("preferred-dark-theme") || "dark"; | |
353b0b11 | 165 | updateLocalStorage("use-system-theme", "true"); |
29967ef6 | 166 | |
353b0b11 FG |
167 | // use light theme if user prefers it, or has no preference |
168 | switchTheme(mql.matches ? darkTheme : lightTheme, true); | |
29967ef6 XL |
169 | // note: we save the theme so that it doesn't suddenly change when |
170 | // the user disables "use-system-theme" and reloads the page or | |
171 | // navigates to another page | |
5099ac24 | 172 | } else { |
353b0b11 | 173 | switchTheme(getSettingValue("theme"), false); |
29967ef6 XL |
174 | } |
175 | } | |
176 | ||
353b0b11 | 177 | mql.addEventListener("change", updateTheme); |
9ffffee4 FG |
178 | |
179 | return updateTheme; | |
180 | })(); | |
5099ac24 | 181 | |
29967ef6 XL |
182 | if (getSettingValue("use-system-theme") !== "false" && window.matchMedia) { |
183 | // update the preferred dark theme if the user is already using a dark theme | |
184 | // See https://github.com/rust-lang/rust/pull/77809#issuecomment-707875732 | |
185 | if (getSettingValue("use-system-theme") === null | |
186 | && getSettingValue("preferred-dark-theme") === null | |
187 | && darkThemes.indexOf(localStoredTheme) >= 0) { | |
5099ac24 | 188 | updateLocalStorage("preferred-dark-theme", localStoredTheme); |
29967ef6 | 189 | } |
29967ef6 | 190 | } |
5099ac24 | 191 | |
9ffffee4 FG |
192 | updateTheme(); |
193 | ||
4b012472 FG |
194 | // Hide, show, and resize the sidebar at page load time |
195 | // | |
196 | // This needs to be done here because this JS is render-blocking, | |
197 | // so that the sidebar doesn't "jump" after appearing on screen. | |
198 | // The user interaction to change this is set up in main.js. | |
923072b8 FG |
199 | if (getSettingValue("source-sidebar-show") === "true") { |
200 | // At this point in page load, `document.body` is not available yet. | |
201 | // Set a class on the `<html>` element instead. | |
add651ee | 202 | addClass(document.documentElement, "src-sidebar-expanded"); |
923072b8 | 203 | } |
4b012472 FG |
204 | if (getSettingValue("hide-sidebar") === "true") { |
205 | // At this point in page load, `document.body` is not available yet. | |
206 | // Set a class on the `<html>` element instead. | |
207 | addClass(document.documentElement, "hide-sidebar"); | |
208 | } | |
209 | function updateSidebarWidth() { | |
210 | const desktopSidebarWidth = getSettingValue("desktop-sidebar-width"); | |
211 | if (desktopSidebarWidth && desktopSidebarWidth !== "null") { | |
212 | document.documentElement.style.setProperty( | |
213 | "--desktop-sidebar-width", | |
e8be2606 | 214 | desktopSidebarWidth + "px", |
4b012472 FG |
215 | ); |
216 | } | |
217 | const srcSidebarWidth = getSettingValue("src-sidebar-width"); | |
218 | if (srcSidebarWidth && srcSidebarWidth !== "null") { | |
219 | document.documentElement.style.setProperty( | |
220 | "--src-sidebar-width", | |
e8be2606 | 221 | srcSidebarWidth + "px", |
4b012472 FG |
222 | ); |
223 | } | |
224 | } | |
225 | updateSidebarWidth(); | |
923072b8 | 226 | |
5099ac24 FG |
227 | // If we navigate away (for example to a settings page), and then use the back or |
228 | // forward button to get back to a page, the theme may have changed in the meantime. | |
229 | // But scripts may not be re-loaded in such a case due to the bfcache | |
230 | // (https://web.dev/bfcache/). The "pageshow" event triggers on such navigations. | |
231 | // Use that opportunity to update the theme. | |
232 | // We use a setTimeout with a 0 timeout here to put the change on the event queue. | |
233 | // For some reason, if we try to change the theme while the `pageshow` event is | |
234 | // running, it sometimes fails to take effect. The problem manifests on Chrome, | |
235 | // specifically when talking to a remote website with no caching. | |
04454e1e | 236 | window.addEventListener("pageshow", ev => { |
5099ac24 | 237 | if (ev.persisted) { |
9ffffee4 | 238 | setTimeout(updateTheme, 0); |
4b012472 | 239 | setTimeout(updateSidebarWidth, 0); |
5099ac24 FG |
240 | } |
241 | }); |