ui.js

  1// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
  2//
  3// SPDX-License-Identifier: AGPL-3.0-or-later
  4
  5import { getSortedItems } from './render.js';
  6
  7export function setupKeyboardShortcuts(options) {
  8  const { items, vote, deleteItem, editItem, undo, render, getSelectedItemId, setSelectedItemId, setShouldScroll } = options;
  9  const listContainer = document.getElementById('list-container');
 10  
 11  document.addEventListener('keydown', (e) => {
 12    // Open help modal on '?'
 13    if (e.key === '?' && !e.target.matches('input, textarea')) {
 14      e.preventDefault();
 15      openHelpModal();
 16      return;
 17    }
 18    
 19    // Close help modal on Esc
 20    if (e.key === 'Escape') {
 21      const helpModal = document.getElementById('help-modal');
 22      if (!helpModal.classList.contains('hidden')) {
 23        e.preventDefault();
 24        closeHelpModal();
 25        return;
 26      }
 27    }
 28    
 29    // Ignore if typing in input/textarea
 30    if (e.target.matches('input, textarea')) return;
 31    
 32    if (listContainer && listContainer.classList.contains('disabled')) return;
 33    
 34    // Undo
 35    if ((e.ctrlKey || e.metaKey) && e.key === 'z') {
 36      e.preventDefault();
 37      undo();
 38      return;
 39    }
 40    
 41    // Navigation and actions only work if we have items
 42    if (items.length === 0) return;
 43    
 44    const sorted = getSortedItems(items);
 45    const selectedItemId = getSelectedItemId();
 46    
 47    // Navigation: j/k or ArrowDown/ArrowUp
 48    if (e.key === 'j' || e.key === 'ArrowDown') {
 49      e.preventDefault();
 50      const currentIdx = sorted.findIndex(i => i.id === selectedItemId);
 51      const nextIdx = Math.min(currentIdx + 1, sorted.length - 1);
 52      setSelectedItemId(sorted[nextIdx].id);
 53      setShouldScroll(true);
 54      render();
 55    } else if (e.key === 'k' || e.key === 'ArrowUp') {
 56      e.preventDefault();
 57      const currentIdx = sorted.findIndex(i => i.id === selectedItemId);
 58      const prevIdx = Math.max(currentIdx - 1, 0);
 59      setSelectedItemId(sorted[prevIdx].id);
 60      setShouldScroll(true);
 61      render();
 62    }
 63    
 64    // Actions on selected item
 65    if (selectedItemId) {
 66      if (e.key === '1' || e.key === 'Enter') {
 67        e.preventDefault();
 68        vote(selectedItemId, 'up');
 69      } else if (e.key === '2') {
 70        e.preventDefault();
 71        vote(selectedItemId, 'down');
 72      } else if (e.key === '3') {
 73        e.preventDefault();
 74        vote(selectedItemId, 'veto');
 75      } else if (e.key === 'e') {
 76        e.preventDefault();
 77        editItem(selectedItemId);
 78      } else if (e.key === 'Delete' || e.key === 'Backspace') {
 79        e.preventDefault();
 80        deleteItem(selectedItemId);
 81      }
 82    }
 83  });
 84}
 85
 86// Help modal
 87export function setupHelpModal() {
 88  const helpModal = document.getElementById('help-modal');
 89  const helpModalClose = helpModal.querySelector('.modal-close');
 90  
 91  helpModalClose.addEventListener('click', closeHelpModal);
 92  
 93  // Close modal on backdrop click
 94  helpModal.addEventListener('click', (e) => {
 95    if (e.target === helpModal) {
 96      closeHelpModal();
 97    }
 98  });
 99}
100
101export function openHelpModal() {
102  const helpModal = document.getElementById('help-modal');
103  const helpModalClose = helpModal.querySelector('.modal-close');
104  helpModal.classList.remove('hidden');
105  helpModalClose.focus();
106}
107
108export function closeHelpModal() {
109  const helpModal = document.getElementById('help-modal');
110  helpModal.classList.add('hidden');
111}
112
113export function setupPressLock() {
114  // Minimal press-lock for non-vote buttons to smooth release
115  document.addEventListener('click', (e) => {
116    const btn = e.target.closest('button');
117    if (!btn) return;
118    if (btn.classList.contains('vote-btn')) return; // handled in vote()
119    btn.classList.add('press-lock');
120    setTimeout(() => btn.classList.remove('press-lock'), 180);
121  });
122}