toggleSelected -> entryAction, maintain selection on refresh, tallySelection on entry list change

This commit is contained in:
joshuaboud 2022-06-16 13:05:43 -03:00
parent 3d0322c6e3
commit 9e931f5bfa
No known key found for this signature in database
GPG Key ID: 17EFB59E2A8BF50E
3 changed files with 51 additions and 31 deletions

View File

@ -1,6 +1,7 @@
<template> <template>
<template v-if="settings.directoryView?.view === 'list'"> <template v-if="settings.directoryView?.view === 'list'">
<tr @dblclick="doubleClickCallback" @click.prevent="$emit('toggleSelected', entry, $event)" <tr @dblclick="doubleClickCallback" @click.prevent="$emit('entryAction', 'toggleSelected', entry, $event)"
@mouseup.stop
:class="{'hover:!bg-red-600/10 select-none dir-entry': true, 'dir-entry-selected': entry.selected, 'suppress-border-t': suppressBorderT, 'suppress-border-b': suppressBorderB }" :class="{'hover:!bg-red-600/10 select-none dir-entry': true, 'dir-entry-selected': entry.selected, 'suppress-border-t': suppressBorderT, 'suppress-border-b': suppressBorderB }"
ref="selectIntersectElement"> ref="selectIntersectElement">
<td class="!pl-1" > <td class="!pl-1" >
@ -57,10 +58,9 @@
@startProcessing="(...args) => $emit('startProcessing', ...args)" @startProcessing="(...args) => $emit('startProcessing', ...args)"
@stopProcessing="(...args) => $emit('stopProcessing', ...args)" @cancelShowEntries="showEntries = false" @stopProcessing="(...args) => $emit('stopProcessing', ...args)" @cancelShowEntries="showEntries = false"
ref="directoryEntryListRef" :level="level + 1" :selectedCount="selectedCount" ref="directoryEntryListRef" :level="level + 1" :selectedCount="selectedCount"
@toggleSelected="(...args) => $emit('toggleSelected', ...args)"
@entryAction="(...args) => $emit('entryAction', ...args)" /> @entryAction="(...args) => $emit('entryAction', ...args)" />
</template> </template>
<div v-else @dblclick="doubleClickCallback" @click.prevent="$emit('toggleSelected', entry, $event)" <div v-else @dblclick="doubleClickCallback" @click.prevent="$emit('entryAction', 'toggleSelected', entry, $event)" @mouseup.stop
ref="selectIntersectElement" ref="selectIntersectElement"
class="hover:!bg-red-600/10 select-none dir-entry flex flex-col items-center overflow-hidden dir-entry-width p-2" class="hover:!bg-red-600/10 select-none dir-entry flex flex-col items-center overflow-hidden dir-entry-width p-2"
:class="{ 'dir-entry-selected': entry.selected, '!border-t-transparent': suppressBorderT, '!border-b-transparent': suppressBorderB, '!border-l-transparent': suppressBorderL, '!border-r-transparent': suppressBorderR }"> :class="{ 'dir-entry-selected': entry.selected, '!border-t-transparent': suppressBorderT, '!border-b-transparent': suppressBorderB, '!border-l-transparent': suppressBorderL, '!border-r-transparent': suppressBorderR }">
@ -158,6 +158,10 @@ export default {
emit('setEntryProp', 'DOMElement', selectIntersectElement.value); emit('setEntryProp', 'DOMElement', selectIntersectElement.value);
}); });
onUpdated(() => {
emit('setEntryProp', 'DOMElement', selectIntersectElement.value);
});
return { return {
settings, settings,
icon, icon,
@ -186,7 +190,6 @@ export default {
ChevronUpIcon, ChevronUpIcon,
}, },
emits: [ emits: [
'toggleSelected',
'startProcessing', 'startProcessing',
'stopProcessing', 'stopProcessing',
'setEntryProp', 'setEntryProp',

View File

@ -1,7 +1,7 @@
<template> <template>
<DirectoryEntry v-for="entry, index in visibleEntries" :key="entry.path" :host="host" :entry="entry" <DirectoryEntry v-for="entry, index in visibleEntries" :key="entry.path" :host="host" :entry="entry"
:inheritedSortCallback="sortCallback" :searchFilterRegExp="searchFilterRegExp" :inheritedSortCallback="sortCallback" :searchFilterRegExp="searchFilterRegExp"
@toggleSelected="(...args) => $emit('toggleSelected', ...args)" @sortEntries="sortEntries" @sortEntries="sortEntries"
@startProcessing="(...args) => $emit('startProcessing', ...args)" @startProcessing="(...args) => $emit('startProcessing', ...args)"
@stopProcessing="(...args) => $emit('stopProcessing', ...args)" ref="entryRefs" :level="level" :selectedCount="selectedCount" @stopProcessing="(...args) => $emit('stopProcessing', ...args)" ref="entryRefs" :level="level" :selectedCount="selectedCount"
@setEntryProp="(prop, value) => entry[prop] = value" @setEntryProp="(prop, value) => entry[prop] = value"
@ -19,14 +19,14 @@
</tr> </tr>
<Teleport to="#footer-text" v-if="selectedCount === 0"> <Teleport to="#footer-text" v-if="selectedCount === 0">
<div> <div>
<span v-if="level > 0">{{ path.split('/').slice(-1 * (level+1)).join('/') }}:</span> <span v-if="level > 0">{{ path.split('/').slice(-1 * (level)).join('/') }}:</span>
{{ stats }} {{ stats }}
</div> </div>
</Teleport> </Teleport>
</template> </template>
<script> <script>
import { ref, reactive, computed, inject, watch, onBeforeUnmount } from 'vue'; import { ref, reactive, computed, inject, watch, onBeforeUnmount, onMounted, nextTick } from 'vue';
import { errorStringHTML } from '@45drives/cockpit-helpers'; import { errorStringHTML } from '@45drives/cockpit-helpers';
import { notificationsInjectionKey, settingsInjectionKey, clipboardInjectionKey } from '../keys'; import { notificationsInjectionKey, settingsInjectionKey, clipboardInjectionKey } from '../keys';
import DirectoryEntry from './DirectoryEntry.vue'; import DirectoryEntry from './DirectoryEntry.vue';
@ -117,8 +117,12 @@ export default {
if (props.path !== cwd) { if (props.path !== cwd) {
return; // changed directory before could finish return; // changed directory before could finish
} }
const selectedIds = gatherEntries([], false).filter(entry => entry.selected).map(entry => entry.uniqueId);
const clipboardIds = clipboard.content.map(entry => entry.uniqueId); const clipboardIds = clipboard.content.map(entry => entry.uniqueId);
tmpEntries.map(entry => entry.cut = clipboardIds.includes(entry.uniqueId)); tmpEntries.map(entry => {
entry.selected = selectedIds.includes(entry.uniqueId);
entry.cut = clipboardIds.includes(entry.uniqueId);
});
entries.value = [...tmpEntries.sort(sortCallbackComputed.value)]; entries.value = [...tmpEntries.sort(sortCallbackComputed.value)];
} catch (error) { } catch (error) {
entries.value = []; entries.value = [];
@ -165,15 +169,6 @@ export default {
); );
} }
const getBorderSuppression = (entry, index) => {
return {
top: visibleEntries.value[index - props.cols]?.selected && !(visibleEntries.value[index - props.cols]?.dirOpen),
bottom: visibleEntries.value[index + props.cols]?.selected && !(entry.dirOpen),
left: settings.directoryView.view !== 'list' && (visibleEntries.value[index - 1]?.selected && (index) % props.cols !== 0),
right: settings.directoryView.view !== 'list' && (visibleEntries.value[index + 1]?.selected && (index + 1) % props.cols !== 0),
}
};
const fileSystemWatcher = FileSystemWatcher(props.path, { superuser: 'try', host: props.host, ignoreSelf: true }); const fileSystemWatcher = FileSystemWatcher(props.path, { superuser: 'try', host: props.host, ignoreSelf: true });
fileSystemWatcher.onCreated = async (eventObj) => { fileSystemWatcher.onCreated = async (eventObj) => {
@ -202,7 +197,7 @@ export default {
if (!newContent) if (!newContent)
return; // temp file deleted too quickly return; // temp file deleted too quickly
const attrsChanged = ["name", "owner", "group", "size", "ctime", "mtime", "atime"].map(key => String(entry[key]) !== String(newContent[key])).includes(true); const attrsChanged = ["name", "owner", "group", "size", "ctime", "mtime", "atime"].map(key => String(entry[key]) !== String(newContent[key])).includes(true);
Object.assign(entry, newContent); Object.assign(entry, newContent, { cut: entry.cut, selected: entry.selected });
if (attrsChanged) sortEntries(); if (attrsChanged) sortEntries();
} }
else else
@ -237,6 +232,7 @@ export default {
return _stats; return _stats;
}, { files: 0, dirs: 0, size: 0 }); }, { files: 0, dirs: 0, size: 0 });
stats.value = `${_stats.files} file${_stats.files === 1 ? '' : 's'}, ${_stats.dirs} director${_stats.dirs === 1 ? 'y' : 'ies'} (${cockpit.format_bytes(_stats.size, 1000).replace(/(?<!B)$/, ' B')})`; stats.value = `${_stats.files} file${_stats.files === 1 ? '' : 's'}, ${_stats.dirs} director${_stats.dirs === 1 ? 'y' : 'ies'} (${cockpit.format_bytes(_stats.size, 1000).replace(/(?<!B)$/, ' B')})`;
nextTick(() => emit('tallySelected'));
}); });
watch(() => props.path, (current, old) => { watch(() => props.path, (current, old) => {
@ -263,7 +259,6 @@ export default {
sortEntries, sortEntries,
entryFilterCallback, entryFilterCallback,
gatherEntries, gatherEntries,
getBorderSuppression,
} }
}, },
components: { components: {
@ -275,6 +270,7 @@ export default {
'cancelShowEntries', 'cancelShowEntries',
'deselectAll', 'deselectAll',
'entryAction', 'entryAction',
'tallySelected',
] ]
} }
</script> </script>

View File

@ -1,6 +1,6 @@
<template> <template>
<div class="h-full" @keydown="keyHandler($event)" tabindex="-1" :class="{ '!cursor-wait': processing }"> <div class="h-full" @keydown="keyHandler($event)" tabindex="-1" :class="{ '!cursor-wait': processing }">
<DragSelectArea class="h-full" @selectRectangle="selectRectangle" @mouseup.exact="deselectAll()"> <DragSelectArea class="h-full" @selectRectangle="selectRectangle" @mouseup.exact="deselectAll">
<Table :key="host + path" v-if="settings.directoryView?.view === 'list'" emptyText="No entries." noHeader stickyHeaders <Table :key="host + path" v-if="settings.directoryView?.view === 'list'" emptyText="No entries." noHeader stickyHeaders
noShrink noShrinkHeight="h-full"> noShrink noShrinkHeight="h-full">
<template #thead> <template #thead>
@ -65,9 +65,10 @@
<template #tbody> <template #tbody>
<DirectoryEntryList :host="host" :path="path" :sortCallback="sortCallback" <DirectoryEntryList :host="host" :path="path" :sortCallback="sortCallback"
:searchFilterRegExp="searchFilterRegExp" @cd="(...args) => $emit('cd', ...args)" :searchFilterRegExp="searchFilterRegExp" @cd="(...args) => $emit('cd', ...args)"
@edit="(...args) => $emit('edit', ...args)" @toggleSelected="toggleSelected" @edit="(...args) => $emit('edit', ...args)"
@startProcessing="processing++" @stopProcessing="processing--" @startProcessing="processing++" @stopProcessing="processing--"
@entryAction="(...args) => $emit('entryAction', ...args)" ref="directoryEntryListRef" @entryAction="handleEntryAction" ref="directoryEntryListRef"
@tallySelected="tallySelected"
:level="0" :cols="1" :selectedCount="selectedCount" /> :level="0" :cols="1" :selectedCount="selectedCount" />
</template> </template>
</Table> </Table>
@ -107,18 +108,23 @@
@wheel="scrollHandler"> @wheel="scrollHandler">
<DirectoryEntryList :host="host" :path="path" :sortCallback="sortCallback" <DirectoryEntryList :host="host" :path="path" :sortCallback="sortCallback"
:searchFilterRegExp="searchFilterRegExp" :searchFilterRegExp="searchFilterRegExp"
@toggleSelected="toggleSelected"
@startProcessing="processing++" @stopProcessing="processing--" @startProcessing="processing++" @stopProcessing="processing--"
@entryAction="(...args) => $emit('entryAction', ...args)" ref="directoryEntryListRef" @entryAction="handleEntryAction"
@tallySelected="tallySelected" ref="directoryEntryListRef"
:level="0" :cols="cols" :selectedCount="selectedCount" /> :level="0" :cols="cols" :selectedCount="selectedCount" />
</div> </div>
</div> </div>
</DragSelectArea> </DragSelectArea>
</div> </div>
<Teleport to="#footer-text">
<div v-if="selectedCount > 1">
{{ selectedCount }} items selected
</div>
</Teleport>
</template> </template>
<script> <script>
import { ref, inject, watch, onMounted, computed, onBeforeUnmount, onUpdated } from 'vue'; import { ref, inject, watch, onMounted, computed, onBeforeUnmount, onUpdated, nextTick } from 'vue';
import Table from './Table.vue'; import Table from './Table.vue';
import { clipboardInjectionKey, notificationsInjectionKey, settingsInjectionKey } from '../keys'; import { clipboardInjectionKey, notificationsInjectionKey, settingsInjectionKey } from '../keys';
import LoadingSpinner from './LoadingSpinner.vue'; import LoadingSpinner from './LoadingSpinner.vue';
@ -132,7 +138,7 @@ export default {
path: String, path: String,
searchFilterRegExp: RegExp, searchFilterRegExp: RegExp,
}, },
setup(props) { setup(props, { emit }) {
/** /**
* @type {NavigatorSettings} * @type {NavigatorSettings}
*/ */
@ -162,7 +168,9 @@ export default {
const getSelected = () => directoryEntryListRef.value?.gatherEntries().filter(entry => entry.selected) ?? []; const getSelected = () => directoryEntryListRef.value?.gatherEntries().filter(entry => entry.selected) ?? [];
const tallySelected = async () => selectedCount.value = getSelected().reduce((n) => n + 1, 0); const tallySelected = () => {
selectedCount.value = getSelected().length;
}
let lastSelectedEntry = null; let lastSelectedEntry = null;
const toggleSelected = (entry, { ctrlKey, shiftKey }) => { const toggleSelected = (entry, { ctrlKey, shiftKey }) => {
@ -193,13 +201,12 @@ export default {
} }
const selectAll = () => { const selectAll = () => {
directoryEntryListRef.value?.gatherEntries().map(entry => entry.selected = true); selectedCount.value = directoryEntryListRef.value?.gatherEntries().map(entry => entry.selected = true).length ?? 0;
tallySelected();
} }
const deselectAll = () => { const deselectAll = (event) => {
directoryEntryListRef.value?.gatherEntries([], false).map(entry => entry.selected = false); directoryEntryListRef.value?.gatherEntries([], false).map(entry => entry.selected = false);
tallySelected(); selectedCount.value = 0;
} }
const selectRectangle = (rect, { ctrlKey, shiftKey }) => { const selectRectangle = (rect, { ctrlKey, shiftKey }) => {
@ -216,6 +223,7 @@ export default {
return; return;
entry.selected = ctrlKey ? !entry.selected : true; entry.selected = ctrlKey ? !entry.selected : true;
}); });
tallySelected();
} }
/** /**
@ -294,6 +302,17 @@ export default {
} }
} }
const handleEntryAction = (action, entry, event, ...args) => {
switch (action) {
case 'toggleSelected':
toggleSelected(entry, event);
break;
default:
emit('entryAction', action, entry, event, ...args);
break;
}
}
const getCols = () => { const getCols = () => {
const gridWidth = gridRef.value?.clientWidth; const gridWidth = gridRef.value?.clientWidth;
if (!gridWidth) if (!gridWidth)
@ -334,6 +353,8 @@ export default {
selectAll, selectAll,
deselectAll, deselectAll,
selectRectangle, selectRectangle,
handleEntryAction,
tallySelected,
} }
}, },
components: { components: {