plugins.js

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