mirror of
https://github.com/45Drives/cockpit-navigator.git
synced 2025-09-26 03:08:41 +02:00
toggleSelected -> entryAction, maintain selection on refresh, tallySelection on entry list change
This commit is contained in:
parent
3d0322c6e3
commit
9e931f5bfa
@ -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',
|
||||||
|
@ -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>
|
||||||
|
@ -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: {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user