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