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}