화살표, Home, End (+Shift) 키보드 입력 기능 추가

This commit is contained in:
2026-05-22 11:55:44 +09:00
parent 4000339477
commit ba0fe2f79f
5 changed files with 132 additions and 3 deletions
+1
View File
@@ -200,6 +200,7 @@ export function createCard(file, idx) {
export function refreshCardSelection() {
Array.from(grid.children).forEach((card, i) => {
card.classList.toggle('selected', state.selectedIdxs.has(i));
card.classList.toggle('keyboard-target', selection.isKeyboardTarget(i));
});
}
+111 -1
View File
@@ -1,4 +1,4 @@
let _rubberBand, _grid;
let _rubberBand, _grid, _gridContainer;
let _state, _cb;
let rbActive = false;
@@ -8,15 +8,24 @@ let _rafPending = false;
let _lastMoveEvent = null;
let lastSelectedIdx = null;
let keyboardTargetIdx = null;
let keyboardExtendBaseSelection = null;
let _rbStartSelection = new Set(); // 러버밴드 시작 시점의 선택 스냅샷
export function resetAnchor() {
lastSelectedIdx = null;
keyboardTargetIdx = null;
keyboardExtendBaseSelection = null;
}
export function isKeyboardTarget(idx) {
return keyboardTargetIdx === idx;
}
export function init({ gridContainer, rubberBand, grid, state, callbacks }) {
_rubberBand = rubberBand;
_grid = grid;
_gridContainer = gridContainer;
_state = state;
_cb = callbacks;
@@ -51,10 +60,39 @@ export function onCardClick(e) {
}
}
keyboardTargetIdx = idx;
keyboardExtendBaseSelection = null;
_cb.refreshCardSelection();
_cb.updateSelCount();
}
export function handleKeyboardNavigation(code, { extend = false } = {}) {
if (!_state?.files.length) return false;
const currentIdx = _getCurrentKeyboardIndex(code);
const nextIdx = _getNextKeyboardIndex(currentIdx, code);
if (nextIdx === null) return false;
if (extend) {
if (lastSelectedIdx === null) lastSelectedIdx = currentIdx ?? nextIdx;
if (!keyboardExtendBaseSelection) {
keyboardExtendBaseSelection = new Set(_state.selectedIdxs);
}
_selectKeyboardRange(lastSelectedIdx, nextIdx, keyboardExtendBaseSelection);
} else {
_state.selectedIdxs.clear();
_state.selectedIdxs.add(nextIdx);
lastSelectedIdx = nextIdx;
keyboardExtendBaseSelection = null;
}
keyboardTargetIdx = nextIdx;
_cb.refreshCardSelection();
_cb.updateSelCount();
_scrollKeyboardTargetIntoView(code, nextIdx);
return true;
}
function _toggle(idx) {
if (_state.selectedIdxs.has(idx)) {
_state.selectedIdxs.delete(idx);
@@ -63,6 +101,7 @@ function _toggle(idx) {
_state.selectedIdxs.add(idx);
lastSelectedIdx = idx;
}
keyboardExtendBaseSelection = null;
}
function _selectRange(idx) {
@@ -70,6 +109,7 @@ function _selectRange(idx) {
_state.selectedIdxs.clear();
_state.selectedIdxs.add(idx);
lastSelectedIdx = idx;
keyboardExtendBaseSelection = null;
return;
}
const start = Math.min(lastSelectedIdx, idx);
@@ -77,6 +117,74 @@ function _selectRange(idx) {
_state.selectedIdxs.clear();
for (let i = start; i <= end; i++) _state.selectedIdxs.add(i);
// anchor(lastSelectedIdx)는 Shift 클릭으로 변경하지 않음
keyboardExtendBaseSelection = null;
}
function _selectKeyboardRange(anchorIdx, targetIdx, baseSelection) {
const start = Math.min(anchorIdx, targetIdx);
const end = Math.max(anchorIdx, targetIdx);
_state.selectedIdxs.clear();
baseSelection.forEach(idx => {
if (_isValidIndex(idx)) _state.selectedIdxs.add(idx);
});
for (let i = start; i <= end; i++) _state.selectedIdxs.add(i);
}
function _getCurrentKeyboardIndex(code) {
if (_isValidIndex(keyboardTargetIdx)) return keyboardTargetIdx;
if (_state.selectedIdxs.size) return Math.min(..._state.selectedIdxs);
return null;
}
function _getNextKeyboardIndex(currentIdx, code) {
const lastIdx = _state.files.length - 1;
if (currentIdx === null) {
return (code === 'ArrowLeft' || code === 'ArrowUp' || code === 'End')
? lastIdx
: 0;
}
if (code === 'Home') return 0;
if (code === 'End') return lastIdx;
if (code === 'ArrowLeft') return Math.max(0, currentIdx - 1);
if (code === 'ArrowRight') return Math.min(lastIdx, currentIdx + 1);
const columnCount = _getColumnCount();
if (code === 'ArrowUp') return Math.max(0, currentIdx - columnCount);
if (code === 'ArrowDown') return Math.min(lastIdx, currentIdx + columnCount);
return null;
}
function _getColumnCount() {
const cards = Array.from(_grid.children).filter(card => card.classList.contains('card'));
if (!cards.length) return 1;
const firstTop = cards[0].getBoundingClientRect().top;
const firstRowCount = cards.findIndex(card =>
Math.abs(card.getBoundingClientRect().top - firstTop) > 2
);
return firstRowCount === -1 ? cards.length : Math.max(1, firstRowCount);
}
function _scrollCardIntoView(idx) {
const card = _grid.querySelector(`.card[data-idx="${idx}"]`);
card?.scrollIntoView({ block: 'nearest', inline: 'nearest' });
}
function _scrollKeyboardTargetIntoView(code, idx) {
if (code === 'Home') {
_gridContainer.scrollTop = 0;
return;
}
if (code === 'End') {
_gridContainer.scrollTop = _gridContainer.scrollHeight - _gridContainer.clientHeight;
return;
}
_scrollCardIntoView(idx);
}
function _isValidIndex(idx) {
return Number.isInteger(idx) && idx >= 0 && idx < _state.files.length;
}
function _onStart(e) {
@@ -102,6 +210,8 @@ function _onStart(e) {
_state.selectedIdxs.clear();
_rbStartSelection.clear();
lastSelectedIdx = null;
keyboardTargetIdx = null;
keyboardExtendBaseSelection = null;
_cb.refreshCardSelection();
}
}
+8
View File
@@ -1,4 +1,5 @@
import { log } from './logger.js';
import * as selection from './handlers/selection.js';
export function initKeyboardShortcuts({
btnOpen,
@@ -39,10 +40,17 @@ export function initKeyboardShortcuts({
btnPreviewToggle.click();
} else if (e.code === 'Backspace' || e.code === 'Delete') {
deleteSelected();
} else if (isNavigationKey(e.code) && !e.metaKey && !e.ctrlKey && !e.altKey) {
e.preventDefault();
selection.handleKeyboardNavigation(e.code, { extend: e.shiftKey });
}
});
}
function isNavigationKey(code) {
return ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Home', 'End'].includes(code);
}
function isEditableTarget(target) {
return ['INPUT', 'TEXTAREA', 'SELECT'].includes(target.tagName) || target.isContentEditable;
}
+4
View File
@@ -338,6 +338,10 @@ main {
background: var(--sel-bg);
box-shadow: 0 0 0 1px var(--sel-border), 0 8px 24px rgba(124,106,247,.2);
}
.card.keyboard-target {
outline: 2px solid rgba(255, 255, 255, .72);
outline-offset: 2px;
}
.card.dragging,
.card.multi-dragging { opacity: .3; transform: scale(.96); cursor: grabbing; }
.card.drag-over { border-color: var(--accent) !important; background: var(--drag-over); }