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// 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});