]> git.lizzy.rs Git - rust.git/blob - src/librustdoc/html/static/js/settings.js
core: Support variety of atomic widths in width-agnostic functions
[rust.git] / src / librustdoc / html / static / js / settings.js
1 // Local js definitions:
2 /* global getSettingValue, getVirtualKey, updateLocalStorage, updateSystemTheme */
3 /* global addClass, removeClass, onEach, onEachLazy, blurHandler, elemIsInParent */
4 /* global MAIN_ID, getVar, getSettingsButton */
5
6 "use strict";
7
8 (function() {
9     const isSettingsPage = window.location.pathname.endsWith("/settings.html");
10
11     function changeSetting(settingName, value) {
12         if (settingName === "theme") {
13             const useSystem = value === "system preference" ? "true" : "false";
14             updateLocalStorage("use-system-theme", useSystem);
15         }
16         updateLocalStorage(settingName, value);
17
18         switch (settingName) {
19             case "theme":
20             case "preferred-dark-theme":
21             case "preferred-light-theme":
22                 updateSystemTheme();
23                 updateLightAndDark();
24                 break;
25             case "line-numbers":
26                 if (value === true) {
27                     window.rustdoc_add_line_numbers_to_examples();
28                 } else {
29                     window.rustdoc_remove_line_numbers_from_examples();
30                 }
31                 break;
32         }
33     }
34
35     function handleKey(ev) {
36         // Don't interfere with browser shortcuts
37         if (ev.ctrlKey || ev.altKey || ev.metaKey) {
38             return;
39         }
40         switch (getVirtualKey(ev)) {
41             case "Enter":
42             case "Return":
43             case "Space":
44                 ev.target.checked = !ev.target.checked;
45                 ev.preventDefault();
46                 break;
47         }
48     }
49
50     function showLightAndDark() {
51         removeClass(document.getElementById("preferred-light-theme").parentElement, "hidden");
52         removeClass(document.getElementById("preferred-dark-theme").parentElement, "hidden");
53     }
54
55     function hideLightAndDark() {
56         addClass(document.getElementById("preferred-light-theme").parentElement, "hidden");
57         addClass(document.getElementById("preferred-dark-theme").parentElement, "hidden");
58     }
59
60     function updateLightAndDark() {
61         const useSystem = getSettingValue("use-system-theme");
62         if (useSystem === "true" || (useSystem === null && getSettingValue("theme") === null)) {
63             showLightAndDark();
64         } else {
65             hideLightAndDark();
66         }
67     }
68
69     function setEvents(settingsElement) {
70         updateLightAndDark();
71         onEachLazy(settingsElement.querySelectorAll("input[type=\"checkbox\"]"), toggle => {
72             const settingId = toggle.id;
73             const settingValue = getSettingValue(settingId);
74             if (settingValue !== null) {
75                 toggle.checked = settingValue === "true";
76             }
77             toggle.onchange = function() {
78                 changeSetting(this.id, this.checked);
79             };
80             toggle.onkeyup = handleKey;
81             toggle.onkeyrelease = handleKey;
82         });
83         onEachLazy(settingsElement.getElementsByClassName("select-wrapper"), elem => {
84             const select = elem.getElementsByTagName("select")[0];
85             const settingId = select.id;
86             const settingValue = getSettingValue(settingId);
87             if (settingValue !== null) {
88                 select.value = settingValue;
89             }
90             select.onchange = function() {
91                 changeSetting(this.id, this.value);
92             };
93         });
94         onEachLazy(settingsElement.querySelectorAll("input[type=\"radio\"]"), elem => {
95             const settingId = elem.name;
96             let settingValue = getSettingValue(settingId);
97             if (settingId === "theme") {
98                 const useSystem = getSettingValue("use-system-theme");
99                 if (useSystem === "true" || settingValue === null) {
100                     if (useSystem !== "false") {
101                         settingValue = "system preference";
102                     } else {
103                         // This is the default theme.
104                         settingValue = "light";
105                     }
106                 }
107             }
108             if (settingValue !== null && settingValue !== "null") {
109                 elem.checked = settingValue === elem.value;
110             }
111             elem.addEventListener("change", ev => {
112                 changeSetting(ev.target.name, ev.target.value);
113             });
114         });
115     }
116
117     /**
118      * This function builds the sections inside the "settings page". It takes a `settings` list
119      * as argument which describes each setting and how to render it. It returns a string
120      * representing the raw HTML.
121      *
122      * @param {Array<Object>} settings
123      *
124      * @return {string}
125      */
126     function buildSettingsPageSections(settings) {
127         let output = "";
128
129         for (const setting of settings) {
130             output += "<div class=\"setting-line\">";
131             const js_data_name = setting["js_name"];
132             const setting_name = setting["name"];
133
134             if (setting["options"] !== undefined) {
135                 // This is a select setting.
136                 output += `\
137 <div class="radio-line" id="${js_data_name}">
138     <div class="setting-name">${setting_name}</div>
139 <div class="choices">`;
140                 onEach(setting["options"], option => {
141                     const checked = option === setting["default"] ? " checked" : "";
142                     const full = `${js_data_name}-${option.replace(/ /g,"-")}`;
143
144                     output += `\
145 <label for="${full}" class="choice">
146     <input type="radio" name="${js_data_name}"
147         id="${full}" value="${option}"${checked}>
148     <span>${option}</span>
149 </label>`;
150                 });
151                 output += "</div></div>";
152             } else {
153                 // This is a checkbox toggle.
154                 const checked = setting["default"] === true ? " checked" : "";
155                 output += `\
156 <label class="settings-toggle">\
157     <input type="checkbox" id="${js_data_name}"${checked}>\
158     <span class="label">${setting_name}</span>\
159 </label>`;
160             }
161             output += "</div>";
162         }
163         return output;
164     }
165
166     /**
167      * This function builds the "settings page" and returns the generated HTML element.
168      *
169      * @return {HTMLElement}
170      */
171     function buildSettingsPage() {
172         const theme_names = getVar("themes").split(",").filter(t => t);
173         theme_names.push("light", "dark", "ayu");
174
175         const settings = [
176             {
177                 "name": "Theme",
178                 "js_name": "theme",
179                 "default": "system preference",
180                 "options": theme_names.concat("system preference"),
181             },
182             {
183                 "name": "Preferred light theme",
184                 "js_name": "preferred-light-theme",
185                 "default": "light",
186                 "options": theme_names,
187             },
188             {
189                 "name": "Preferred dark theme",
190                 "js_name": "preferred-dark-theme",
191                 "default": "dark",
192                 "options": theme_names,
193             },
194             {
195                 "name": "Auto-hide item contents for large items",
196                 "js_name": "auto-hide-large-items",
197                 "default": true,
198             },
199             {
200                 "name": "Auto-hide item methods' documentation",
201                 "js_name": "auto-hide-method-docs",
202                 "default": false,
203             },
204             {
205                 "name": "Auto-hide trait implementation documentation",
206                 "js_name": "auto-hide-trait-implementations",
207                 "default": false,
208             },
209             {
210                 "name": "Directly go to item in search if there is only one result",
211                 "js_name": "go-to-only-result",
212                 "default": false,
213             },
214             {
215                 "name": "Show line numbers on code examples",
216                 "js_name": "line-numbers",
217                 "default": false,
218             },
219             {
220                 "name": "Disable keyboard shortcuts",
221                 "js_name": "disable-shortcuts",
222                 "default": false,
223             },
224         ];
225
226         // Then we build the DOM.
227         const elementKind = isSettingsPage ? "section" : "div";
228         const innerHTML = `<div class="settings">${buildSettingsPageSections(settings)}</div>`;
229         const el = document.createElement(elementKind);
230         el.id = "settings";
231         if (!isSettingsPage) {
232             el.className = "popover";
233         }
234         el.innerHTML = innerHTML;
235
236         if (isSettingsPage) {
237             document.getElementById(MAIN_ID).appendChild(el);
238         } else {
239             el.setAttribute("tabindex", "-1");
240             getSettingsButton().appendChild(el);
241         }
242         return el;
243     }
244
245     const settingsMenu = buildSettingsPage();
246
247     function displaySettings() {
248         settingsMenu.style.display = "";
249     }
250
251     function settingsBlurHandler(event) {
252         blurHandler(event, getSettingsButton(), window.hidePopoverMenus);
253     }
254
255     if (isSettingsPage) {
256         // We replace the existing "onclick" callback to do nothing if clicked.
257         getSettingsButton().onclick = function(event) {
258             event.preventDefault();
259         };
260     } else {
261         // We replace the existing "onclick" callback.
262         const settingsButton = getSettingsButton();
263         const settingsMenu = document.getElementById("settings");
264         settingsButton.onclick = function(event) {
265             if (elemIsInParent(event.target, settingsMenu)) {
266                 return;
267             }
268             event.preventDefault();
269             const shouldDisplaySettings = settingsMenu.style.display === "none";
270
271             window.hideAllModals();
272             if (shouldDisplaySettings) {
273                 displaySettings();
274             }
275         };
276         settingsButton.onblur = settingsBlurHandler;
277         settingsButton.querySelector("a").onblur = settingsBlurHandler;
278         onEachLazy(settingsMenu.querySelectorAll("input"), el => {
279             el.onblur = settingsBlurHandler;
280         });
281         settingsMenu.onblur = settingsBlurHandler;
282     }
283
284     // We now wait a bit for the web browser to end re-computing the DOM...
285     setTimeout(() => {
286         setEvents(settingsMenu);
287         // The setting menu is already displayed if we're on the settings page.
288         if (!isSettingsPage) {
289             displaySettings();
290         }
291         removeClass(getSettingsButton(), "rotate");
292     }, 0);
293 })();