feat: keyboard functionaliteit cmd/option up/down toegevoegd
This commit is contained in:
@@ -0,0 +1,147 @@
|
|||||||
|
# UI_KEYBOARD_V1_1_AND_WILDCARD_DESIGN.md
|
||||||
|
|
||||||
|
## Deel 1: Keyboard Navigation v1.1 (Mac-vriendelijk)
|
||||||
|
|
||||||
|
### Doel
|
||||||
|
Keyboard v1.1 bouwt voort op v1 en maakt navigatie sneller op Mac zonder de bestaande muisflow of backendcontracten te wijzigen.
|
||||||
|
|
||||||
|
### Shortcut scope (v1.1)
|
||||||
|
- `Tab`: wissel actief paneel (`left` <-> `right`)
|
||||||
|
- `ArrowUp` / `ArrowDown`: verplaats current row
|
||||||
|
- `Enter`: open directory op current row
|
||||||
|
- `Space`: toggle selectie op current row
|
||||||
|
- `Escape`: wis selectie in actief paneel
|
||||||
|
- `Cmd + ArrowUp`: ga naar eerste rij
|
||||||
|
- `Cmd + ArrowDown`: ga naar laatste rij
|
||||||
|
- `Alt + ArrowUp`: grotere stap omhoog
|
||||||
|
- `Alt + ArrowDown`: grotere stap omlaag
|
||||||
|
|
||||||
|
### State impact
|
||||||
|
Geen nieuw globaal model nodig; uitbreiding op bestaand model:
|
||||||
|
- per paneel blijft:
|
||||||
|
- `currentPath`
|
||||||
|
- `visibleItems`
|
||||||
|
- `selectedItems`
|
||||||
|
- `currentRowIndex`
|
||||||
|
- globaal blijft:
|
||||||
|
- `activePane`
|
||||||
|
|
||||||
|
Aanvullende constante voor stapgrootte:
|
||||||
|
- `PAGE_STEP` (voorstel: 10 rijen)
|
||||||
|
|
||||||
|
### Navigatie- en scrollgedrag
|
||||||
|
- `Cmd + ArrowUp`: `currentRowIndex = 0`
|
||||||
|
- `Cmd + ArrowDown`: `currentRowIndex = visibleItems.length - 1`
|
||||||
|
- `Alt + ArrowUp`: `currentRowIndex = max(0, currentRowIndex - PAGE_STEP)`
|
||||||
|
- `Alt + ArrowDown`: `currentRowIndex = min(last, currentRowIndex + PAGE_STEP)`
|
||||||
|
- na elke indexwijziging: `scrollIntoView({ block: "nearest" })`
|
||||||
|
- bij leeg paneel: alle row-shortcuts zijn no-op
|
||||||
|
|
||||||
|
### Focusregels
|
||||||
|
Shortcuts alleen actief als focus niet in interactieve controls zit:
|
||||||
|
- `input`, `textarea`, `select`, `button`, `checkbox`, `contenteditable`
|
||||||
|
|
||||||
|
Extra voor Mac-combinaties:
|
||||||
|
- alleen onderscheppen als de combinatie exact matcht met ondersteunde shortcuts
|
||||||
|
- geen brede override van browser/system shortcuts buiten scope
|
||||||
|
|
||||||
|
### Regressierisico
|
||||||
|
- Mac key-detectie kan verschillen (`metaKey`/`altKey` + `Arrow*` combinaties)
|
||||||
|
- Overcapturing van `Alt+Arrow` kan botsen met OS/browsergedrag op sommige layouts
|
||||||
|
- Onbedoelde interactie met bestaande klikselectie bij snelle keyboard/muis mix
|
||||||
|
|
||||||
|
Mitigatie:
|
||||||
|
- centrale keyboard dispatcher met expliciete guard
|
||||||
|
- alleen exacte combinaties afvangen
|
||||||
|
- bestaande click handlers ongewijzigd laten
|
||||||
|
|
||||||
|
### Teststrategie
|
||||||
|
Geautomatiseerd (beperkt):
|
||||||
|
- bestaande UI smoke tests behouden
|
||||||
|
- optioneel kleine statische regressiecheck op paneelstructuur ongewijzigd
|
||||||
|
|
||||||
|
Handmatig (primair):
|
||||||
|
- `Cmd+ArrowUp/Down` springt naar begin/eind van lijst
|
||||||
|
- `Alt+ArrowUp/Down` springt met grote stap en scrollt correct mee
|
||||||
|
- shortcuts werken alleen in actief paneel
|
||||||
|
- shortcuts doen niets in inputs/checkboxfocus
|
||||||
|
- bestaande v1 shortcuts blijven correct werken
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Deel 2: Wildcard selectie ontwerp
|
||||||
|
|
||||||
|
### Doel
|
||||||
|
Snelle bulkselectie op patroon in het actieve paneel, zonder backendaanpassing.
|
||||||
|
|
||||||
|
### Trigger shortcuts
|
||||||
|
- `Shift + +` (oftewel `Shift` + `=` op veel toetsenborden): selecteer items op patroon
|
||||||
|
- `Shift + -` : deselecteer items op patroon
|
||||||
|
|
||||||
|
### Scopekeuzes (v1)
|
||||||
|
- Werkt **alleen op actief paneel**
|
||||||
|
- Werkt **alleen op zichtbare items** (`visibleItems`), niet recursief
|
||||||
|
- Werkt op **files én directories** in v1 (consistent met zichtbare lijst)
|
||||||
|
- `..` parent-entry doet niet mee
|
||||||
|
|
||||||
|
### Patroonformaat
|
||||||
|
- Glob-achtig, minimaal:
|
||||||
|
- `*` = willekeurige reeks
|
||||||
|
- `?` = één karakter
|
||||||
|
- Voorbeelden:
|
||||||
|
- `*.mkv`
|
||||||
|
- `S??E??*`
|
||||||
|
- `Project*`
|
||||||
|
|
||||||
|
### Case sensitivity
|
||||||
|
- Voorstel v1: **case-insensitive matching**
|
||||||
|
- Reden: voorspelbaarder voor eindgebruikers en sluit aan op typische file-manager verwachtingen
|
||||||
|
|
||||||
|
### Gedrag met bestaande selectie
|
||||||
|
- `Shift + +`:
|
||||||
|
- additief: matchende items worden toegevoegd aan bestaande selectie
|
||||||
|
- niet-matchende selectie blijft behouden
|
||||||
|
- `Shift + -`:
|
||||||
|
- subtractief: alleen matchende items worden uit selectie verwijderd
|
||||||
|
|
||||||
|
### Minimale popup UX
|
||||||
|
Eenvoudige modal/prompt met:
|
||||||
|
- titel: `Select by pattern` of `Deselect by pattern`
|
||||||
|
- één inputveld: patroon
|
||||||
|
- knoppen: `Apply`, `Cancel`
|
||||||
|
- compacte feedbackregel na apply:
|
||||||
|
- `N items matched, M changed`
|
||||||
|
|
||||||
|
Paneelcontext:
|
||||||
|
- popup wordt gestart vanuit actief paneel en toont dat expliciet (`Active pane: left/right`)
|
||||||
|
|
||||||
|
### Wat expliciet nog niet in scope is
|
||||||
|
- Geen regex-modus
|
||||||
|
- Geen include/exclude in één dialoog
|
||||||
|
- Geen persistente pattern-history
|
||||||
|
- Geen backend batch-endpoints
|
||||||
|
- Geen recursieve mapmatching
|
||||||
|
- Geen geavanceerde filters op size/date/type
|
||||||
|
|
||||||
|
### Regressierisico
|
||||||
|
- Shortcut-conflict met keyboard-layouts (`+` op verschillende toetsen)
|
||||||
|
- Matchlogica kan onduidelijk zijn bij verborgen bestanden (afhankelijk van `show_hidden`)
|
||||||
|
- Onbedoelde selectie van directories als gebruiker file-only verwacht
|
||||||
|
|
||||||
|
Mitigatie:
|
||||||
|
- in popup korte hinttekst over scope (`visible items in active pane`)
|
||||||
|
- heldere result-feedback (`matched/changed`)
|
||||||
|
- parent-entry expliciet uitsluiten
|
||||||
|
|
||||||
|
### Teststrategie
|
||||||
|
Geautomatiseerd:
|
||||||
|
- basis smoke: UI laadt, paneelstructuur blijft intact
|
||||||
|
- (indien kleine JS-tests bestaan) unitniveau voor glob-matcher helper
|
||||||
|
|
||||||
|
Handmatig:
|
||||||
|
- `Shift + +` selecteert matchende zichtbare items in actief paneel
|
||||||
|
- `Shift + -` deselecteert matchende geselecteerde items
|
||||||
|
- inactief paneel blijft onaangeraakt
|
||||||
|
- behavior met gemixte selectie (file+dir) is consistent
|
||||||
|
- case-insensitive matching bevestigd
|
||||||
|
|
||||||
+260
-21
@@ -5,18 +5,23 @@ let state = {
|
|||||||
showHidden: false,
|
showHidden: false,
|
||||||
selectedItem: null,
|
selectedItem: null,
|
||||||
selectedItems: [],
|
selectedItems: [],
|
||||||
|
visibleItems: [],
|
||||||
|
currentRowIndex: -1,
|
||||||
},
|
},
|
||||||
right: {
|
right: {
|
||||||
currentPath: "storage1",
|
currentPath: "storage1",
|
||||||
showHidden: false,
|
showHidden: false,
|
||||||
selectedItem: null,
|
selectedItem: null,
|
||||||
selectedItems: [],
|
selectedItems: [],
|
||||||
|
visibleItems: [],
|
||||||
|
currentRowIndex: -1,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
activePane: "left",
|
activePane: "left",
|
||||||
selectedTaskId: null,
|
selectedTaskId: null,
|
||||||
lastTaskCount: 0,
|
lastTaskCount: 0,
|
||||||
};
|
};
|
||||||
|
const ROW_JUMP_STEP = 10;
|
||||||
|
|
||||||
function paneState(pane) {
|
function paneState(pane) {
|
||||||
return state.panes[pane];
|
return state.panes[pane];
|
||||||
@@ -122,6 +127,17 @@ function toggleSelection(pane, item) {
|
|||||||
updateActionButtons();
|
updateActionButtons();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function currentRowItem(pane) {
|
||||||
|
const model = paneState(pane);
|
||||||
|
if (!Array.isArray(model.visibleItems) || model.visibleItems.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (model.currentRowIndex < 0 || model.currentRowIndex >= model.visibleItems.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return model.visibleItems[model.currentRowIndex];
|
||||||
|
}
|
||||||
|
|
||||||
function updateActionButtons() {
|
function updateActionButtons() {
|
||||||
const selectedItems = activePaneState().selectedItems;
|
const selectedItems = activePaneState().selectedItems;
|
||||||
const count = selectedItems.length;
|
const count = selectedItems.length;
|
||||||
@@ -189,10 +205,15 @@ function formatModified(isoString) {
|
|||||||
function createBrowseItem(pane, entry, kind) {
|
function createBrowseItem(pane, entry, kind) {
|
||||||
const li = document.createElement("li");
|
const li = document.createElement("li");
|
||||||
li.className = "selectable";
|
li.className = "selectable";
|
||||||
|
li.dataset.path = entry.path;
|
||||||
const paths = selectedPaths(pane);
|
const paths = selectedPaths(pane);
|
||||||
|
const model = paneState(pane);
|
||||||
if (paths.includes(entry.path)) {
|
if (paths.includes(entry.path)) {
|
||||||
li.classList.add("is-selected");
|
li.classList.add("is-selected");
|
||||||
}
|
}
|
||||||
|
if (model.currentRowIndex >= 0 && model.visibleItems[model.currentRowIndex]?.path === entry.path) {
|
||||||
|
li.classList.add("is-current-row");
|
||||||
|
}
|
||||||
|
|
||||||
li.onclick = () => {
|
li.onclick = () => {
|
||||||
setActivePane(pane);
|
setActivePane(pane);
|
||||||
@@ -251,32 +272,56 @@ function createBrowseItem(pane, entry, kind) {
|
|||||||
return li;
|
return li;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadBrowsePane(pane) {
|
function scrollCurrentRowIntoView(pane) {
|
||||||
setError(`${pane}-browse-error`, "");
|
|
||||||
try {
|
|
||||||
const model = paneState(pane);
|
const model = paneState(pane);
|
||||||
const query = new URLSearchParams({
|
if (model.currentRowIndex < 0) {
|
||||||
path: model.currentPath,
|
return;
|
||||||
show_hidden: String(model.showHidden),
|
}
|
||||||
});
|
const list = document.getElementById(`${pane}-items`);
|
||||||
const data = await apiRequest("GET", `/api/browse?${query.toString()}`);
|
const row = list.querySelector(`li[data-row-index="${model.currentRowIndex}"]`);
|
||||||
model.currentPath = data.path;
|
if (row) {
|
||||||
document.getElementById(`${pane}-current-path`).textContent = data.path;
|
row.scrollIntoView({ block: "nearest" });
|
||||||
renderBreadcrumbs(pane, data.path);
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderPaneItems(pane) {
|
||||||
|
const model = paneState(pane);
|
||||||
const items = document.getElementById(`${pane}-items`);
|
const items = document.getElementById(`${pane}-items`);
|
||||||
items.innerHTML = "";
|
items.innerHTML = "";
|
||||||
|
|
||||||
const parent = currentParentPath(data.path);
|
if (!Array.isArray(model.visibleItems) || model.visibleItems.length === 0) {
|
||||||
if (parent) {
|
model.currentRowIndex = -1;
|
||||||
|
updateActionButtons();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (model.currentRowIndex < 0 || model.currentRowIndex >= model.visibleItems.length) {
|
||||||
|
model.currentRowIndex = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
model.visibleItems.forEach((entry, index) => {
|
||||||
|
if (entry.isParent) {
|
||||||
const up = document.createElement("li");
|
const up = document.createElement("li");
|
||||||
up.className = "selectable";
|
up.className = "selectable";
|
||||||
|
up.dataset.rowIndex = String(index);
|
||||||
|
up.dataset.path = entry.path;
|
||||||
|
if (index === model.currentRowIndex) {
|
||||||
|
up.classList.add("is-current-row");
|
||||||
|
}
|
||||||
|
up.onclick = () => {
|
||||||
|
setActivePane(pane);
|
||||||
|
model.currentRowIndex = index;
|
||||||
|
renderPaneItems(pane);
|
||||||
|
};
|
||||||
up.append(document.createElement("span"));
|
up.append(document.createElement("span"));
|
||||||
const upName = document.createElement("button");
|
const upName = document.createElement("button");
|
||||||
upName.type = "button";
|
upName.type = "button";
|
||||||
upName.className = "dir-link";
|
upName.className = "dir-link";
|
||||||
upName.textContent = "../";
|
upName.textContent = "../";
|
||||||
upName.onclick = () => navigateTo(pane, parent);
|
upName.onclick = (ev) => {
|
||||||
|
ev.stopPropagation();
|
||||||
|
setActivePane(pane);
|
||||||
|
navigateTo(pane, entry.path);
|
||||||
|
};
|
||||||
const upNameCell = document.createElement("span");
|
const upNameCell = document.createElement("span");
|
||||||
upNameCell.className = "entry-name entry-dir";
|
upNameCell.className = "entry-name entry-dir";
|
||||||
upNameCell.append(upName);
|
upNameCell.append(upName);
|
||||||
@@ -290,23 +335,87 @@ async function loadBrowsePane(pane) {
|
|||||||
upModified.textContent = "-";
|
upModified.textContent = "-";
|
||||||
up.append(upModified);
|
up.append(upModified);
|
||||||
items.append(up);
|
items.append(up);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const visiblePaths = new Set();
|
const row = createBrowseItem(pane, entry, entry.kind);
|
||||||
|
row.dataset.rowIndex = String(index);
|
||||||
|
if (index === model.currentRowIndex) {
|
||||||
|
row.classList.add("is-current-row");
|
||||||
|
}
|
||||||
|
row.onclick = () => {
|
||||||
|
setActivePane(pane);
|
||||||
|
model.currentRowIndex = index;
|
||||||
|
setSingleSelection(pane, { path: entry.path, name: entry.name, kind: entry.kind });
|
||||||
|
renderPaneItems(pane);
|
||||||
|
};
|
||||||
|
const checkbox = row.querySelector(".select-marker");
|
||||||
|
if (checkbox) {
|
||||||
|
checkbox.onclick = (ev) => {
|
||||||
|
ev.stopPropagation();
|
||||||
|
setActivePane(pane);
|
||||||
|
model.currentRowIndex = index;
|
||||||
|
toggleSelection(pane, { path: entry.path, name: entry.name, kind: entry.kind });
|
||||||
|
renderPaneItems(pane);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const dirLink = row.querySelector(".dir-link");
|
||||||
|
if (dirLink) {
|
||||||
|
dirLink.onclick = (ev) => {
|
||||||
|
ev.stopPropagation();
|
||||||
|
setActivePane(pane);
|
||||||
|
navigateTo(pane, entry.path);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const fileName = row.querySelector(".entry-file span");
|
||||||
|
if (fileName) {
|
||||||
|
fileName.onclick = (ev) => {
|
||||||
|
ev.stopPropagation();
|
||||||
|
setActivePane(pane);
|
||||||
|
model.currentRowIndex = index;
|
||||||
|
setSingleSelection(pane, { path: entry.path, name: entry.name, kind: entry.kind });
|
||||||
|
renderPaneItems(pane);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
items.append(row);
|
||||||
|
});
|
||||||
|
updateActionButtons();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadBrowsePane(pane) {
|
||||||
|
setError(`${pane}-browse-error`, "");
|
||||||
|
try {
|
||||||
|
const model = paneState(pane);
|
||||||
|
const query = new URLSearchParams({
|
||||||
|
path: model.currentPath,
|
||||||
|
show_hidden: String(model.showHidden),
|
||||||
|
});
|
||||||
|
const data = await apiRequest("GET", `/api/browse?${query.toString()}`);
|
||||||
|
model.currentPath = data.path;
|
||||||
|
document.getElementById(`${pane}-current-path`).textContent = data.path;
|
||||||
|
renderBreadcrumbs(pane, data.path);
|
||||||
|
|
||||||
|
const visibleItems = [];
|
||||||
|
const parent = currentParentPath(data.path);
|
||||||
|
if (parent) {
|
||||||
|
visibleItems.push({ path: parent, name: "..", kind: "directory", isParent: true });
|
||||||
|
}
|
||||||
for (const entry of data.directories) {
|
for (const entry of data.directories) {
|
||||||
visiblePaths.add(entry.path);
|
visibleItems.push({ ...entry, kind: "directory" });
|
||||||
items.append(createBrowseItem(pane, entry, "directory"));
|
|
||||||
}
|
}
|
||||||
for (const entry of data.files) {
|
for (const entry of data.files) {
|
||||||
visiblePaths.add(entry.path);
|
visibleItems.push({ ...entry, kind: "file" });
|
||||||
items.append(createBrowseItem(pane, entry, "file"));
|
|
||||||
}
|
}
|
||||||
|
model.visibleItems = visibleItems;
|
||||||
|
|
||||||
|
const visiblePaths = new Set(visibleItems.filter((item) => !item.isParent).map((item) => item.path));
|
||||||
model.selectedItems = model.selectedItems.filter((item) => visiblePaths.has(item.path));
|
model.selectedItems = model.selectedItems.filter((item) => visiblePaths.has(item.path));
|
||||||
if (model.selectedItem && !visiblePaths.has(model.selectedItem.path)) {
|
if (model.selectedItem && !visiblePaths.has(model.selectedItem.path)) {
|
||||||
model.selectedItem = model.selectedItems.length > 0 ? model.selectedItems[model.selectedItems.length - 1] : null;
|
model.selectedItem = model.selectedItems.length > 0 ? model.selectedItems[model.selectedItems.length - 1] : null;
|
||||||
}
|
}
|
||||||
updateActionButtons();
|
|
||||||
|
renderPaneItems(pane);
|
||||||
|
scrollCurrentRowIntoView(pane);
|
||||||
setStatus(`Loaded ${pane}: ${data.path}`);
|
setStatus(`Loaded ${pane}: ${data.path}`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(`${pane}-browse-error`, `Browse: ${err.message}`);
|
setError(`${pane}-browse-error`, `Browse: ${err.message}`);
|
||||||
@@ -314,7 +423,9 @@ async function loadBrowsePane(pane) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function navigateTo(pane, path) {
|
function navigateTo(pane, path) {
|
||||||
paneState(pane).currentPath = path;
|
const model = paneState(pane);
|
||||||
|
model.currentPath = path;
|
||||||
|
model.currentRowIndex = 0;
|
||||||
setSelectedItem(pane, null);
|
setSelectedItem(pane, null);
|
||||||
loadBrowsePane(pane);
|
loadBrowsePane(pane);
|
||||||
}
|
}
|
||||||
@@ -488,6 +599,133 @@ async function addBookmark() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function shouldHandleShortcut(target) {
|
||||||
|
if (!target || !(target instanceof Element)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (target.closest("[contenteditable='true']")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const control = target.closest("input, textarea, select, button");
|
||||||
|
if (!control) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (control.tagName === "INPUT" && control.type === "checkbox") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function moveCurrentRow(delta) {
|
||||||
|
const pane = state.activePane;
|
||||||
|
const model = paneState(pane);
|
||||||
|
if (!Array.isArray(model.visibleItems) || model.visibleItems.length === 0) {
|
||||||
|
model.currentRowIndex = -1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (model.currentRowIndex < 0) {
|
||||||
|
model.currentRowIndex = 0;
|
||||||
|
} else {
|
||||||
|
const maxIndex = model.visibleItems.length - 1;
|
||||||
|
model.currentRowIndex = Math.max(0, Math.min(maxIndex, model.currentRowIndex + delta));
|
||||||
|
}
|
||||||
|
renderPaneItems(pane);
|
||||||
|
scrollCurrentRowIntoView(pane);
|
||||||
|
}
|
||||||
|
|
||||||
|
function jumpCurrentRow(edge) {
|
||||||
|
const pane = state.activePane;
|
||||||
|
const model = paneState(pane);
|
||||||
|
if (!Array.isArray(model.visibleItems) || model.visibleItems.length === 0) {
|
||||||
|
model.currentRowIndex = -1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
model.currentRowIndex = edge === "start" ? 0 : model.visibleItems.length - 1;
|
||||||
|
renderPaneItems(pane);
|
||||||
|
scrollCurrentRowIntoView(pane);
|
||||||
|
}
|
||||||
|
|
||||||
|
function openCurrentDirectory() {
|
||||||
|
const pane = state.activePane;
|
||||||
|
const item = currentRowItem(pane);
|
||||||
|
if (!item || item.kind !== "directory") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
navigateTo(pane, item.path);
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleCurrentSelection() {
|
||||||
|
const pane = state.activePane;
|
||||||
|
const item = currentRowItem(pane);
|
||||||
|
if (!item || item.isParent) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
toggleSelection(pane, { path: item.path, name: item.name, kind: item.kind });
|
||||||
|
renderPaneItems(pane);
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearSelectionForActivePane() {
|
||||||
|
const pane = state.activePane;
|
||||||
|
setSelectedItem(pane, null);
|
||||||
|
renderPaneItems(pane);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleKeyboardShortcuts(event) {
|
||||||
|
if (!shouldHandleShortcut(event.target)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.metaKey && event.key === "ArrowUp") {
|
||||||
|
event.preventDefault();
|
||||||
|
jumpCurrentRow("start");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (event.metaKey && event.key === "ArrowDown") {
|
||||||
|
event.preventDefault();
|
||||||
|
jumpCurrentRow("end");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (event.altKey && event.key === "ArrowUp") {
|
||||||
|
event.preventDefault();
|
||||||
|
moveCurrentRow(-ROW_JUMP_STEP);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (event.altKey && event.key === "ArrowDown") {
|
||||||
|
event.preventDefault();
|
||||||
|
moveCurrentRow(ROW_JUMP_STEP);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (event.key === "Tab") {
|
||||||
|
event.preventDefault();
|
||||||
|
setActivePane(otherPane(state.activePane));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (event.key === "ArrowUp") {
|
||||||
|
event.preventDefault();
|
||||||
|
moveCurrentRow(-1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (event.key === "ArrowDown") {
|
||||||
|
event.preventDefault();
|
||||||
|
moveCurrentRow(1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (event.key === "Enter") {
|
||||||
|
event.preventDefault();
|
||||||
|
openCurrentDirectory();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (event.key === " " || event.code === "Space") {
|
||||||
|
event.preventDefault();
|
||||||
|
toggleCurrentSelection();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (event.key === "Escape") {
|
||||||
|
event.preventDefault();
|
||||||
|
clearSelectionForActivePane();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function setupPaneEvents(pane) {
|
function setupPaneEvents(pane) {
|
||||||
document.getElementById(`${pane}-pane`).onclick = () => setActivePane(pane);
|
document.getElementById(`${pane}-pane`).onclick = () => setActivePane(pane);
|
||||||
document.getElementById(`${pane}-hidden-toggle`).onchange = (ev) => {
|
document.getElementById(`${pane}-hidden-toggle`).onchange = (ev) => {
|
||||||
@@ -500,6 +738,7 @@ function setupPaneEvents(pane) {
|
|||||||
function setupEvents() {
|
function setupEvents() {
|
||||||
setupPaneEvents("left");
|
setupPaneEvents("left");
|
||||||
setupPaneEvents("right");
|
setupPaneEvents("right");
|
||||||
|
document.addEventListener("keydown", handleKeyboardShortcuts);
|
||||||
document.getElementById("rename-btn").onclick = renameSelected;
|
document.getElementById("rename-btn").onclick = renameSelected;
|
||||||
document.getElementById("delete-btn").onclick = deleteSelected;
|
document.getElementById("delete-btn").onclick = deleteSelected;
|
||||||
document.getElementById("copy-btn").onclick = startCopySelected;
|
document.getElementById("copy-btn").onclick = startCopySelected;
|
||||||
|
|||||||
@@ -215,6 +215,18 @@ button:disabled {
|
|||||||
background: #e9f0fd;
|
background: #e9f0fd;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.list li.is-current-row {
|
||||||
|
box-shadow: inset 0 0 0 1px #9cb7e8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list li.is-current-row:not(.is-selected) {
|
||||||
|
background: #f4f8ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list li.is-current-row.is-selected {
|
||||||
|
background: #e3edff;
|
||||||
|
}
|
||||||
|
|
||||||
.select-marker {
|
.select-marker {
|
||||||
appearance: none;
|
appearance: none;
|
||||||
width: 10px;
|
width: 10px;
|
||||||
|
|||||||
Reference in New Issue
Block a user