]>
Commit | Line | Data |
---|---|---|
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. | |
6 | "use strict"; | |
7 | ||
8 | const builtinThemes = ["light", "dark", "ayu"]; | |
9 | const darkThemes = ["dark", "ayu"]; | |
10 | window.currentTheme = document.getElementById("themeStyle"); | |
11 | ||
12 | const settingsDataset = (function() { | |
13 | const settingsElement = document.getElementById("default-settings"); | |
14 | return settingsElement && settingsElement.dataset ? settingsElement.dataset : null; | |
15 | })(); | |
16 | ||
17 | function getSettingValue(settingName) { | |
18 | const current = getCurrentValue(settingName); | |
19 | if (current === null && settingsDataset !== null) { | |
20 | // See the comment for `default_settings.into_iter()` etc. in | |
21 | // `Options::from_matches` in `librustdoc/config.rs`. | |
22 | const def = settingsDataset[settingName.replace(/-/g,"_")]; | |
23 | if (def !== undefined) { | |
24 | return def; | |
25 | } | |
26 | } | |
27 | return current; | |
28 | } | |
29 | ||
30 | const localStoredTheme = getSettingValue("theme"); | |
31 | ||
32 | // eslint-disable-next-line no-unused-vars | |
33 | function hasClass(elem, className) { | |
34 | return elem && elem.classList && elem.classList.contains(className); | |
35 | } | |
36 | ||
37 | function addClass(elem, className) { | |
38 | if (elem && elem.classList) { | |
39 | elem.classList.add(className); | |
40 | } | |
41 | } | |
42 | ||
43 | // eslint-disable-next-line no-unused-vars | |
44 | function removeClass(elem, className) { | |
45 | if (elem && elem.classList) { | |
46 | elem.classList.remove(className); | |
47 | } | |
48 | } | |
49 | ||
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 | |
54 | */ | |
55 | function onEach(arr, func) { | |
56 | for (const elem of arr) { | |
57 | if (func(elem)) { | |
58 | return true; | |
59 | } | |
60 | } | |
61 | return false; | |
62 | } | |
63 | ||
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 | |
72 | */ | |
73 | // eslint-disable-next-line no-unused-vars | |
74 | function onEachLazy(lazyArray, func) { | |
75 | return onEach( | |
76 | Array.prototype.slice.call(lazyArray), | |
77 | func); | |
78 | } | |
79 | ||
80 | function updateLocalStorage(name, value) { | |
81 | try { | |
82 | window.localStorage.setItem("rustdoc-" + name, value); | |
83 | } catch (e) { | |
84 | // localStorage is not accessible, do nothing | |
85 | } | |
86 | } | |
87 | ||
88 | function getCurrentValue(name) { | |
89 | try { | |
90 | return window.localStorage.getItem("rustdoc-" + name); | |
91 | } catch (e) { | |
92 | return null; | |
93 | } | |
94 | } | |
95 | ||
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) { | |
99 | const el = document.querySelector("head > meta[name='rustdoc-vars']"); | |
100 | return el ? el.attributes["data-" + name].value : null; | |
101 | }); | |
102 | ||
103 | function switchTheme(newThemeName, saveTheme) { | |
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 | ||
112 | // If this new value comes from a system setting or from the previously | |
113 | // saved theme, no need to save it. | |
114 | if (saveTheme) { | |
115 | updateLocalStorage("theme", newThemeName); | |
116 | } | |
117 | ||
118 | document.documentElement.setAttribute("data-theme", newThemeName); | |
119 | ||
120 | if (builtinThemes.indexOf(newThemeName) !== -1) { | |
121 | if (window.currentTheme) { | |
122 | window.currentTheme.parentNode.removeChild(window.currentTheme); | |
123 | window.currentTheme = null; | |
124 | } | |
125 | } else { | |
126 | const newHref = getVar("root-path") + encodeURIComponent(newThemeName) + | |
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 | } | |
144 | } | |
145 | } | |
146 | ||
147 | const updateTheme = (function() { | |
148 | // only listen to (prefers-color-scheme: dark) because light is the default | |
149 | const mql = window.matchMedia("(prefers-color-scheme: dark)"); | |
150 | ||
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() { | |
161 | // maybe the user has disabled the setting in the meantime! | |
162 | if (getSettingValue("use-system-theme") !== "false") { | |
163 | const lightTheme = getSettingValue("preferred-light-theme") || "light"; | |
164 | const darkTheme = getSettingValue("preferred-dark-theme") || "dark"; | |
165 | updateLocalStorage("use-system-theme", "true"); | |
166 | ||
167 | // use light theme if user prefers it, or has no preference | |
168 | switchTheme(mql.matches ? darkTheme : lightTheme, true); | |
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 | |
172 | } else { | |
173 | switchTheme(getSettingValue("theme"), false); | |
174 | } | |
175 | } | |
176 | ||
177 | mql.addEventListener("change", updateTheme); | |
178 | ||
179 | return updateTheme; | |
180 | })(); | |
181 | ||
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) { | |
188 | updateLocalStorage("preferred-dark-theme", localStoredTheme); | |
189 | } | |
190 | } | |
191 | ||
192 | updateTheme(); | |
193 | ||
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. | |
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. | |
202 | addClass(document.documentElement, "src-sidebar-expanded"); | |
203 | } | |
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", | |
214 | desktopSidebarWidth + "px", | |
215 | ); | |
216 | } | |
217 | const srcSidebarWidth = getSettingValue("src-sidebar-width"); | |
218 | if (srcSidebarWidth && srcSidebarWidth !== "null") { | |
219 | document.documentElement.style.setProperty( | |
220 | "--src-sidebar-width", | |
221 | srcSidebarWidth + "px", | |
222 | ); | |
223 | } | |
224 | } | |
225 | updateSidebarWidth(); | |
226 | ||
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. | |
236 | window.addEventListener("pageshow", ev => { | |
237 | if (ev.persisted) { | |
238 | setTimeout(updateTheme, 0); | |
239 | setTimeout(updateSidebarWidth, 0); | |
240 | } | |
241 | }); |