1// rclone.js â replaces bootstrap.js + jquery + popper + custom.js 2 3(function() { 4 "use strict"; 5 6 // ===== Navbar collapse toggle ===== 7 document.addEventListener("click", function(e) { 8 var toggler = e.target.closest(".navbar-toggler"); 9 if (!toggler) return; 10 var targetId = toggler.getAttribute("data-target"); 11 if (!targetId) return; 12 var target = document.querySelector(targetId); 13 if (target) target.classList.toggle("show"); 14 }); 15 16 // ===== Dropdown toggle ===== 17 document.addEventListener("click", function(e) { 18 var toggle = e.target.closest("[data-toggle='dropdown']"); 19 20 // Close all open dropdowns first 21 var openMenus = document.querySelectorAll(".dropdown-menu.show"); 22 for (var i = 0; i < openMenus.length; i++) { 23 // Don't close the menu we're about to toggle 24 if (toggle && openMenus[i].parentNode.contains(toggle)) continue; 25 openMenus[i].classList.remove("show"); 26 } 27 28 if (!toggle) return; 29 e.preventDefault(); 30 31 var menu = toggle.nextElementSibling; 32 if (menu && menu.classList.contains("dropdown-menu")) { 33 menu.classList.toggle("show"); 34 } 35 }); 36 37 // ===== Mega menu: mobile headers + filter input ===== 38 var preScrollMenus = document.querySelectorAll(".dropdown-menu.pre-scrollable"); 39 for (var m = 0; m < preScrollMenus.length; m++) { 40 (function(menu) { 41 var toggleBtn = menu.previousElementSibling; 42 if (!toggleBtn) return; 43 var label = toggleBtn.textContent.trim().replace(/\s*â¾$/, ""); 44 45 // Mobile close header 46 var header = document.createElement("div"); 47 header.className = "dropdown-mobile-header"; 48 var labelSpan = document.createElement("span"); 49 labelSpan.textContent = label; 50 var closeSpan = document.createElement("span"); 51 closeSpan.className = "dropdown-mobile-close"; 52 closeSpan.innerHTML = "×"; 53 header.appendChild(labelSpan); 54 header.appendChild(closeSpan); 55 menu.insertBefore(header, menu.firstChild); 56 header.addEventListener("click", function() { 57 menu.classList.remove("show"); 58 }); 59 60 // Filter input (desktop + mobile) 61 var filterWrap = document.createElement("div"); 62 filterWrap.className = "dropdown-filter-wrap"; 63 var filterInput = document.createElement("input"); 64 filterInput.className = "dropdown-filter-input"; 65 filterInput.type = "text"; 66 filterInput.placeholder = "Filter\u2026"; 67 filterInput.setAttribute("autocomplete", "off"); 68 filterWrap.appendChild(filterInput); 69 // Insert after mobile header 70 header.insertAdjacentElement("afterend", filterWrap); 71 72 // Prevent clicks on the filter from closing the dropdown 73 filterWrap.addEventListener("click", function(e) { e.stopPropagation(); }); 74 75 // Filtering logic 76 var items = menu.querySelectorAll(".dropdown-item"); 77 var headings = menu.querySelectorAll(".dropdown-letter-heading"); 78 var dividers = menu.querySelectorAll(".dropdown-divider"); 79 80 filterInput.addEventListener("input", function() { 81 var query = filterInput.value.toLowerCase(); 82 83 // Show/hide items 84 for (var i = 0; i < items.length; i++) { 85 if (!query || items[i].textContent.toLowerCase().indexOf(query) !== -1) { 86 items[i].classList.remove("filter-hidden"); 87 } else { 88 items[i].classList.add("filter-hidden"); 89 } 90 } 91 92 // Show/hide letter headings: hide if all following items until next heading/divider are hidden 93 for (var h = 0; h < headings.length; h++) { 94 var hasVisible = false; 95 var sibling = headings[h].nextElementSibling; 96 while (sibling && !sibling.classList.contains("dropdown-letter-heading") && !sibling.classList.contains("dropdown-divider")) { 97 if (sibling.classList.contains("dropdown-item") && !sibling.classList.contains("filter-hidden")) { 98 hasVisible = true; 99 break; 100 } 101 sibling = sibling.nextElementSibling; 102 } 103 if (hasVisible || !query) { 104 headings[h].classList.remove("filter-hidden"); 105 } else { 106 headings[h].classList.add("filter-hidden"); 107 } 108 } 109 110 // Hide dividers if filtering is active 111 for (var d = 0;
111 d < dividers.length; d++) { 112 if (query) { 113 dividers[d].classList.add("filter-hidden"); 114 } else { 115 dividers[d].classList.remove("filter-hidden"); 116 } 117 } 118 }); 119 120 // Enter: if exactly one visible result, navigate to it 121 filterInput.addEventListener("keydown", function(e) { 122 if (e.key === "Enter") { 123 var visible = []; 124 for (var i = 0; i < items.length; i++) { 125 if (!items[i].classList.contains("filter-hidden")) visible.push(items[i]); 126 } 127 if (visible.length === 1 && visible[0].href) { 128 window.location.href = visible[0].href; 129 } 130 } 131 }); 132 133 // Escape: clear filter, or close dropdown if already clear 134 filterInput.addEventListener("keydown", function(e) { 135 if (e.key === "Escape") { 136 if (filterInput.value) { 137 filterInput.value = ""; 138 filterInput.dispatchEvent(new Event("input")); 139 e.stopPropagation(); 140 } else { 141 menu.classList.remove("show"); 142 } 143 } 144 }); 145 146 // Store reference for auto-focus 147 menu._filterInput = filterInput; 148 })(preScrollMenus[m]); 149 } 150 151 // Auto-focus filter input when a pre-scrollable dropdown opens, clear on close 152 document.addEventListener("click", function(e) { 153 var toggle = e.target.closest("[data-toggle='dropdown']"); 154 if (!toggle) return; 155 var menu = toggle.nextElementSibling; 156 if (!menu || !menu.classList.contains("pre-scrollable") || !menu._filterInput) return; 157 // Defer to next frame so the .show class has been toggled 158 setTimeout(function() { 159 if (menu.classList.contains("show")) { 160 menu._filterInput.value = ""; 161 menu._filterInput.dispatchEvent(new Event("input")); 162 menu._filterInput.focus(); 163 } 164 }, 0); 165 }); 166 167 // ===== Header hover links ===== 168 var headings = document.querySelectorAll("h2, h3, h4, h5, h6"); 169 for (var j = 0; j < headings.length; j++) { 170 var el = headings[j]; 171 var id = el.getAttribute("id"); 172 if (id) { 173 var a = document.createElement("a"); 174 a.className = "header-link"; 175 a.href = "#" + id; 176 a.innerHTML = '<svg class="icon"><use href="#icon-link"/></svg>'; 177 el.insertBefore(a, el.firstChild); 178 } 179 } 180 181 // ===== TOC expand / collapse ===== 182 var colToc = document.querySelector(".col-toc:not(.col-toc-empty)"); 183 var tocSidebar = colToc ? colToc.querySelector(".toc-sidebar") : null; 184 if (colToc) { 185 var tocToggle = colToc.querySelector(".toc-toggle"); 186 187 // Toggle button opens/closes the overlay panel 188 if (tocToggle) { 189 tocToggle.addEventListener("click", function(e) { 190 e.stopPropagation(); 191 colToc.classList.toggle("toc-open"); 192 }); 193 } 194 195 // Clicking a TOC link closes the overlay 196 if (tocSidebar) { 197 tocSidebar.addEventListener("click", function(e) { 198 if (e.target.closest("a")) { 199 colToc.classList.remove("toc-open"); 200 } 201 }); 202 } 203 204 // Clicking outside the panel closes it 205 document.addEventListener("click", function(e) { 206 if (!colToc.classList.contains("toc-open")) return; 207 if (e.target.closest(".toc-sidebar") || e.target.closest(".toc-toggle")) return; 208 colToc.classList.remove("toc-open"); 209 }); 210 } 211 212 // ===== TOC active section highlighting ===== 213 if (tocSidebar) { 214 var tocLinks = tocSidebar.querySelectorAll("nav#TableOfContents a"); 215 216 // Tooltip for truncated TOC links 217 var tocTooltip = document.createElement("div"); 218 tocTooltip.className = "toc-tooltip"; 219 tocSidebar.appendChild(tocTooltip); 220 221 for (var i = 0; i < tocLinks.length; i++) { 222 (function(link) { 223 link.addEventListener("mouseenter", function() { 224 if (link.scrollWidth <= link.clientWidth) return; 225 tocTooltip.textContent = link.textContent; 226 var linkRect = link.getBoundingClientRect(); 227 var sidebarRect = tocSidebar.getBoundingClientRect(); 228 tocTooltip.style.top = (linkRect.bottom - sidebarRect.top + tocSidebar.scrollTop) + "px"; 229 tocTooltip.classList.add("visible"); 230 }); 231 link.addEventListener("mouseleave", function() { 232 tocTooltip.classList.remove("visible"); 233 }); 234 })(tocLinks[i]); 235 } 236 237 if (tocLinks.length > 0) { 238 // Build a map from heading id to TOC link 239 var tocMap = {}; 240 var observedHeadings = []; 241 for (var t = 0; t < tocLinks.length; t++) { 242 var href = tocLinks[t].getAttribute("href"); 243 if (href && href.charAt(0) === "#") { 244 var targetId = href.substring(1); 245 var heading = document.getElementById(targetId); 246 if (heading) { 247 tocMap[targetId] = tocLinks[t]; 248 observedHeadings.push(heading); 249 } 250 } 251 } 252 253 if (observedHeadings.length > 0) { 254 var currentActive = null; 255 var suppressObserver = false;
256 257 function activateTocLink(link) { 258 if (currentActive) currentActive.classList.remove("toc-active"); 259 currentActive = link; 260 currentActive.classList.add("toc-active"); 261 // Keep the active link visible in the sidebar (nearest, not centered) 262 var sidebarRect = tocSidebar.getBoundingClientRect(); 263 var linkRect = link.getBoundingClientRect(); 264 if (linkRect.top < sidebarRect.top) { 265 tocSidebar.scrollBy({ top: linkRect.top - sidebarRect.top - 10, behavior: "smooth" }); 266 } else if (linkRect.bottom > sidebarRect.bottom) { 267 tocSidebar.scrollBy({ top: linkRect.bottom - sidebarRect.bottom + 10, behavior: "smooth" }); 268 } 269 } 270 271 function centerTocLink(link) { 272 // Centre the link vertically in the sidebar 273 // (don't use scrollIntoView â it scrolls the whole page) 274 var sidebarRect = tocSidebar.getBoundingClientRect(); 275 var linkRect = link.getBoundingClientRect(); 276 var offset = linkRect.top - sidebarRect.top - (sidebarRect.height / 2) + (linkRect.height / 2); 277 tocSidebar.scrollBy({ top: offset, behavior: "smooth" }); 278 } 279 280 // Suppress observer during smooth scroll (click or hash navigation) 281 function suppressAndActivate(link) { 282 suppressObserver = true; 283 activateTocLink(link); 284 // Re-enable after scroll completes 285 if ("onscrollend" in window) { 286 document.addEventListener("scrollend", function() { 287 suppressObserver = false; 288 }, { once: true }); 289 } else { 290 setTimeout(function() { suppressObserver = false; }, 1500); 291 } 292 } 293 294 var observer = new IntersectionObserver(function(entries) { 295 if (suppressObserver) return; 296 // Find the topmost visible heading 297 var topEntry = null; 298 for (var i = 0; i < entries.length; i++) { 299 if (entries[i].isIntersecting) { 300 if (!topEntry || entries[i].boundingClientRect.top < topEntry.boundingClientRect.top) { 301 topEntry = entries[i]; 302 } 303 } 304 } 305 if (topEntry) { 306 var id = topEntry.target.getAttribute("id"); 307 if (id && tocMap[id]) { 308 activateTocLink(tocMap[id]); 309 } 310 } 311 }, { rootMargin: "0px 0px -80% 0px", threshold: 0 }); 312 313 // Clicking a TOC link: suppress observer and highlight immediately 314 tocSidebar.addEventListener("click", function(e) { 315 var link = e.target.closest("a"); 316 if (!link) return; 317 var href = link.getAttribute("href"); 318 if (href && href.charAt(0) === "#") { 319 var id = decodeURIComponent(href.substring(1)); 320 if (tocMap[id]) { 321 suppressAndActivate(tocMap[id]); 322 } 323 } 324 }); 325 326 // On page load with a #hash, activate and centre immediately 327 if (window.location.hash) { 328 var hashId = decodeURIComponent(window.location.hash.substring(1)); 329 if (tocMap[hashId]) { 330 suppressAndActivate(tocMap[hashId]); 331 centerTocLink(tocMap[hashId]); 332 } 333 } 334 335 // Start observer 336 for (var h = 0; h < observedHeadings.length; h++) { 337 observer.observe(observedHeadings[h]); 338 } 339 } 340 } 341 } 342 343 // ===== Scrollable tables with sticky header + top scrollbar ===== 344 var tables = document.querySelectorAll(".col-content table"); 345 for (var ti = 0; ti < tables.length; ti++) { 346 (function(table) { 347 var parent = table.parentNode; 348 // Only wrap tables that overflow their container 349 if (table.scrollWidth <= parent.clientWidth) return; 350 351 // Must have a thead to split 352 var thead = table.querySelector("thead"); 353 if (!thead) return; 354 355 // Measure column widths from the rendered table 356 var firstRow = thead.querySelector("tr"); 357 if (!firstRow) return; 358 var cells = firstRow.children; 359 var widths = []; 360 for (var c = 0; c < cells.length; c++) { 361 widths.push(cells[c].getBoundingClientRect().width); 362 } 363 var tableWidth = table.scrollWidth; 364 365 // Helper: create a colgroup with explicit widths 366 function makeColgroup() { 367 var cg = document.createElement("colgroup"); 368 for (var i = 0; i < widths.length; i++) { 369 var col = document.createElement("col"); 370 col.style.width = widths[i] + "px"; 371 cg.appendChild(col); 372 } 373 return cg; 374 } 375 376 // Clone table for the sticky header (thead only) 377 var headerTable = table.cloneNode(true); 378 var cloneBody = headerTable.querySelector("tbody"); 379 if (cloneBody) cloneBody.remove(); 380 headerTable.insertBefore(makeColgroup(), headerTable.firstChild); 381 headerTable.style.width = tableWidth + "px"; 382 headerTable.style.minWidth = tableWidth + "px"; 383 384 // Lock column widths on the original table 385 table.insertBefore(makeColgroup(), table.firstChild); 386 table.style.width = tableWidth + "px"; 387 table.style.minWidth = tableWidth + "px"; 388 389 // Build: .table-scroll-wrap > (.table-scroll-header > inner > headerTable) 390 // + (.table-scroll-body > table) 391 var wrap = document.createElement("div"); 392 wrap.className = "table-scroll-wrap"; 393 394 var headerWrap = document.createElement("div"); 395 headerWrap.className = "table-scroll-header"; 396 var headerInner = document.createElement("div"); 397 headerInner.className = "table-scroll-header-inner"; 398 headerInner.appendChild(headerTable); 399 headerWrap.appendChild(headerInner); 400 401 var bodyWrap = document.createElement("div"); 402 bodyWrap.className = "table-scroll-body"; 403 404 // Scroll arrow buttons 405 var arrowsDiv = document.createElement("div"); 406 arrowsDiv.className = "table-scroll-arrows"; 407 var leftBtn = document.createElement("button"); 408 leftBtn.className = "table-scroll-arrow"; 409 leftBtn.setAttribute("aria-label", "Scroll table left"); 410 leftBtn.innerHTML = '<svg aria-hidden="true"><use href="#icon-chevron-left"/></svg>'; 411 var rightBtn = document.createElement("button"); 412 rightBtn.className = "table-scroll-arrow"; 413 rightBtn.setAttribute("aria-label", "Scroll table right"); 414 rightBtn.innerHTML = '<svg aria-hidden="true"><use href="#icon-chevron-right"/></svg>'; 415 arrowsDiv.appendChild(leftBtn); 416 arrowsDiv.appendChild(rightBtn); 417 418 parent.insertBefore(wrap, table); 419 bodyWrap.appendChild(table); 420 wrap.appendChild(arrowsDiv); 421 wrap.appendChild(headerWrap); 422 wrap.appendChild(bodyWrap); 423 424 // Update arrow visibility based on scroll position 425 function updateArrows() { 426 var scrollLeft = bodyWrap.scrollLeft; 427 var maxScroll = bodyWrap.scrollWidth - bodyWrap.clientWidth; 428 if (scrollLeft > 0) { 429 leftBtn.classList.add("visible"); 430 } else { 431 leftBtn.classList.remove("visible"); 432 } 433 if (scrollLeft < maxScroll - 1) { 434 rightBtn.classList.add("visible"); 435 } else { 436 rightBtn.classList.remove("visible"); 437 } 438 } 439 updateArrows(); 440 441 // Build column edge positions, skipping the sticky first column 442 var colEdges = []; 443 var edge = 0; 444 for (var ci = 0; ci < widths.length; ci++) { 445 edge += widths[ci]; 446 if (ci > 0) colEdges.push(edge); 447 } 448 function scrollTo(target) { 449 bodyWrap.scrollLeft = target; 450 headerInner.scrollLeft = target; 451 updateArrows(); 452 } 453 leftBtn.addEventListener("click", function() { 454 var pos = bodyWrap.scrollLeft; 455 for (var ci = colEdges.length - 1; ci >= 0; ci--) { 456 if (colEdges[ci] < pos - 1) { 457 scrollTo(colEdges[ci]); 458 return; 459 } 460 } 461 scrollTo(0); 462 }); 463 rightBtn.addEventListener("click", function() { 464 var pos = bodyWrap.scrollLeft; 465 for (var ci = 0; ci < colEdges.length; ci++) { 466 if (colEdges[ci] > pos + 1) { 467 scrollTo(colEdges[ci]); 468 return; 469 } 470 } 471 }); 472 473 // Sync horizontal scroll between header and body, update arrows 474 var syncing = false;
475 headerInner.addEventListener("scroll", function() { 476 if (syncing) return; 477 syncing = true; 478 bodyWrap.scrollLeft = headerInner.scrollLeft; 479 updateArrows(); 480 syncing = false; 481 }); 482 bodyWrap.addEventListener("scroll", function() { 483 if (syncing) return; 484 syncing = true; 485 headerInner.scrollLeft = bodyWrap.scrollLeft; 486 updateArrows(); 487 syncing = false; 488 }); 489 })(tables[ti]); 490 } 491 492 // ===== Copy to clipboard ===== 493 var copyIcon = '<svg class="icon" aria-hidden="true"><use href="#icon-copy"/></svg>'; 494 495 // Inject copy button into every pre block (outside pre, in a wrapper) 496 var pres = document.querySelectorAll("pre"); 497 for (var p = 0; p < pres.length; p++) { 498 var wrap = document.createElement("div"); 499 wrap.className = "pre-wrap"; 500 pres[p].parentNode.insertBefore(wrap, pres[p]); 501 wrap.appendChild(pres[p]); 502 var btn = document.createElement("button"); 503 btn.className = "copy-btn"; 504 btn.innerHTML = copyIcon; 505 btn.setAttribute("type", "button"); 506 wrap.appendChild(btn); 507 } 508 509 // Single delegated handler for all copy buttons (.copy-btn) 510 document.addEventListener("click", function(e) { 511 var btn = e.target.closest(".copy-btn"); 512 if (!btn) return; 513 514 // Determine what to copy 515 var text; 516 var preWrap = btn.closest(".pre-wrap"); 517 if (preWrap) { 518 // Code block: copy the code element text (excludes button) 519 var pre = preWrap.querySelector("pre"); 520 if (pre) { 521 var code = pre.querySelector("code"); 522 text = (code || pre).textContent; 523 } 524 } else { 525 // Input field: copy value of the preceding input 526 var input = btn.previousElementSibling; 527 if (input && input.value) text = input.value; 528 } 529 if (!text) return; 530 531 navigator.clipboard.writeText(text).then(function() { 532 btn.classList.add("copied"); 533 setTimeout(function() { 534 btn.classList.remove("copied"); 535 }, 2000); 536 }); 537 }); 538})(); 539 540// ===== Google site search ===== 541function on_search() { 542 document.search_form.q.value = document.search_form.words.value + " -site:forum.rclone.org"; 543 return true; 544}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.