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