PageSourceSearch

https://rclone.org/js/rclone.js?r=1788877872

js rclone.org collected 2026-10-01 07:20:47 UTC 23,198 bytes, 544 lines download raw bytes

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 = "&times;";
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.