1function detectOS() {
2 var userAgent = navigator.userAgent;
3
4 var platform = navigator.platform;
5 var macosPlatforms = ["Macintosh", "MacIntel", "MacPPC", "Mac68K"];
6 var windowsPlatforms = ["Win32", "Win64", "Windows", "WinCE"];
7 var iosPlatforms = ["iPhone", "iPad", "iPod"];
8
9 if (macosPlatforms.indexOf(platform) !== -1) {
10 return "Mac";
11 } else if (iosPlatforms.indexOf(platform) !== -1) {
12 return "iOS";
13 } else if (windowsPlatforms.indexOf(platform) !== -1) {
14 return "Windows";
15 } else if (/Android/.test(userAgent)) {
16 return "Android";
17 } else if (/Linux/.test(platform)) {
18 return "Linux";
19 }
20
21 return "Unknown";
22}
23
24var os = detectOS();
25console.log("Operating System:", os);
26
27// Defer keybinding processing to avoid blocking initial render
28function updateKeybindings() {
29 const os = detectOS();
30 const isMac = os === "Mac" || os === "iOS";
31
32 function processKeybinding(element) {
33 const [macKeybinding, linuxKeybinding] = element.textContent.split("|");
34 element.textContent = isMac ? macKeybinding : linuxKeybinding;
35 element.classList.add("keybinding");
36 }
37
38 // Process all kbd elements at once (more efficient than walking entire DOM)
39 const kbdElements = document.querySelectorAll("kbd");
40 kbdElements.forEach(processKeybinding);
41}
42
43// Use requestIdleCallback if available, otherwise requestAnimationFrame
44if (typeof requestIdleCallback === "function") {
45 requestIdleCallback(updateKeybindings);
46} else {
47 requestAnimationFrame(updateKeybindings);
48}
49
50function darkModeToggle() {
51 var html = document.documentElement;
52 var themeToggleButton = document.getElementById("theme-toggle");
53 var themePopup = document.getElementById("theme-list");
54 var themePopupButtons = themePopup.querySelectorAll("button");
55
56 function setTheme(theme) {
57 html.setAttribute("data-theme", theme);
58 html.setAttribute("data-color-scheme", theme);
59 html.className = theme;
60 localStorage.setItem("mdbook-theme", theme);
61 }
62
63 themeToggleButton.addEventListener("click", function (event) {
64 event.preventDefault();
65 themePopup.style.display =
66 themePopup.style.display === "block" ? "none" : "block";
67 });
68
69 themePopupButtons.forEach(function (button) {
70 button.addEventListener("click", function () {
71 setTheme(this.id);
72 themePopup.style.display = "none";
73 });
74 });
75
76 document.addEventListener("click", function (event) {
77 if (
78 !themePopup.contains(event.target) &&
79 !themeToggleButton.contains(event.target)
80 ) {
81 themePopup.style.display = "none";
82 }
83 });
84
85 // Set initial theme
86 var currentTheme = localStorage.getItem("mdbook-theme");
87 if (currentTheme) {
88 setTheme(currentTheme);
89 } else {
90 // If no theme is set, use the system's preference
91 var systemPreference = window.matchMedia("(prefers-color-scheme: dark)")
92 .matches
93 ? "dark"
94 : "light";
95 setTheme(systemPreference);
96 }
97
98 // Listen for system's preference changes
99 const darkModeMediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
100 darkModeMediaQuery.addEventListener("change", function (e) {
101 if (!localStorage.getItem("mdbook-theme")) {
102 setTheme(e.matches ? "dark" : "light");
103 }
104 });
105}
106
107const copyMarkdown = () => {
108 const copyButton = document.getElementById("copy-markdown-toggle");
109 if (!copyButton) return;
110
111 // Store the original icon class, loading state, and timeout reference
112 const originalIconClass = "fa fa-copy";
113 let isLoading = false;
114 let iconTimeoutId = null;
115
116 const getCurrentPagePath = () => {
117 const pathname = window.location.pathname;
118
119 // Handle root docs path
120 if (pathname === "/docs/" || pathname === "/docs") {
121 return "getting-started.md";
122 }
123
124 // Remove /docs/ prefix and .html suffix, then add .md
125 const cleanPath = pathname
126 .replace(/^\/docs\//, "")
127 .replace(/\.html$/, "")
128 .replace(/\/$/, "");
129
130 return cleanPath ? cleanPath + ".md" : "getting-started.md";
131 };
132
133 const showToast = (message, isSuccess = true) => {
134 // Remove existing toast if any
135 const existingToast = document.getElementById("copy-toast");
136 existingToast?.remove();
137
138 const toast = document.createElement("div");
139 toast.id = "copy-toast";
140 toast.className = `copy-toast ${isSuccess ? "success" : "error"}`;
141 toast.textContent = message;
142
143 document.body.appendChild(toast);
144
145 // Show toast with animation
146 setTimeout(() => {
147 toast.classList.add("show");
148 }, 10);
149
150 // Hide and remove toast after 2 seconds
151 setTimeout(() => {
152 toast.classList.remove("show");
153 setTimeout(() => {
154 toast.parentNode?.removeChild(toast);
155 }, 300);
156 }, 2000);
157 };
158
159 const changeButtonIcon = (iconClass, duration = 1000) => {
160 const icon = copyButton.querySelector("i");
161 if (!icon) return;
162
163 // Clear any existing timeout
164 if (iconTimeoutId) {
165 clearTimeout(iconTimeoutId);
166 iconTimeoutId = null;
167 }
168
169 icon.className = iconClass;
170
171 if (duration > 0) {
172 iconTimeoutId = setTimeout(() => {
173 icon.className = originalIconClass;
174 iconTimeoutId = null;
175 }, duration);
176 }
177 };
178
179 const fetchAndCopyMarkdown = async () => {
180 // Prevent multiple simultaneous requests
181 if (isLoading) return;
182
183 try {
184 isLoading = true;
185 changeButtonIcon("fa fa-spinner fa-spin", 0); // Don't auto-restore spinner
186
187 const pagePath = getCurrentPagePath();
188 const rawUrl = `https://raw.githubusercontent.com/zed-industries/zed/main/docs/src/${pagePath}`;
189
190 const response = await fetch(rawUrl);
191 if (!response.ok) {
192 throw new Error(
193 `Failed to fetch markdown: ${response.status} ${response.statusText}`,
194 );
195 }
196
197 const markdownContent = await response.text();
198
199 // Copy to clipboard using modern API
200 if (navigator.clipboard?.writeText) {
201 await navigator.clipboard.writeText(markdownContent);
202 } else {
203 // Fallback: throw error if clipboard API isn't available
204 throw new Error("Clipboard API not supported in this browser");
205 }
206
207 changeButtonIcon("fa fa-check", 1000);
208 showToast("Page content copied to clipboard!");
209 } catch (error) {
210 console.error("Error copying markdown:", error);
211 changeButtonIcon("fa fa-exclamation-triangle", 2000);
212 showToast("Failed to copy markdown. Please try again.", false);
213 } finally {
214 isLoading = false;
215 }
216 };
217
218 copyButton.addEventListener("click", fetchAndCopyMarkdown);
219};
220
221// Initialize functionality when DOM is loaded
222document.addEventListener("DOMContentLoaded", () => {
223 darkModeToggle();
224 copyMarkdown();
225});
226
227// Collapsible sidebar navigation for entire sections
228// Note: Initial collapsed state is applied in index.hbs to prevent flicker
229function initCollapsibleSidebar() {
230 var sidebar = document.getElementById("sidebar");
231 if (!sidebar) return;
232
233 var chapterList = sidebar.querySelector("ol.chapter");
234 if (!chapterList) return;
235
236 var partTitles = Array.from(chapterList.querySelectorAll("li.part-title"));
237
238 partTitles.forEach(function (partTitle) {
239 // Get all sibling elements that belong to this section
240 var sectionItems = getSectionItems(partTitle);
241
242 if (sectionItems.length > 0) {
243 setupCollapsibleSection(partTitle, sectionItems);
244 }
245 });
246}
247
248// Saves the list of collapsed section names to sessionStorage
249// This gets reset when the tab is closed and opened again
250function saveCollapsedSections() {
251 var collapsedSections = [];
252 var partTitles = document.querySelectorAll(
253 "#sidebar li.part-title.collapsible",
254 );
255
256 partTitles.forEach(function (partTitle) {
257 if (!partTitle.classList.contains("expanded")) {
258 collapsedSections.push(partTitle._sectionName);
259 }
260 });
261
262 try {
263 sessionStorage.setItem(
264 "sidebar-collapsed-sections",
265 JSON.stringify(collapsedSections),
266 );
267 } catch (e) {
268 // sessionStorage might not be available
269 }
270}
271
272function getSectionItems(partTitle) {
273 var items = [];
274 var sibling = partTitle.nextElementSibling;
275
276 while (sibling) {
277 // Stop when we hit another part-title
278 if (sibling.classList.contains("part-title")) {
279 break;
280 }
281 items.push(sibling);
282 sibling = sibling.nextElementSibling;
283 }
284
285 return items;
286}
287
288function setupCollapsibleSection(partTitle, sectionItems) {
289 partTitle.classList.add("collapsible");
290 partTitle.setAttribute("role", "button");
291 partTitle.setAttribute("tabindex", "0");
292 partTitle._sectionItems = sectionItems;
293
294 var isCurrentlyCollapsed = partTitle._isCollapsed;
295 if (isCurrentlyCollapsed) {
296 partTitle.setAttribute("aria-expanded", "false");
297 } else {
298 partTitle.classList.add("expanded");
299 partTitle.setAttribute("aria-expanded", "true");
300 }
301
302 partTitle.addEventListener("click", function (e) {
303 e.preventDefault();
304 toggleSection(partTitle);
305 });
306
307 // a11y: Add keyboard support (Enter and Space)
308 partTitle.addEventListener("keydown", function (e) {
309 if (e.key === "Enter" || e.key === " ") {
310 e.preventDefault();
311 toggleSection(partTitle);
312 }
313 });
314}
315
316function toggleSection(partTitle) {
317 var isExpanded = partTitle.classList.contains("expanded");
318 var sectionItems = partTitle._sectionItems;
319 var spacerAfter = partTitle._spacerAfter;
320
321 if (isExpanded) {
322 partTitle.classList.remove("expanded");
323 partTitle.setAttribute("aria-expanded", "false");
324 sectionItems.forEach(function (item) {
325 item.classList.add("section-hidden");
326 });
327 if (spacerAfter) {
328 spacerAfter.classList.add("section-hidden");
329 }
330 } else {
331 partTitle.classList.add("expanded");
332 partTitle.setAttribute("aria-expanded", "true");
333 sectionItems.forEach(function (item) {
334 item.classList.remove("section-hidden");
335 });
336 if (spacerAfter) {
337 spacerAfter.classList.remove("section-hidden");
338 }
339 }
340
341 saveCollapsedSections();
342}
343
344document.addEventListener("DOMContentLoaded", function () {
345 initCollapsibleSidebar();
346});