feat: keyboard functionaliteit shift + / Shift - toegevoegd
This commit is contained in:
Binary file not shown.
@@ -37,6 +37,8 @@ class UiSmokeGoldenTest(unittest.TestCase):
|
|||||||
self.assertIn('id="move-btn"', body)
|
self.assertIn('id="move-btn"', body)
|
||||||
self.assertIn('id="left-breadcrumbs"', body)
|
self.assertIn('id="left-breadcrumbs"', body)
|
||||||
self.assertIn('id="right-breadcrumbs"', body)
|
self.assertIn('id="right-breadcrumbs"', body)
|
||||||
|
self.assertIn('id="wildcard-popup"', body)
|
||||||
|
self.assertIn('id="wildcard-pattern-input"', body)
|
||||||
self.assertNotIn('id="bookmarks-panel"', body)
|
self.assertNotIn('id="bookmarks-panel"', body)
|
||||||
self.assertNotIn('id="tasks-panel"', body)
|
self.assertNotIn('id="tasks-panel"', body)
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ let state = {
|
|||||||
lastTaskCount: 0,
|
lastTaskCount: 0,
|
||||||
};
|
};
|
||||||
const ROW_JUMP_STEP = 10;
|
const ROW_JUMP_STEP = 10;
|
||||||
|
let wildcardDialogMode = "select";
|
||||||
|
|
||||||
function paneState(pane) {
|
function paneState(pane) {
|
||||||
return state.panes[pane];
|
return state.panes[pane];
|
||||||
@@ -616,6 +617,104 @@ function shouldHandleShortcut(target) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function wildcardPopupElements() {
|
||||||
|
return {
|
||||||
|
overlay: document.getElementById("wildcard-popup"),
|
||||||
|
title: document.getElementById("wildcard-popup-title"),
|
||||||
|
meta: document.getElementById("wildcard-popup-meta"),
|
||||||
|
input: document.getElementById("wildcard-pattern-input"),
|
||||||
|
error: document.getElementById("wildcard-popup-error"),
|
||||||
|
applyButton: document.getElementById("wildcard-apply-btn"),
|
||||||
|
cancelButton: document.getElementById("wildcard-cancel-btn"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function isWildcardPopupOpen() {
|
||||||
|
return !wildcardPopupElements().overlay.classList.contains("hidden");
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeRegExp(text) {
|
||||||
|
return text.replace(/[|\\{}()[\]^$+?.]/g, "\\$&");
|
||||||
|
}
|
||||||
|
|
||||||
|
function globToRegExp(pattern) {
|
||||||
|
const escaped = pattern
|
||||||
|
.split("*")
|
||||||
|
.map((part) => escapeRegExp(part))
|
||||||
|
.join(".*");
|
||||||
|
return new RegExp(`^${escaped}$`, "i");
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyWildcardSelection(mode, pattern) {
|
||||||
|
const pane = state.activePane;
|
||||||
|
const model = paneState(pane);
|
||||||
|
const matcher = globToRegExp(pattern);
|
||||||
|
const candidates = model.visibleItems.filter((entry) => !entry.isParent);
|
||||||
|
const matches = candidates.filter((entry) => matcher.test(entry.name));
|
||||||
|
const matchPaths = new Set(matches.map((entry) => entry.path));
|
||||||
|
|
||||||
|
let changed = 0;
|
||||||
|
if (mode === "select") {
|
||||||
|
const existing = new Set(model.selectedItems.map((item) => item.path));
|
||||||
|
for (const entry of matches) {
|
||||||
|
if (existing.has(entry.path)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
model.selectedItems.push({ path: entry.path, name: entry.name, kind: entry.kind });
|
||||||
|
changed += 1;
|
||||||
|
}
|
||||||
|
if (matches.length > 0) {
|
||||||
|
const last = matches[matches.length - 1];
|
||||||
|
model.selectedItem = { path: last.path, name: last.name, kind: last.kind };
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const before = model.selectedItems.length;
|
||||||
|
model.selectedItems = model.selectedItems.filter((item) => !matchPaths.has(item.path));
|
||||||
|
changed = before - model.selectedItems.length;
|
||||||
|
if (!model.selectedItem || matchPaths.has(model.selectedItem.path)) {
|
||||||
|
model.selectedItem = model.selectedItems.length > 0 ? model.selectedItems[model.selectedItems.length - 1] : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
renderPaneItems(pane);
|
||||||
|
setStatus(`Wildcard ${mode}: ${matches.length} matched, ${changed} changed`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeWildcardPopup() {
|
||||||
|
const elements = wildcardPopupElements();
|
||||||
|
elements.overlay.classList.add("hidden");
|
||||||
|
elements.error.textContent = "";
|
||||||
|
elements.input.value = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function submitWildcardPopup() {
|
||||||
|
const elements = wildcardPopupElements();
|
||||||
|
const pattern = elements.input.value.trim();
|
||||||
|
if (!pattern) {
|
||||||
|
elements.error.textContent = "Pattern is required";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
applyWildcardSelection(wildcardDialogMode, pattern);
|
||||||
|
closeWildcardPopup();
|
||||||
|
} catch (err) {
|
||||||
|
elements.error.textContent = `Wildcard: ${err.message}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function openWildcardPopup(mode) {
|
||||||
|
wildcardDialogMode = mode;
|
||||||
|
const pane = state.activePane;
|
||||||
|
const elements = wildcardPopupElements();
|
||||||
|
elements.title.textContent = mode === "select" ? "Wildcard Select" : "Wildcard Deselect";
|
||||||
|
elements.meta.textContent = `Active pane: ${pane} (visible items only, case-insensitive)`;
|
||||||
|
elements.applyButton.textContent = mode === "select" ? "Select" : "Deselect";
|
||||||
|
elements.error.textContent = "";
|
||||||
|
elements.input.value = "";
|
||||||
|
elements.overlay.classList.remove("hidden");
|
||||||
|
elements.input.focus();
|
||||||
|
}
|
||||||
|
|
||||||
function moveCurrentRow(delta) {
|
function moveCurrentRow(delta) {
|
||||||
const pane = state.activePane;
|
const pane = state.activePane;
|
||||||
const model = paneState(pane);
|
const model = paneState(pane);
|
||||||
@@ -671,10 +770,23 @@ function clearSelectionForActivePane() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleKeyboardShortcuts(event) {
|
function handleKeyboardShortcuts(event) {
|
||||||
|
if (isWildcardPopupOpen()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!shouldHandleShortcut(event.target)) {
|
if (!shouldHandleShortcut(event.target)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (event.shiftKey && event.key === "+") {
|
||||||
|
event.preventDefault();
|
||||||
|
openWildcardPopup("select");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (event.shiftKey && event.key === "_") {
|
||||||
|
event.preventDefault();
|
||||||
|
openWildcardPopup("deselect");
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (event.metaKey && event.key === "ArrowUp") {
|
if (event.metaKey && event.key === "ArrowUp") {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
jumpCurrentRow("start");
|
jumpCurrentRow("start");
|
||||||
@@ -745,6 +857,26 @@ function setupEvents() {
|
|||||||
document.getElementById("move-btn").onclick = startMoveSelected;
|
document.getElementById("move-btn").onclick = startMoveSelected;
|
||||||
document.getElementById("mkdir-btn").onclick = createFolderForActivePane;
|
document.getElementById("mkdir-btn").onclick = createFolderForActivePane;
|
||||||
document.getElementById("add-bookmark-btn").onclick = addBookmark;
|
document.getElementById("add-bookmark-btn").onclick = addBookmark;
|
||||||
|
|
||||||
|
const wildcard = wildcardPopupElements();
|
||||||
|
wildcard.cancelButton.onclick = closeWildcardPopup;
|
||||||
|
wildcard.applyButton.onclick = submitWildcardPopup;
|
||||||
|
wildcard.input.onkeydown = (event) => {
|
||||||
|
if (event.key === "Enter") {
|
||||||
|
event.preventDefault();
|
||||||
|
submitWildcardPopup();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (event.key === "Escape") {
|
||||||
|
event.preventDefault();
|
||||||
|
closeWildcardPopup();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
wildcard.overlay.onclick = (event) => {
|
||||||
|
if (event.target === wildcard.overlay) {
|
||||||
|
closeWildcardPopup();
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function init() {
|
async function init() {
|
||||||
|
|||||||
@@ -73,6 +73,20 @@
|
|||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="wildcard-popup" class="popup-overlay hidden" role="dialog" aria-modal="true" aria-labelledby="wildcard-popup-title">
|
||||||
|
<div class="popup-card">
|
||||||
|
<h3 id="wildcard-popup-title">Wildcard Select</h3>
|
||||||
|
<div id="wildcard-popup-meta" class="popup-meta"></div>
|
||||||
|
<label for="wildcard-pattern-input" class="popup-label">Pattern</label>
|
||||||
|
<input id="wildcard-pattern-input" type="text" autocomplete="off" placeholder="*.mkv">
|
||||||
|
<div id="wildcard-popup-error" class="error"></div>
|
||||||
|
<div class="popup-actions">
|
||||||
|
<button id="wildcard-apply-btn" type="button">Apply</button>
|
||||||
|
<button id="wildcard-cancel-btn" type="button">Cancel</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script src="/ui/app.js"></script>
|
<script src="/ui/app.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -298,6 +298,52 @@ button:disabled {
|
|||||||
padding: 4px 10px 2px 10px;
|
padding: 4px 10px 2px 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.popup-overlay {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(20, 32, 50, 0.25);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-overlay.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-card {
|
||||||
|
width: min(420px, calc(100vw - 24px));
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 10px;
|
||||||
|
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.16);
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-meta {
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 12px;
|
||||||
|
margin: 4px 0 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-label {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
#wildcard-pattern-input {
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 4px;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 1200px) {
|
@media (max-width: 1200px) {
|
||||||
.workspace {
|
.workspace {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
|
|||||||
Reference in New Issue
Block a user