feat: keyboard functionaliteit cmd/option up/down toegevoegd
This commit is contained in:
+263
-24
@@ -5,18 +5,23 @@ let state = {
|
||||
showHidden: false,
|
||||
selectedItem: null,
|
||||
selectedItems: [],
|
||||
visibleItems: [],
|
||||
currentRowIndex: -1,
|
||||
},
|
||||
right: {
|
||||
currentPath: "storage1",
|
||||
showHidden: false,
|
||||
selectedItem: null,
|
||||
selectedItems: [],
|
||||
visibleItems: [],
|
||||
currentRowIndex: -1,
|
||||
},
|
||||
},
|
||||
activePane: "left",
|
||||
selectedTaskId: null,
|
||||
lastTaskCount: 0,
|
||||
};
|
||||
const ROW_JUMP_STEP = 10;
|
||||
|
||||
function paneState(pane) {
|
||||
return state.panes[pane];
|
||||
@@ -122,6 +127,17 @@ function toggleSelection(pane, item) {
|
||||
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() {
|
||||
const selectedItems = activePaneState().selectedItems;
|
||||
const count = selectedItems.length;
|
||||
@@ -189,10 +205,15 @@ function formatModified(isoString) {
|
||||
function createBrowseItem(pane, entry, kind) {
|
||||
const li = document.createElement("li");
|
||||
li.className = "selectable";
|
||||
li.dataset.path = entry.path;
|
||||
const paths = selectedPaths(pane);
|
||||
const model = paneState(pane);
|
||||
if (paths.includes(entry.path)) {
|
||||
li.classList.add("is-selected");
|
||||
}
|
||||
if (model.currentRowIndex >= 0 && model.visibleItems[model.currentRowIndex]?.path === entry.path) {
|
||||
li.classList.add("is-current-row");
|
||||
}
|
||||
|
||||
li.onclick = () => {
|
||||
setActivePane(pane);
|
||||
@@ -251,32 +272,56 @@ function createBrowseItem(pane, entry, kind) {
|
||||
return li;
|
||||
}
|
||||
|
||||
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);
|
||||
function scrollCurrentRowIntoView(pane) {
|
||||
const model = paneState(pane);
|
||||
if (model.currentRowIndex < 0) {
|
||||
return;
|
||||
}
|
||||
const list = document.getElementById(`${pane}-items`);
|
||||
const row = list.querySelector(`li[data-row-index="${model.currentRowIndex}"]`);
|
||||
if (row) {
|
||||
row.scrollIntoView({ block: "nearest" });
|
||||
}
|
||||
}
|
||||
|
||||
const items = document.getElementById(`${pane}-items`);
|
||||
items.innerHTML = "";
|
||||
function renderPaneItems(pane) {
|
||||
const model = paneState(pane);
|
||||
const items = document.getElementById(`${pane}-items`);
|
||||
items.innerHTML = "";
|
||||
|
||||
const parent = currentParentPath(data.path);
|
||||
if (parent) {
|
||||
if (!Array.isArray(model.visibleItems) || model.visibleItems.length === 0) {
|
||||
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");
|
||||
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"));
|
||||
const upName = document.createElement("button");
|
||||
upName.type = "button";
|
||||
upName.className = "dir-link";
|
||||
upName.textContent = "../";
|
||||
upName.onclick = () => navigateTo(pane, parent);
|
||||
upName.onclick = (ev) => {
|
||||
ev.stopPropagation();
|
||||
setActivePane(pane);
|
||||
navigateTo(pane, entry.path);
|
||||
};
|
||||
const upNameCell = document.createElement("span");
|
||||
upNameCell.className = "entry-name entry-dir";
|
||||
upNameCell.append(upName);
|
||||
@@ -290,23 +335,87 @@ async function loadBrowsePane(pane) {
|
||||
upModified.textContent = "-";
|
||||
up.append(upModified);
|
||||
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) {
|
||||
visiblePaths.add(entry.path);
|
||||
items.append(createBrowseItem(pane, entry, "directory"));
|
||||
visibleItems.push({ ...entry, kind: "directory" });
|
||||
}
|
||||
for (const entry of data.files) {
|
||||
visiblePaths.add(entry.path);
|
||||
items.append(createBrowseItem(pane, entry, "file"));
|
||||
visibleItems.push({ ...entry, kind: "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));
|
||||
if (model.selectedItem && !visiblePaths.has(model.selectedItem.path)) {
|
||||
model.selectedItem = model.selectedItems.length > 0 ? model.selectedItems[model.selectedItems.length - 1] : null;
|
||||
}
|
||||
updateActionButtons();
|
||||
|
||||
renderPaneItems(pane);
|
||||
scrollCurrentRowIntoView(pane);
|
||||
setStatus(`Loaded ${pane}: ${data.path}`);
|
||||
} catch (err) {
|
||||
setError(`${pane}-browse-error`, `Browse: ${err.message}`);
|
||||
@@ -314,7 +423,9 @@ async function loadBrowsePane(pane) {
|
||||
}
|
||||
|
||||
function navigateTo(pane, path) {
|
||||
paneState(pane).currentPath = path;
|
||||
const model = paneState(pane);
|
||||
model.currentPath = path;
|
||||
model.currentRowIndex = 0;
|
||||
setSelectedItem(pane, null);
|
||||
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) {
|
||||
document.getElementById(`${pane}-pane`).onclick = () => setActivePane(pane);
|
||||
document.getElementById(`${pane}-hidden-toggle`).onchange = (ev) => {
|
||||
@@ -500,6 +738,7 @@ function setupPaneEvents(pane) {
|
||||
function setupEvents() {
|
||||
setupPaneEvents("left");
|
||||
setupPaneEvents("right");
|
||||
document.addEventListener("keydown", handleKeyboardShortcuts);
|
||||
document.getElementById("rename-btn").onclick = renameSelected;
|
||||
document.getElementById("delete-btn").onclick = deleteSelected;
|
||||
document.getElementById("copy-btn").onclick = startCopySelected;
|
||||
|
||||
@@ -215,6 +215,18 @@ button:disabled {
|
||||
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 {
|
||||
appearance: none;
|
||||
width: 10px;
|
||||
|
||||
Reference in New Issue
Block a user