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}