// manages row selection state for queue table (single, multi, range) class QueueSelection { constructor(container, options = {}) { this.container = container; this.selected = new Set(); this.lastSelected = null; this.shiftAnchor = null; this.rowSelector = options.rowSelector || "tr"; this.indexAttribute = options.indexAttribute || "data-index"; this.selectedClass = options.selectedClass || "selected"; this.listeners = []; this.virtualScroller = options.virtualScroller || null; } getRowIndex(row) { // get index from row element return parseInt(row.getAttribute(this.indexAttribute)); } select(index, options = {}) { const { multi = false, shift = false } = options; if (shift && this.lastSelected !== null) { // range selection if (this.shiftAnchor === null) { this.shiftAnchor = this.lastSelected; } const start = Math.min(this.shiftAnchor, index); const end = Math.max(this.shiftAnchor, index); this.selected.clear(); for (let i = start; i <= end; i++) { this.selected.add(i); } } else if (multi) { // toggle selection with ctrl/cmd this.selected.has(index) ? this.selected.delete(index) : this.selected.add(index); this.shiftAnchor = null; } else { // single selection, clear others this.selected.clear(); this.selected.add(index); this.shiftAnchor = null; } this.lastSelected = index; this.updateUI(); this.focusSelectedRow(); this.notifyListeners(); } clear() { // clear selection this.selected.clear(); this.lastSelected = null; this.shiftAnchor = null; this.updateUI(); this.notifyListeners(); } isSelected(index) { // check if index is in selection return this.selected.has(index); } getSelected() { // return all selected indices sorted return Array.from(this.selected).sort((a, b) => a - b); } updateUI() { // update dom to match selection state, only change what's different const rows = this.container.querySelectorAll(this.rowSelector); rows.forEach((row) => { const idx = this.getRowIndex(row); const isSelected = this.selected.has(idx); const hasClass = row.classList.contains(this.selectedClass); // only update dom if state changed if (isSelected !== hasClass) { isSelected ? row.classList.add(this.selectedClass) : row.classList.remove(this.selectedClass); } }); } notifyListeners() { // notify all listeners of selection changes this.listeners.forEach((callback) => { callback(this.getSelected()); }); } setSelection(indices) { // set selection from external source this.selected.clear(); indices.forEach((idx) => this.selected.add(idx)); this.lastSelected = indices.length > 0 ? indices[indices.length - 1] : null; this.shiftAnchor = null; this.updateUI(); this.focusSelectedRow(); this.notifyListeners(); } focusSelectedRow() { // scroll selected row into view without focusing individual rows // (focus stays on container for keyboard nav during virtual scrolling) if (this.lastSelected === null) return; const rows = this.container.querySelectorAll(this.rowSelector); const row = Array.from(rows).find( (r) => this.getRowIndex(r) === this.lastSelected, ); if (row) { row.scrollIntoView({ block: "nearest", behavior: "auto" }); } else if (this.virtualScroller && this.virtualScroller.rowHeight > 0) { // row is outside visible range, use actual measured row height const targetScrollTop = Math.max( 0, this.lastSelected * this.virtualScroller.rowHeight - this.virtualScroller.container.clientHeight / 2, ); this.virtualScroller.container.scrollTop = targetScrollTop; } } navigateTo(index) { // navigate to a specific index const maxIdx = Math.max(0, (state?.queue?.length || 0) - 1); const boundedIdx = Math.min(Math.max(0, index), maxIdx); this.select(boundedIdx); } navigatePageUp(pageSize = 10) { // navigate up by page size const currentIdx = this.lastSelected ?? 0; this.navigateTo(currentIdx - pageSize); } navigatePageDown(pageSize = 10) { // navigate down by page size const currentIdx = this.lastSelected ?? 0; this.navigateTo(currentIdx + pageSize); } executeAction(actionName, handlers = {}) { // execute an action with provided handler functions const selected = this.getSelected(); if (selected.length === 0) return; handlers[actionName]?.(selected); } }