PageSourceSearch

https://railsblocks.com/assets/controllers/checkbox_select_all_con…5494b0fc0bf32b77bf2fa8153b38dfd5eb118992.js

js railsblocks.com collected 2026-09-25 20:22:53 UTC 26,368 bytes, 717 lines download raw bytes

1import { Controller } from "@hotwired/stimulus";
2
3// Connects to data-controller="checkbox-select-all"
4export default class extends Controller {
5  static targets = [
6    "selectAll", // The "select all" checkbox that toggles all other checkboxes
7    "checkbox", // Individual checkboxes to be selected/deselected
8    "child", // Child checkboxes within a parent group (for nested hierarchies)
9    "parent", // Container element for parent-child checkbox groups
10    "actionBar", // Element shown/hidden based on selection state (e.g., bulk actions toolbar)
11    "count", // Element displaying the count of selected items
12    "total", // Element displaying the calculated total from selected amounts
13    "amount", // Elements containing numeric values to sum when their checkbox is selected
14    "pageSelectionInfo", // Shows "X of Y row(s) selected." (visible when NOT in all-pages mode)
15    "selectAllPagesPrompt", // Shows "Select all Y rows" button (when all on page selected but not all pages)
16    "allPagesSelectedInfo", // Shows "All Y row(s) selected." (when all pages are selected)
17    "allPagesInput", // Hidden input to pass all-pages selection state to server
18  ];
19  static values = {
20    toggleKey: { type: String, default: "" }, // Keyboard shortcut to toggle focused checkbox (recommended: "x")
21    baseAmount: { type: Number, default: 0 }, // Base amount for total calculation
22    totalItems: { type: Number, default: 0 }, // Total items across all pages (for "select all pages" feature)
23  };
24
25  connect() {
26    this.lastCheckedIndex = null;
27    this.lastCheckedState = null;
28    this.lastShiftEnd = null; // Track the endpoint of the last shift-selection
29    this.allPagesSelected = false; // Virtual "all pages" selection state
30    this.shiftHeld = false; // Track if shift key is currently held
31    this.updateSelectAllState();
32    this.updateActionBarVisibility();
33    this.updateTotal();
34
35    // Bind keyboard navigation
36    this.handleKeydown = this.handleKeydown.bind(this);
37    this.element.addEventListener("keydown", this.handleKeydown);
38
39    // Bind global shift key listeners for anchor indicator
40    this.handleGlobalKeydown = this.handleGlobalKeydown.bind(this);
41    this.handleGlobalKeyup = this.handleGlobalKeyup.bind(this);
42    this.handleWindowBlur = this.handleWindowBlur.bind(this);
43    document.addEventListener("keydown", this.handleGlobalKeydown);
44    document.addEventListener("keyup", this.handleGlobalKeyup);
45    window.addEventListener("blur", this.handleWindowBlur);
46  }
47
48  disconnect() {
49    this.element.removeEventListener("keydown", this.handleKeydown);
50    document.removeEventListener("keydown", this.handleGlobalKeydown);
51    document.removeEventListener("keyup", this.handleGlobalKeyup);
52    window.removeEventListener("blur", this.handleWindowBlur);
53    this.clearAnchorIndicator();
54  }
55
56  // Handle global keydown to show anchor indicator when shift is pressed
57  handleGlobalKeydown(event) {
58    if (event.key === "Shift" && !this.shiftHeld) {
59      this.shiftHeld = true;
60      this.updateAnchorIndicator();
61    }
62  }
63
64  // Handle global keyup to hide anchor indicator when shift is released
65  handleGlobalKeyup(event) {
66    if (event.key === "Shift") {
67      this.shiftHeld = false;
68      this.clearAnchorIndicator();
69    }
70  }
71
72  // Handle window blur to clear shift state (in case shift is released while window is not focused)
73  handleWindowBlur() {
74    this.shiftHeld = false;
75    this.clearAnchorIndicator();
76  }
77
78  // Show dashed outline on the anchor checkbox when shift is held
79  updateAnchorIndicator() {
80    this.clearAnchorIndicator();
81
82    if (this.shiftHeld && this.lastCheckedIndex !== null) {
83      const anchorCheckbox = this.checkboxTargets[this.lastCheckedIndex];
84      if (anchorCheckbox) {
85        anchorCheckbox.classList.add("checkbox-anchor");
86      }
87    }
88  }
89
90  // Remove anchor indicator from all checkboxes
91  clearAnchorIndicator() {
92    this.checkboxTargets.forEach((cb) => {
93      cb.classList.remove("checkbox-anchor");
94    });
95  }
96
97  // Check if we have more items than visible on current page
98  get hasMultiplePages() {
99    return this.hasTotalItemsValue && this.totalItemsValue > this.checkboxTargets.length;
100  }
101
102  // Check if all checkboxes on current page are selected
103  get allOnPageSelected() {
104    const enabledCheckboxes = this.checkboxTargets.filter((cb) => !cb.disabled);
105    return enabledCheckboxes.length > 0 && enabledCheckboxes.every((cb) => cb.checked);
106  }
107
108  // Toggle all checkboxes when the select-all checkbox is clicked
109  toggleAll(event) {
110    const isChecked = event.target.checked;
111
112    // Reset "all pages" selection when manually toggling
113    this.allPagesSelected = false;
114
115    // Select ALL checkboxes including nested ones
116    this.checkboxTargets.forEach((checkbox) => {
117      if (!checkbox.disabled) {
118        checkbox.checked = isChecked;
119        checkbox.indeterminate = false;
120      }
121    });
122
123    // Also update any parent checkboxes that aren't in the checkbox targets
124    this.element
125      .querySelectorAll('[data-checkbox-select-all-target="parent"] input[type="checkbox"]')
126      .forEach((parentCheckbox) => {
127        if (!parentCheckbox.disabled) {
128          parentCheckbox.checked = isChecked;
129          parentCheckbox.indeterminate = false;
130        }
131      });
132
133    this.lastCheckedIndex = null;
134    this.lastCheckedState = null;
135    this.lastShiftEnd = null;
136    this.clearAnchorIndicator();
137    this.updateSelectAllState();
138    this.updateActionBarVisibility();
139    this.updateTotal();
140  }
141
142  // Select all items across ALL pages (virtual selection)
143  // This doesn't load any data, just sets a flag that can be passed to the server
144  selectAllPages() {
145    // First, ensure all checkboxes on current page are selected
146    this.checkboxTargets.forEach((checkbox) => {
147      if (!checkbox.disabled) {
148        checkbox.checked = true;
149        checkbox.indeterminate = false;
150      }
151    });
152
153    if (this.hasSelectAllTarget) {
154      this.selectAllTarget.checked = true;
155      this.selectAllTarget.indeterminate = false;
156    }
157
158    // Set virtual "all pages" selection
159    this.allPagesSelected = true;
160
161    this.updateActionBarVisibility();
162    this.updateTotal();
163  }
164
165  // Clear "all pages" selection and uncheck everything
166  clearAllPages() {
167    this.allPagesSelected = false;
168    this.clearAll();
169  }
170
171  // Clear all selections (current page only, resets all-pages state)
172  clearAll() {
173    // Reset "all pages" selection
174    this.allPagesSelected = false;
175
176    this.checkboxTargets.forEach((checkbox) => {
177      if (!checkbox.disabled) {
178        checkbox.checked = false;
179        checkbox.indeterminate = false;
180      }
181    });
182
183    // Clear parent checkboxes too
184    this.element
185      .querySelectorAll('[data-checkbox-select-all-target="parent"] input[type="checkbox"]')
186      .forEach((parentCheckbox) => {
187        if (!parentCheckbox.disabled) {
188          parentCheckbox.checked = false;
189          parentCheckbox.indeterminate = false;
190        }
191      });
192
193    if (this.hasSelectAllTarget) {
194      this.selectAllTarget.checked = false;
195      this.selectAllTarget.indeterminate = false;
196    }
197
198    this.lastCheckedIndex = null;
199    this.lastCheckedState = null;
200    this.lastShiftEnd = null;
201    this.clearAnchorIndicator();
202    this.updateActionBarVisibility();
203    this.updateTotal();
204  }
205
206  // Handle individual checkbox clicks
207  toggle(event) {
208    const checkbox = event.target;
209    const currentIndex = this.checkboxTargets.indexOf(checkbox);
210
211    // If the clicked element isn't one of our targets, ignore
212    if (currentIndex === -1) return;
213
214    // Reset "all pages" selection when manually toggling individual items
215    this.allPagesSelected = false;
216
217    // Handle shift-click for batch selection
218    if (event.shiftKey && this.lastCheckedIndex !== null) {
219      const anchor = this.lastCheckedIndex;
220      const targetState = this.lastCheckedState;
221
222      const newStart = Math.min(anchor, currentIndex);
223      const newEnd = Math.max(anchor, currentIndex);
224
225      // If we had a previous shift selection, handle items outside the new range
226      if (this.lastShiftEnd !== null) {
227        const prevStart = Math.min(anchor, this.lastShiftEnd);
228        const prevEnd = Math.max(anchor, this.lastShiftEnd);
229
230        // Deselect items that were in the previous range but not in the new range
231        this.checkboxTargets.forEach((cb, index) => {
232          if (!cb.disabled) {
233            const wasInPrevRange = index >= prevStart && index <= prevEnd;
234            const isInNewRange = index >= newStart && index <= newEnd;
235
236            if (wasInPrevRange && !isInNewRange) {
237              // This item was selected by previous shift-click but is outside new range
238              cb.checked = !targetState;
239              cb.indeterminate = false;
240            }
241          }
242        });
243      }
244
245      // Apply the target state to all items in the new range
246      this.checkboxTargets.forEach((cb, index) => {
247        if (!cb.disabled && index >= newStart && index <= newEnd) {
248          cb.checked = targetState;
249          cb.indeterminate = false;
250        }
251      });
252
253      // Remember this shift end for future shift-clicks
254      this.lastShiftEnd = currentIndex;
255
256      // We do NOT update lastCheckedIndex on shift-click, preserving the original anchor
257    } else {
258      // Normal click - this becomes the new anchor
259      this.lastCheckedIndex = currentIndex;
260      this.lastCheckedState = checkbox.checked;
261      this.lastShiftEnd = null; // Reset shift selection tracking
262      this.updateAnchorIndicator(); // Update visual indicator for new anchor
263    }
264
265    this.updateAllParentStates();
266    this.updateSelectAllState();
267    this.updateActionBarVisibility();
268    this.updateTotal();
269  }
270
271  // Update the select-all checkbox state based on individual checkboxes
272  updateSelectAllState() {
273    if (!this.hasSelectAllTarget) return;
274
275    // Get all leaf checkboxes (checkboxes that don't have children)
276    // These are checkboxes that are NOT parent checkboxes
277    const leafCheckboxes = this.checkboxTargets.filter((cb) => {
278      if (cb.disabled) return false;
279      // Check if this checkbox is a parent (has a parent container with children)
280      const parentContainer = cb.closest('[data-checkbox-select-all-target="parent"]');
281      if (!parentContainer) return true; // Not in a parent container, so it's a leaf
282
283      // If it's in a parent container, check if it's the parent checkbox itself
284      const parentCheckbox = parentContainer.querySelector('input[type="checkbox"]');
285      return cb !== parentCheckbox; // Only include if it's not the parent checkbox
286    });
287
288    const checkedCount = leafCheckboxes.filter((cb) => cb.checked).length;
289
290    if (checkedCount === 0) {
291      this.selectAllTarget.checked = false;
292      this.selectAllTarget.indeterminate = false;
293    } else if (checkedCount === leafCheckboxes.length) {
294      this.selectAllTarget.checked = true;
295      this.selectAllTarget.indeterminate = false;
296    } else {
297      this.selectAllTarget.checked = false;
298      this.selectAllTarget.indeterminate = true;
299    }
300  }
301
302  // Update visibility of action bar based on selection state
303  updateActionBarVisibility() {
304    if (!this.hasActionBarTarget) return;
305
306    const checkedCount = this.checkboxTargets.filter((cb) => cb.checked && !cb.disabled).length;
307    const hasSelection = checkedCount > 0 || this.allPagesSelected;
308
309    // Show/hide action bar
310    this.actionBarTargets.forEach((actionBar) => {
311      actionBar.hidden = !hasSelection;
312    });
313
314    // Update "select all pages" UI elements
315    this.updateSelectAllPagesUI(checkedCount);
316
317    // Update hidden input for form submission
318    this.updateAllPagesInput();
319
320    this.updateCount();
321  }
322
323  // Update the "select all pages" UI elements
324  updateSelectAllPagesUI(checkedCount) {
325    const showSelectAllPrompt = this.allOnPageSelected && this.hasMultiplePages && !this.allPagesSelected;
326    const showAllPagesSelected = this.allPagesSelected;
327    const showPageSelection = !showAllPagesSelected;
328
329    // "X of Y row(s) selected." - shown when NOT in all-pages mode
330    this.pageSelectionInfoTargets.forEach((el) => {
331      el.classList.toggle("hidden", !showPageSelection);
332    });
333
334    // "Select all Y rows" prompt - shown when all on page selected but not all pages
335    this.selectAllPagesPromptTargets.forEach((el) => {
336      el.classList.toggle("hidden", !showSelectAllPrompt);
337    });
338
339    // "All Y row(s) selected." - shown when all pages selected
340    this.allPagesSelectedInfoTargets.forEach((el) => {
341      el.classList.toggle("hidden", !showAllPagesSelected);
342    });
343  }
344
345  // Update hidden input for form submission
346  // This allows the server to know if "all pages" is selected
347  updateAllPagesInput() {
348    this.allPagesInputTargets.forEach((input) => {
349      input.value = this.allPagesSelected ? "true" : "false";
350      input.disabled = !this.allPagesSelected;
351    });
352  }
353
354  // Update the count display
355  updateCount() {
356    if (!this.hasCountTarget) return;
357
358    // When all pages selected, show total count; otherwise show checked count
359    const displayCount = this.allPagesSelected
360      ? this.totalItemsValue
361      : this.checkboxTargets.filter((cb) => cb.checked && !cb.disabled).length;
362
363    this.countTargets.forEach((count) => {
364      count.textContent = `${displayCount}`;
365    });
366  }
367
368  // Update the total by summing amounts from checked items
369  updateTotal() {
370    if (!this.hasTotalTarget) return;
371
372    // Start with the base amount (e.g., subscription fee)
373    let total = this.baseAmountValue;
374
375    // For each checked checkbox, find its associated amount element
376    this.checkboxTargets.forEach((checkbox) => {
377      if (checkbox.checked && !checkbox.disabled) {
378        // Find the amount target in the same container as the checkbox
379        const container = checkbox.closest('[data-controller*="checkbox-select-all"]') || this.element;
380        const allAmounts = Array.from(container.querySelectorAll('[data-checkbox-select-all-target*="amount"]'));
381
382        // Find the amount that's in the same parent as this checkbox
383        const checkboxParent = checkbox.closest("label, div, tr, li");
384        if (checkboxParent) {
385          const amount = allAmounts.find((amt) => checkboxParent.contains(amt));
386          if (amount) {
387            // Parse the number from the amount's text content
388            // Remove currency symbols, commas, /mo, /year, etc.
389            const text = amount.textContent.trim();
390            const numberMatch = text.match(/[\d,]+\.?\d*/);
391            if (numberMatch) {
392              const value = parseFloat(numberMatch[0].replace(/,/g, ""));
393              if (!isNaN(value)) {
394                total += value;
395              }
396            }
397          }
398        }
399      }
400    });
401
402    // Update all total targets with the calculated sum
403    this.totalTargets.forEach((totalElement) => {
404      // Get the original format from the element (to preserve currency symbols, etc.)
405      const originalText = totalElement.textContent;
406      const currencyMatch = originalText.match(/^[^\d]*/);
407      const suffixMatch = originalText.match(/[^\d,.]+$/);
408
409      const prefix = currencyMatch ? currencyMatch[0] : "$";
410      const suffix = suffixMatch ? suffixMatch[0] : "";
411
412      // Format with 2 decimal places
413      const formattedTotal = total.toFixed(2);
414
415      totalElement.textContent = `${prefix}${formattedTotal}${suffix}`;
416    });
417  }
418
419  // Update state of all parent groups
420  updateAllParentStates() {
421    this.element.querySelectorAll('[data-checkbox-select-all-target="parent"]').forEach((container) => {
422      this.updateChildrenState(container);
423    });
424  }
425
426  // Update children checkboxes when parent is toggled
427  toggleChildren(event) {
428    const parentCheckbox = event.target;
429    const isChecked = parentCheckbox.checked;
430
431    const container = parentCheckbox.closest('[data-checkbox-select-all-target="parent"]');
432    if (!container) return;
433
434    // Select ALL child checkboxes at ALL nesting levels within this parent
435    const allChildCheckboxes = container.querySelectorAll('input[type="checkbox"]');
436    allChildCheckboxes.forEach((checkbox) => {
437      // Skip the parent checkbox itself
438      if (checkbox !== parentCheckbox && !checkbox.disabled) {
439        checkbox.checked = isChecked;
440        checkbox.indeterminate = false;
441      }
442    });
443
444    // Ensure parent stays in the correct state (checked or unchecked, not indeterminate)
445    parentCheckbox.indeterminate = false;
446
447    this.updateAllParentStates();
448    this.updateSelectAllState();
449    this.updateActionBarVisibility();
450    this.updateTotal();
451  }
452
453  // Update parent state based on children
454  updateChildrenState(container) {
455    const parentCheckboxInput = container.querySelector('input[type="checkbox"]');
456    if (!parentCheckboxInput) return;
457
458    // Get all checkboxes in the container
459    const allCheckboxes = Array.from(container.querySelectorAll('input[type="checkbox"]'));
460
461    // Filter to get only children (exclude the parent itself)
462    const childCheckboxes = allCheckboxes.filter((cb) => cb !== parentCheckboxInput && !cb.disabled);
463
464    const checkedChildren = childCheckboxes.filter((cb) => cb.checked).length;
465
466    if (checkedChildren === 0) {
467      parentCheckboxInput.checked = false;
468      parentCheckboxInput.indeterminate = false;
469    } else if (checkedChildren === childCheckboxes.length) {
470      parentCheckboxInput.checked = true;
471      parentCheckboxInput.indeterminate = false;
472    } else {
473      parentCheckboxInput.checked = false;
474      parentCheckboxInput.indeterminate = true;
475    }
476  }
477
478  // Keyboard navigation handler
479  handleKeydown(event) {
480    const activeElement = document.activeElement;
481
482    // Check if the active element is the select-all checkbox
483    const isSelectAll = this.hasSelectAllTarget && activeElement === this.selectAllTarget;
484
485    // Check if the active element is one of our checkboxes
486    const currentIndex = this.checkboxTargets.indexOf(activeElement);
487
488    // If it's neither, ignore
489    if (!isSelectAll && currentIndex === -1) return;
490
491    // Handle arrow key navigation (ArrowDown and ArrowRight move forward, ArrowUp and ArrowLeft move backward)
492    if (event.key === "ArrowDown" || event.key === "ArrowRight") {
493      event.preventDefault();
494      if (isSelectAll) {
495        // From select-all, go to first visible checkbox
496        this.focusFirstVisibleCheckbox();
497      } else {
498        // Try to go to next checkbox, or loop back if at end
499        const moved = this.focusNextCheckbox(currentIndex);
500        if (!moved) {
501          // At the last visible checkbox, loop back to select-all or first checkbox
502          if (this.hasSelectAllTarget) {
503            this.selectAllTarget.focus();
504          } else {
505            this.focusFirstVisibleCheckbox();
506          }
507        }
508      }
509    } else if (event.key === "ArrowUp" || event.key === "ArrowLeft") {
510      event.preventDefault();
511      if (isSelectAll) {
512        // From select-all, loop to last visible checkbox
513        this.focusLastVisibleCheckbox();
514      } else {
515        // Try to go to previous checkbox, or loop back if at start
516        const moved = this.focusPreviousCheckbox(currentIndex);
517        if (!moved) {
518          // At the first visible checkbox, go to select-all or loop to last
519          if (this.hasSelectAllTarget) {
520            this.selectAllTarget.focus();
521          } else {
522            this.focusLastVisibleCheckbox();
523          }
524        }
525      }
526    }
527    // Handle toggle key (if configured) - case insensitive
528    else if (this.hasToggleKeyValue && event.key.toLowerCase() === this.toggleKeyValue.toLowerCase()) {
529      event.preventDefault();
530
531      // Only handle toggle for regular checkboxes (not select-all)
532      if (!isSelectAll) {
533        // Shift + toggle key: batch toggle like shift-click
534        if (event.shiftKey && this.lastCheckedIndex !== null) {
535          const anchor = this.lastCheckedIndex;
536          const targetState = this.lastCheckedState;
537
538          const newStart = Math.min(anchor, currentIndex);
539          const newEnd = Math.max(anchor, currentIndex);
540
541          // If we had a previous shift selection, handle items outside the new range
542          if (this.lastShiftEnd !== null) {
543            const prevStart = Math.min(anchor, this.lastShiftEnd);
544            const prevEnd = Math.max(anchor, this.lastShiftEnd);
545
546            // Deselect items that were in the previous range but not in the new range
547            this.checkboxTargets.forEach((cb, index) => {
548              if (!cb.disabled) {
549                const wasInPrevRange = index >= prevStart && index <= prevEnd;
550                const isInNewRange = index >= newStart && index <= newEnd;
551
552                if (wasInPrevRange && !isInNewRange) {
553                  cb.checked = !targetState;
554                  cb.indeterminate = false;
555                }
556              }
557            });
558          }
559
560          // Apply the target state to all items in the new range
561          this.checkboxTargets.forEach((cb, index) => {
562            if (!cb.disabled && index >= newStart && index <= newEnd) {
563              cb.checked = targetState;
564              cb.indeterminate = false;
565            }
566          });
567
568          // Remember this shift end
569          this.lastShiftEnd = currentIndex;
570
571          this.updateAllParentStates();
572          this.updateSelectAllState();
573          this.updateActionBarVisibility();
574          this.updateTotal();
575        } else {
576          // Normal toggle: just click the focused checkbox
577          activeElement.click();
578        }
579      } else {
580        // For select-all, just click it
581        activeElement.click();
582      }
583    }
584  }
585
586  // Check if a checkbox is hidden (e.g., inside a collapsed folder)
587  isCheckboxHidden(checkbox) {
588    let current = checkbox;
589    while (current && current !== this.element) {
590      if (current.hasAttribute("hidden") || current.classList.contains("hidden")) {
591        return true;
592      }
593      // Check for collapsed state via data-state attribute
594      if (current.getAttribute("data-state") === "closed") {
595        return true;
596      }
597      current = current.parentElement;
598    }
599    return false;
600  }
601
602  // Check if a checkbox can be focused (not disabled and not hidden)
603  isCheckboxFocusable(checkbox) {
604    return !checkbox.disabled && !this.isCheckboxHidden(checkbox);
605  }
606
607  // Focus a checkbox at a specific index (returns true if successful)
608  focusCheckboxAtIndex(index) {
609    if (index >= 0 && index < this.checkboxTargets.length) {
610      const checkbox = this.checkboxTargets[index];
611      if (this.isCheckboxFocusable(checkbox)) {
612        checkbox.focus();
613        this.scrollToCheckbox(checkbox);
614        return true;
615      } else {
616        // If disabled or hidden, try the next one
617        if (index < this.checkboxTargets.length - 1) {
618          return this.focusCheckboxAtIndex(index + 1);
619        }
620      }
621    }
622    return false;
623  }
624
625  // Focus the first visible and enabled checkbox
626  focusFirstVisibleCheckbox() {
627    for (let i = 0; i < this.checkboxTargets.length; i++) {
628      if (this.isCheckboxFocusable(this.checkboxTargets[i])) {
629        this.checkboxTargets[i].focus();
630        this.scrollToCheckbox(this.checkboxTargets[i]);
631        return true;
632      }
633    }
634    return false;
635  }
636
637  // Focus the last visible and enabled checkbox
638  focusLastVisibleCheckbox() {
639    for (let i = this.checkboxTargets.length - 1; i >= 0; i--) {
640      if (this.isCheckboxFocusable(this.checkboxTargets[i])) {
641        this.checkboxTargets[i].focus();
642        this.scrollToCheckbox(this.checkboxTargets[i]);
643        return true;
644      }
645    }
646    return false;
647  }
648
649  // Focus the next enabled and visible checkbox (returns true if moved)
650  focusNextCheckbox(currentIndex) {
651    for (let i = currentIndex + 1; i < this.checkboxTargets.length; i++) {
652      const checkbox = this.checkboxTargets[i];
653      if (this.isCheckboxFocusable(checkbox)) {
654        checkbox.focus();
655        this.scrollToCheckbox(checkbox);
656        return true;
657      }
658    }
659    return false;
660  }
661
662  // Focus the previous enabled and visible checkbox (returns true if moved)
663  focusPreviousCheckbox(currentIndex) {
664    for (let i = currentIndex - 1; i >= 0; i--) {
665      const checkbox = this.checkboxTargets[i];
666      if (this.isCheckboxFocusable(checkbox)) {
667        checkbox.focus();
668        this.scrollToCheckbox(checkbox);
669        return true;
670      }
671    }
672    return false;
673  }
674
675  // Scroll checkbox into view with padding
676  scrollToCheckbox(checkbox) {
677    // Find the row element (for table layouts)
678    const row = checkbox.closest("tr");
679    const element = row || checkbox;
680
681    // Find the scrollable container
682    const content = element.closest(".overflow-y-auto, .overflow-auto");
683    if (!content) return;
684
685    const contentRect = content.getBoundingClientRect();
686    const elementRect = element.getBoundingClientRect();
687
688    // Find sticky elements that reduce visible scroll area
689    const stickyHeader = content.querySelector(".sticky.top-0, thead .sticky");
690    const stickyFooter = content.querySelector(".sticky.bottom-0");
691
692    const stickyHeaderHeight = stickyHeader ? stickyHeader.offsetHeight : 0;
693    const stickyFooterHeight = stickyFooter && !stickyFooter.hidden ? stickyFooter.offsetHeight : 0;
694
695    // Calculate the element's position relative to the scrollable content
696    const elementRelativeTop = elementRect.top - contentRect.top;
697    const elementRelativeBottom = elementRelativeTop + elementRect.height;
698    const contentScrollTop = content.scrollTop;
699    const contentHeight = contentRect.height;
700
701    // Define padding: scroll when the element is within this distance from the edge
702    const scrollPadding = elementRect.height * 1;
703
704    // Adjust visible area by excluding sticky elements
705    const visibleTop = stickyHeaderHeight + scrollPadding;
706    const visibleBottom = contentHeight - stickyFooterHeight - scrollPadding;
707
708    // Check if we need to scroll down (element is too far down)
709    if (elementRelativeBottom > visibleBottom) {
710      content.scrollTop = contentScrollTop + (elementRelativeBottom - visibleBottom);
711    }
712    // Check if we need to scroll up (element is too far up)
713    else if (elementRelativeTop < visibleTop) {
714      content.scrollTop = contentScrollTop + (elementRelativeTop - visibleTop);
715    }
716  }
717};

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.