overhaul selection and entry gathering

This commit is contained in:
joshuaboud 2022-06-13 13:38:34 -03:00
parent c4ef085c1f
commit 2508e23541
No known key found for this signature in database
GPG Key ID: 17EFB59E2A8BF50E
3 changed files with 286 additions and 246 deletions

View File

@ -1,20 +1,15 @@
<template>
<template v-if="settings.directoryView?.view === 'list'">
<tr
v-show="show || showEntries"
@dblclick.stop="doubleClickCallback"
@click.prevent.stop="$emit('toggleSelected', { ctrlKey: $event.ctrlKey, shiftKey: $event.shiftKey })"
:class="['hover:!bg-red-600/10 select-none']"
>
<td :class="['!pl-1', ...selectedClasses]">
<tr v-show="show || showEntries" @dblclick="doubleClickCallback"
@click.prevent="$emit('toggleSelected', entry, $event)"
:class="['hover:!bg-red-600/10 select-none dir-entry', entry.selected ? 'dir-entry-selected' : '']"
ref="selectIntersectElement">
<td class="!pl-1" :class="{ '!border-t-0': suppressBorders.top, '!border-b-0': suppressBorders.bottom }">
<div :class="[entry.cut ? 'line-through' : '', 'flex items-center gap-1']">
<div :style="{ width: `${24 * level}px` }"></div>
<div class="relative w-6">
<component :is="icon" class="size-icon icon-default" />
<LinkIcon
v-if="entry.type === 'l'"
class="w-2 h-2 absolute right-0 bottom-0 text-default"
/>
<component :is="icon" class="size-icon icon-default" :class="{ 'text-gray-500/50': entry.cut }" />
<LinkIcon v-if="entry.type === 'l'" class="w-2 h-2 absolute right-0 bottom-0 text-default" />
</div>
<button v-if="directoryLike" @click.stop="toggleShowEntries">
<ChevronDownIcon v-if="!showEntries" class="size-icon icon-default" />
@ -24,83 +19,68 @@
<div v-if="entry.type === 'l'" class="inline-flex gap-1 items-center">
<div class="inline relative">
<ArrowNarrowRightIcon class="text-default size-icon-sm inline" />
<XIcon
v-if="entry.target?.broken"
class="icon-danger size-icon-sm absolute inset-x-0 bottom-0"
/>
<XIcon v-if="entry.target?.broken"
class="icon-danger size-icon-sm absolute inset-x-0 bottom-0" />
</div>
<div v-html="escapeStringHTML(entry.target?.rawPath ?? '')" :title="entry.target.rawPath"></div>
</div>
</div>
</td>
<td
v-if="settings?.directoryView?.cols?.mode"
:class="['font-mono', ...(selectedClasses)]"
>{{ entry.modeStr }}</td>
<td v-if="settings?.directoryView?.cols?.owner" :class="selectedClasses">{{ entry.owner }}</td>
<td v-if="settings?.directoryView?.cols?.group" :class="selectedClasses">{{ entry.group }}</td>
<td
v-if="settings?.directoryView?.cols?.size"
:class="['font-mono text-right', ...(selectedClasses)]"
>{{ entry.sizeHuman }}</td>
<td
v-if="settings?.directoryView?.cols?.ctime"
:class="selectedClasses"
>{{ entry.ctime?.toLocaleString() ?? '-' }}</td>
<td
v-if="settings?.directoryView?.cols?.mtime"
:class="selectedClasses"
>{{ entry.mtime?.toLocaleString() ?? '-' }}</td>
<td
v-if="settings?.directoryView?.cols?.atime"
:class="selectedClasses"
>{{ entry.atime?.toLocaleString() ?? '-' }}</td>
<td v-if="settings?.directoryView?.cols?.mode" class="font-mono"
:class="{ '!border-t-0': suppressBorders.top, '!border-b-0': suppressBorders.bottom }">{{ entry.modeStr
}}</td>
<td v-if="settings?.directoryView?.cols?.owner"
:class="{ '!border-t-0': suppressBorders.top, '!border-b-0': suppressBorders.bottom }">{{ entry.owner }}
</td>
<td v-if="settings?.directoryView?.cols?.group"
:class="{ '!border-t-0': suppressBorders.top, '!border-b-0': suppressBorders.bottom }">{{ entry.group }}
</td>
<td v-if="settings?.directoryView?.cols?.size" class="font-mono text-right"
:class="{ '!border-t-0': suppressBorders.top, '!border-b-0': suppressBorders.bottom }">{{
entry.sizeHuman
}}</td>
<td v-if="settings?.directoryView?.cols?.ctime"
:class="{ '!border-t-0': suppressBorders.top, '!border-b-0': suppressBorders.bottom }">{{
entry.ctime?.toLocaleString() ?? '-'
}}</td>
<td v-if="settings?.directoryView?.cols?.mtime"
:class="{ '!border-t-0': suppressBorders.top, '!border-b-0': suppressBorders.bottom }">{{
entry.mtime?.toLocaleString() ?? '-'
}}</td>
<td v-if="settings?.directoryView?.cols?.atime"
:class="{ '!border-t-0': suppressBorders.top, '!border-b-0': suppressBorders.bottom }">{{
entry.atime?.toLocaleString() ?? '-'
}}</td>
</tr>
<component
:show="show || showEntries"
:is="DirectoryEntryList"
v-if="directoryLike && showEntries"
:host="host"
:path="entry.path"
:isChild="true"
:sortCallback="inheritedSortCallback"
:searchFilterRegExp="searchFilterRegExp"
@cd="(...args) => $emit('cd', ...args)"
<component :show="show || showEntries" :is="DirectoryEntryList" v-if="directoryLike && showEntries" :host="host"
:path="entry.path" :isChild="true" :sortCallback="inheritedSortCallback"
:searchFilterRegExp="searchFilterRegExp" @cd="(...args) => $emit('cd', ...args)"
@edit="(...args) => $emit('edit', ...args)"
@startProcessing="(...args) => $emit('startProcessing', ...args)"
@stopProcessing="(...args) => $emit('stopProcessing', ...args)"
@cancelShowEntries="showEntries = false"
@deselectAll="$emit('deselectAll')"
ref="directoryViewRef"
:level="level + 1"
/>
@stopProcessing="(...args) => $emit('stopProcessing', ...args)" @cancelShowEntries="showEntries = false"
ref="directoryEntryListRef" :level="level + 1"
@toggleSelected="(...args) => $emit('toggleSelected', ...args)" />
</template>
<div
v-else
v-show="show"
@dblclick.stop="doubleClickCallback"
@click.prevent.stop="$emit('toggleSelected', { ctrlKey: $event.ctrlKey, shiftKey: $event.shiftKey })"
>
<div :class="[...selectedClasses, 'flex flex-col items-center w-20 overflow-hidden select-none']">
<template v-else>
<div v-show="show" @dblclick="doubleClickCallback"
@click.prevent="$emit('toggleSelected', entry, $event)"
ref="selectIntersectElement"
:class="['hover:!bg-red-600/10 select-none dir-entry flex flex-col items-center w-20 overflow-hidden', entry.selected ? 'dir-entry-selected' : '']">
<div class="relative w-20">
<component :is="icon" class="icon-default w-20 h-auto" />
<div
:class="[directoryLike ? 'right-3 bottom-5' : 'right-5 bottom-3', 'inline absolute']"
:title="`-> ${entry.target?.rawPath ?? '?'}`"
>
<LinkIcon
v-if="entry.type === 'l'"
:class="[entry.target?.broken ? 'text-red-300 dark:text-red-800' : 'text-gray-100 dark:text-gray-900', 'w-4 h-auto']"
/>
<component :is="icon" class="icon-default w-20 h-auto" :class="{ 'text-gray-500/50': entry.cut }" />
<div :class="[directoryLike ? 'right-3 bottom-5' : 'right-5 bottom-3', 'inline absolute']"
:title="`-> ${entry.target?.rawPath ?? '?'}`">
<LinkIcon v-if="entry.type === 'l'"
:class="[entry.target?.broken ? 'text-red-300 dark:text-red-800' : 'text-gray-100 dark:text-gray-900', 'w-4 h-auto']" />
</div>
</div>
<div class="text-center w-full" style="overflow-wrap: break-word;">{{ entry.name }}</div>
<div class="text-center w-full" :class="{ truncate: !entry.selected, 'line-through': entry.cut }" style="overflow-wrap: break-word;" v-html="escapeStringHTML(entry.name)" :title="entry.name"></div>
</div>
</div>
</template>
</template>
<script>
import { ref, inject, watch, nextTick } from 'vue';
import { ref, inject, watch, nextTick, onBeforeUnmount, onMounted, onActivated, onDeactivated } from 'vue';
import { DocumentIcon, FolderIcon, LinkIcon, DocumentRemoveIcon, ArrowNarrowRightIcon, XIcon, ChevronDownIcon, ChevronUpIcon } from '@heroicons/vue/solid';
import { settingsInjectionKey } from '../keys';
import DirectoryEntryList from './DirectoryEntryList.vue';
@ -119,16 +99,34 @@ export default {
default: null,
},
level: Number,
neighboursSelected: Object,
suppressBorders: {
type: Object,
required: false,
default: {
top: false,
bottom: false,
left: false,
right: false,
}
},
},
setup(props, { emit }) {
const settings = inject(settingsInjectionKey);
const icon = ref(FolderIcon);
const directoryLike = ref(false);
const showEntries = ref(false);
const directoryViewRef = ref();
const directoryEntryListRef = ref();
const selectIntersectElement = ref();
const selectedClasses = ref([]);
// const selectedClasses = ref([]);
if (props.entry.type === 'd' || (props.entry.type === 'l' && props.entry.target?.type === 'd')) {
icon.value = FolderIcon;
directoryLike.value = true;
} else {
icon.value = DocumentIcon;
directoryLike.value = false;
}
const doubleClickCallback = () => {
if (directoryLike.value) {
@ -139,60 +137,50 @@ export default {
}
const refresh = () => {
return directoryViewRef.value?.refresh?.();
return directoryEntryListRef.value?.refresh?.();
}
const toggleShowEntries = () => {
emit('startProcessing');
nextTick(() => {
showEntries.value = !showEntries.value;
nextTick(() => emit('stopProcessing'));
})
showEntries.value = !showEntries.value;
emit('setEntryProp', 'dirOpen', showEntries.value);
}
const getSelected = () => directoryViewRef.value.getSelected() ?? [];
const selectAll = () => {
directoryViewRef.value?.selection.selectAll();
/**
* Recursive get all entries for browser
*
* @param {DirectoryEntryObj[]} - Holds all entries
*
* @returns {DirectoryEntryObj[]} - the accumulator
*/
const gatherEntries = (accumulator = [], onlyVisible = true) => {
return directoryEntryListRef.value?.gatherEntries(accumulator, onlyVisible) ?? accumulator;
}
const deselectAllForward = () => {
directoryViewRef.value?.selection.deselectAllForward();
}
onMounted(() => {
watch(selectIntersectElement, () =>
emit('setEntryProp', 'DOMElement', selectIntersectElement.value), { immediate: true }
);
});
watch(props.entry, () => {
if (props.entry.type === 'd' || (props.entry.type === 'l' && props.entry.target?.type === 'd')) {
icon.value = FolderIcon;
directoryLike.value = true;
} else {
icon.value = DocumentIcon;
directoryLike.value = false;
}
}, { immediate: true });
watch([() => props.neighboursSelected, () => props.entry.selected, () => settings.directoryView?.view], () => selectedClasses.value = [
'border-dashed border-red-600/50 first:border-l-2 last:border-r-2',
props.entry.selected ? 'bg-red-600/5 first:border-l-red-600/50 last:border-r-red-600/50' : 'first:border-l-transparent last:border-r-transparent',
props.entry.selected && (!props.neighboursSelected.above || settings.directoryView?.view !== 'list') ? 'border-t-2' : 'border-t-0',
props.entry.selected && (!props.neighboursSelected.below || settings.directoryView?.view !== 'list') ? 'border-b-2' : 'border-b-0',
], { immediate: true, deep: true });
onBeforeUnmount(() => {
emit('setEntryProp', 'dirOpen', false)
emit('setEntryProp', 'DOMElement', undefined), { immediate: true }
});
return {
settings,
icon,
directoryLike,
selectedClasses,
showEntries,
directoryViewRef,
directoryEntryListRef,
doubleClickCallback,
refresh,
toggleShowEntries,
getSelected,
selectAll,
deselectAllForward,
gatherEntries,
escapeStringHTML,
DirectoryEntryList,
nextTick,
selectIntersectElement,
}
},
components: {
@ -212,7 +200,25 @@ export default {
'toggleSelected',
'startProcessing',
'stopProcessing',
'deselectAll',
'setEntryProp',
]
}
</script>
<style scoped>
tr.dir-entry>td {
@apply border-solid border-y-red-600/50 border-y-0 first:border-l last:border-r first:border-l-transparent last:border-r-transparent;
}
tr.dir-entry-selected>td {
@apply border-y first:border-l-red-600/50 last:border-red-600/50 bg-red-600/10;
}
/*
div.dir-entry {
@apply border-solid border border-transparent;
} */
div.dir-entry-selected {
@apply bg-red-600/10;
}
</style>

View File

@ -3,11 +3,12 @@
<DirectoryEntry :show="true" :host="host" :entry="entry" :inheritedSortCallback="sortCallback"
:searchFilterRegExp="searchFilterRegExp" @cd="(...args) => $emit('cd', ...args)"
@edit="(...args) => $emit('edit', ...args)"
@toggleSelected="modifiers => selection.toggle(entry, index, modifiers)"
@deselectAll="selection.deselectAllBackward()" @sortEntries="sortEntries" @updateStats="emitStats"
@toggleSelected="(...args) => $emit('toggleSelected', ...args)"
@sortEntries="sortEntries" @updateStats="emitStats"
@startProcessing="(...args) => $emit('startProcessing', ...args)"
@stopProcessing="(...args) => $emit('stopProcessing', ...args)" ref="entryRefs" :level="level"
:neighboursSelected="{ above: visibleEntries[index - 1]?.selected ?? false, below: visibleEntries[index + 1]?.selected ?? false }" />
@setEntryProp="(prop, value) => entry[prop] = value"
:suppressBorders="{ top: visibleEntries[index - 1]?.selected && !(visibleEntries[index - 1]?.dirOpen), bottom: visibleEntries[index + 1]?.selected && !(entry.dirOpen), left: false, right: false }" />
</template>
<tr v-if="show && visibleEntries.length === 0">
<td :colspan="Object.values(settings?.directoryView?.cols ?? {}).reduce((sum, current) => current ? sum + 1 : sum, 1) ?? 100"
@ -72,59 +73,6 @@ export default {
return props.sortCallback(a, b);
}
});
const selection = reactive({
lastSelectedInd: null,
toggle: (entry, index, modifiers) => {
const entrySelectedValue = entry.selected;
if (!modifiers.ctrlKey) {
const tmpLastSelectedInd = selection.lastSelectedInd;
selection.deselectAllBackward();
selection.lastSelectedInd = tmpLastSelectedInd;
}
if (modifiers.shiftKey && selection.lastSelectedInd !== null) {
let [startInd, endInd] = [selection.lastSelectedInd, index];
if (endInd < startInd)
[startInd, endInd] = [endInd, startInd];
visibleEntries.value
.slice(startInd, endInd + 1)
.map(entry => entry.selected = true);
} else {
entry.selected = modifiers.ctrlKey ? !entrySelectedValue : true;
if (entry.selected)
selection.lastSelectedInd = index;
else
selection.lastSelectedInd = null;
}
},
getSelected: () => [
...visibleEntries.value.filter(entry => entry.selected),
...entryRefs.value
.filter(entryRef => entryRef.showEntries)
.map(entryRef => entryRef.getSelected())
.flat(1),
],
clear: () => {
entries.value.map(entry => entry.selected = false);
},
selectAll: () => {
visibleEntries.value
.map(entry => entry.selected = true);
entryRefs.value
.map(entryRef => entryRef.selectAll());
},
deselectAllBackward: () => {
if (props.level > 0)
emit('deselectAll');
else
selection.deselectAllForward();
},
deselectAllForward: () => {
selection.clear();
selection.lastSelectedInd = null;
entryRefs.value
.map(entryRef => entryRef.deselectAllForward());
},
});
const processingHandler = {
count: 0,
start: () => {
@ -147,7 +95,6 @@ export default {
if (!props.path) {
return;
}
selection.lastSelectedInd = null;
processingHandler.start();
try {
const cwd = props.path;
@ -162,7 +109,7 @@ export default {
);
if (props.path !== cwd)
return; // changed directory before could finish
entries.value = [...tmpEntries.sort(sortCallbackComputed.value)].map(entry => reactive({...entry, cut: clipboard.content.find(a => a.path === entry.path && a.host === entry.host) ?? false}));
entries.value = [...tmpEntries.sort(sortCallbackComputed.value)].map(entry => reactive({ ...entry, cut: clipboard.content.find(a => a.path === entry.path && a.host === entry.host) ?? false }));
console.timeEnd('getEntries');
} catch (error) {
entries.value = [];
@ -203,6 +150,23 @@ export default {
(!/^\./.test(entry.name) || settings?.directoryView?.showHidden)
&& (props.searchFilterRegExp?.test(entry.name) ?? true);
/**
* Recursive get all entries for browser
*
* @param {DirectoryEntryObj[]} - Holds all entries
*
* @returns {DirectoryEntryObj[]} - the accumulator
*/
const gatherEntries = (accumulator = [], onlyVisible = true) => {
const subset = onlyVisible ? visibleEntries.value : entries.value;
return accumulator.concat(
subset,
entryRefs.value
.filter(entryRef => entryRef.showEntries)
.map(entryRef => entryRef.gatherEntries(accumulator, onlyVisible)).flat(1)
);
}
const fileSystemWatcher = FileSystemWatcher(props.path, { superuser: 'try', host: props.host, ignoreSelf: true });
fileSystemWatcher.onCreated = async (eventObj) => {
@ -278,12 +242,12 @@ export default {
entries,
visibleEntries,
entryRefs,
selection,
getEntries,
refresh,
emitStats,
sortEntries,
entryFilterCallback,
gatherEntries,
}
},
components: {

View File

@ -1,74 +1,83 @@
<template>
<div class="h-full" @keydown.prevent.stop="keyHandler($event)" tabindex="-1">
<Table v-if="settings.directoryView?.view === 'list'" emptyText="No entries." noHeader stickyHeaders noShrink
noShrinkHeight="h-full">
<template #thead>
<tr>
<th class="!pl-1 border-l-2 border-l-transparent last:border-r-2 last:border-r-transparent">
<div class="flex flex-row flex-nowrap gap-1 items-center">
<div class="flex items-center justify-center w-6">
<LoadingSpinner v-if="processing" class="size-icon" />
<div class="h-full" @keydown="keyHandler($event)" tabindex="-1">
<DragSelectArea class="h-full" @selectRectangle="selectRectangle" @mouseup.exact="deselectAll()">
<Table v-if="settings.directoryView?.view === 'list'" emptyText="No entries." noHeader stickyHeaders
noShrink noShrinkHeight="h-full">
<template #thead>
<tr>
<th class="!pl-1 border-l-2 border-l-transparent last:border-r-2 last:border-r-transparent">
<div class="flex flex-row flex-nowrap gap-1 items-center">
<div class="flex items-center justify-center w-6">
<LoadingSpinner v-if="processing" class="size-icon" />
</div>
<div class="grow">Name</div>
<SortCallbackButton initialFuncIsMine v-model="sortCallback"
:compareFunc="sortCallbacks.name" />
</div>
<div class="grow">Name</div>
<SortCallbackButton initialFuncIsMine v-model="sortCallback"
:compareFunc="sortCallbacks.name" />
</div>
</th>
<th class="last:border-r-2 last:border-r-transparent" v-if="settings?.directoryView?.cols?.mode">
Mode</th>
<th class="last:border-r-2 last:border-r-transparent" v-if="settings?.directoryView?.cols?.owner">
<div class="flex flex-row flex-nowrap gap-2 items-center">
<div class="grow">Owner</div>
<SortCallbackButton v-model="sortCallback" :compareFunc="sortCallbacks.owner" />
</div>
</th>
<th class="last:border-r-2 last:border-r-transparent" v-if="settings?.directoryView?.cols?.group">
<div class="flex flex-row flex-nowrap gap-2 items-center">
<div class="grow">Group</div>
<SortCallbackButton v-model="sortCallback" :compareFunc="sortCallbacks.group" />
</div>
</th>
<th class="last:border-r-2 last:border-r-transparent" v-if="settings?.directoryView?.cols?.size">
<div class="flex flex-row flex-nowrap gap-2 items-center">
<div class="grow text-right">Size</div>
<SortCallbackButton v-model="sortCallback" :compareFunc="sortCallbacks.size" />
</div>
</th>
<th class="last:border-r-2 last:border-r-transparent" v-if="settings?.directoryView?.cols?.ctime">
<div class="flex flex-row flex-nowrap gap-2 items-center">
<div class="grow">Created</div>
<SortCallbackButton v-model="sortCallback" :compareFunc="sortCallbacks.ctime" />
</div>
</th>
<th class="last:border-r-2 last:border-r-transparent" v-if="settings?.directoryView?.cols?.mtime">
<div class="flex flex-row flex-nowrap gap-2 items-center">
<div class="grow">Modified</div>
<SortCallbackButton v-model="sortCallback" :compareFunc="sortCallbacks.mtime" />
</div>
</th>
<th class="last:border-r-2 last:border-r-transparent" v-if="settings?.directoryView?.cols?.atime">
<div class="flex flex-row flex-nowrap gap-2 items-center">
<div class="grow">Accessed</div>
<SortCallbackButton v-model="sortCallback" :compareFunc="sortCallbacks.atime" />
</div>
</th>
</tr>
</template>
<template #tbody>
</th>
<th class="last:border-r-2 last:border-r-transparent"
v-if="settings?.directoryView?.cols?.mode">
Mode</th>
<th class="last:border-r-2 last:border-r-transparent"
v-if="settings?.directoryView?.cols?.owner">
<div class="flex flex-row flex-nowrap gap-2 items-center">
<div class="grow">Owner</div>
<SortCallbackButton v-model="sortCallback" :compareFunc="sortCallbacks.owner" />
</div>
</th>
<th class="last:border-r-2 last:border-r-transparent"
v-if="settings?.directoryView?.cols?.group">
<div class="flex flex-row flex-nowrap gap-2 items-center">
<div class="grow">Group</div>
<SortCallbackButton v-model="sortCallback" :compareFunc="sortCallbacks.group" />
</div>
</th>
<th class="last:border-r-2 last:border-r-transparent"
v-if="settings?.directoryView?.cols?.size">
<div class="flex flex-row flex-nowrap gap-2 items-center">
<div class="grow text-right">Size</div>
<SortCallbackButton v-model="sortCallback" :compareFunc="sortCallbacks.size" />
</div>
</th>
<th class="last:border-r-2 last:border-r-transparent"
v-if="settings?.directoryView?.cols?.ctime">
<div class="flex flex-row flex-nowrap gap-2 items-center">
<div class="grow">Created</div>
<SortCallbackButton v-model="sortCallback" :compareFunc="sortCallbacks.ctime" />
</div>
</th>
<th class="last:border-r-2 last:border-r-transparent"
v-if="settings?.directoryView?.cols?.mtime">
<div class="flex flex-row flex-nowrap gap-2 items-center">
<div class="grow">Modified</div>
<SortCallbackButton v-model="sortCallback" :compareFunc="sortCallbacks.mtime" />
</div>
</th>
<th class="last:border-r-2 last:border-r-transparent"
v-if="settings?.directoryView?.cols?.atime">
<div class="flex flex-row flex-nowrap gap-2 items-center">
<div class="grow">Accessed</div>
<SortCallbackButton v-model="sortCallback" :compareFunc="sortCallbacks.atime" />
</div>
</th>
</tr>
</template>
<template #tbody>
<DirectoryEntryList :host="host" :path="path" :sortCallback="sortCallback"
:searchFilterRegExp="searchFilterRegExp" @cd="(...args) => $emit('cd', ...args)"
@edit="(...args) => $emit('edit', ...args)" @toggleSelected="toggleSelected"
@updateStats="(...args) => $emit('updateStats', ...args)" @startProcessing="processing++"
@stopProcessing="processing--" ref="directoryEntryListRef" :level="0" />
</template>
</Table>
<div v-else class="flex flex-wrap p-2 bg-well h-full overflow-y-auto content-start">
<DirectoryEntryList :host="host" :path="path" :sortCallback="sortCallback"
:searchFilterRegExp="searchFilterRegExp" @cd="(...args) => $emit('cd', ...args)"
@edit="(...args) => $emit('edit', ...args)"
@edit="(...args) => $emit('edit', ...args)" @toggleSelected="toggleSelected"
@updateStats="(...args) => $emit('updateStats', ...args)" @startProcessing="processing++"
@stopProcessing="processing--" ref="directoryEntryListRef" :level="0" />
</template>
</Table>
<div v-else class="flex flex-wrap p-2 gap-2 bg-well h-full overflow-y-auto content-start"
@click.prevent.stop="directoryEntryListRef.selection.deselectAllForward()">
<DirectoryEntryList :host="host" :path="path" :sortCallback="sortCallback"
:searchFilterRegExp="searchFilterRegExp" @cd="(...args) => $emit('cd', ...args)"
@edit="(...args) => $emit('edit', ...args)" @updateStats="(...args) => $emit('updateStats', ...args)"
@startProcessing="processing++" @stopProcessing="processing--" ref="directoryEntryListRef" :level="0" />
</div>
</div>
</DragSelectArea>
</div>
</template>
@ -79,6 +88,7 @@ import { clipboardInjectionKey, notificationsInjectionKey, settingsInjectionKey
import LoadingSpinner from './LoadingSpinner.vue';
import SortCallbackButton from './SortCallbackButton.vue';
import DirectoryEntryList from './DirectoryEntryList.vue';
import DragSelectArea from './DragSelectArea.vue';
export default {
props: {
@ -111,29 +121,80 @@ export default {
return directoryEntryListRef.value?.refresh?.();
}
const getSelected = () => directoryEntryListRef.value.selection.getSelected() ?? [];
const getSelected = () => directoryEntryListRef.value?.gatherEntries().filter(entry => entry.selected) ?? [];
let lastSelectedEntry = null;
const toggleSelected = (entry, { ctrlKey, shiftKey }) => {
const entrySelectedValue = entry.selected;
if (!ctrlKey)
deselectAll();
if (shiftKey && lastSelectedEntry !== null) {
const entries = directoryEntryListRef.value?.gatherEntries();
let [startInd, endInd] = [entries.indexOf(lastSelectedEntry), entries.indexOf(entry)];
if (startInd != -1 && endInd != -1) {
if (endInd < startInd)
[startInd, endInd] = [endInd, startInd];
entries
.slice(startInd, endInd + 1)
.map(entry => entry.selected = true);
return;
}
}
entry.selected = ctrlKey ? !entrySelectedValue : true;
if (entry.selected)
lastSelectedEntry = entry;
else
lastSelectedEntry = null;
}
const selectAll = () => directoryEntryListRef.value?.gatherEntries().map(entry => entry.selected = true);
const deselectAll = () => directoryEntryListRef.value?.gatherEntries([], false).map(entry => entry.selected = false);
const selectRectangle = (rect, { ctrlKey, shiftKey }) => {
if (!(ctrlKey || shiftKey))
deselectAll();
directoryEntryListRef.value?.gatherEntries().map(entry => {
const entryRect = entry.DOMElement?.getBoundingClientRect();
if (
!entryRect
|| rect.x1 > entryRect.right || rect.x2 < entryRect.left
|| rect.y1 > entryRect.bottom || rect.y2 < entryRect.top
)
return;
entry.selected = ctrlKey ? !entry.selected : true;
});
}
/**
* @param {KeyboardEvent} event
*/
const keyHandler = (event) => {
console.log("DirectoryView::keyHandler:", event);
if (event.key === 'Escape')
directoryEntryListRef.value?.selection.deselectAllForward();
if (event.key === 'Escape') {
if (getSelected().length === 0) {
clipboard.content.map(entry => entry.cut = false);
clipboard.content = [];
} else {
deselectAll();
}
}
if (event.ctrlKey) {
switch (event.key.toLowerCase()) {
const keypress = event.key.toLowerCase();
switch (keypress) {
case 'a':
directoryEntryListRef.value?.selection.selectAll();
selectAll();
break;
case 'h':
settings.directoryView.showHidden = !settings.directoryView.showHidden;
break;
case 'c':
clipboard.content = getSelected();
break;
case 'x':
const isCut = keypress === 'x';
clipboard.content.map(entry => entry.cut = false);
clipboard.content = getSelected().map(entry => {
entry.cut = true;
entry.cut = isCut;
return entry;
});
break;
@ -144,20 +205,23 @@ export default {
destination = selected[0];
if (destination.type !== 'd' && !(destination.type === 'l' && destination.target.type === 'd')) {
notifications.value.constructNotification("Paste Failed", 'Cannot paste to non-directory.', 'error');
return;
break;
}
} else if (selected.length === 0) {
destination = { host: props.host, path: props.path };
} else {
notifications.value.constructNotification("Paste Failed", 'Cannot paste to multiple directories.', 'error');
return;
break;
}
console.log("paste", clipboard.content.map(entry => ({ host: entry.host, path: entry.path })), destination);
break;
default:
break;
return;
}
} else {
return;
}
event.preventDefault();
}
return {
@ -169,6 +233,11 @@ export default {
refresh,
getSelected,
keyHandler,
getSelected,
toggleSelected,
selectAll,
deselectAll,
selectRectangle,
}
},
components: {
@ -176,6 +245,7 @@ export default {
Table,
LoadingSpinner,
SortCallbackButton,
DragSelectArea,
},
emits: [
'cd',