mirror of
https://github.com/45Drives/cockpit-navigator.git
synced 2025-07-29 16:45:13 +02:00
add context menu
This commit is contained in:
parent
b96902391e
commit
116a422320
86
navigator/src/components/ContextMenu.vue
Normal file
86
navigator/src/components/ContextMenu.vue
Normal file
@ -0,0 +1,86 @@
|
||||
<template>
|
||||
<transition enter-active-class="origin-top-left transition ease-out duration-100" enter-from-class="transform opacity-0 scale-95"
|
||||
enter-to-class="transform opacity-100 scale-100" leave-active-class="origin-top-left transition ease-in duration-75"
|
||||
leave-from-class="transform opacity-100 scale-100" leave-to-class="transform opacity-0 scale-95">
|
||||
<div v-if="show" class="fixed inset-0 bg-transparent" @click="$emit('hide')">
|
||||
<div class="fixed z-20 max-w-sm flex flex-col items-stretch bg-default shadow-lg divide-y divide-default position-contextmenu">
|
||||
<div class="flex items-stretch">
|
||||
<button :disabled="!pathHistory?.backAllowed()" @click="$emit('browserAction', 'back')" :class="{'grow flex items-center justify-center p-2': true, 'hover:bg-red-600/10': pathHistory?.backAllowed()}">
|
||||
<ArrowLeftIcon class="size-icon icon-default" />
|
||||
</button>
|
||||
<button :disabled="!pathHistory?.forwardAllowed()" @click="$emit('browserAction', 'forward')" :class="{'grow flex items-center justify-center p-2': true, 'hover:bg-red-600/10': pathHistory?.forwardAllowed()}">
|
||||
<ArrowRightIcon class="size-icon icon-default" />
|
||||
</button>
|
||||
<button @click="$emit('browserAction', 'up')" class="grow hover:bg-red-600/10 flex items-center justify-center p-2">
|
||||
<ArrowUpIcon class="size-icon icon-default" />
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex flex-col items-stretch">
|
||||
<!-- general actions -->
|
||||
<button v-if="entry?.path !== '/'" @click="$emit('browserAction', 'editPermissions', entry)" class="context-menu-button">
|
||||
Edit permissions
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="entry?.resolvedType === 'f'" class="flex flex-col items-stretch">
|
||||
<!-- regular file actions -->
|
||||
<button @click="$emit('browserAction', 'edit', entry)" class="context-menu-button">
|
||||
Edit contents
|
||||
</button>
|
||||
<button @click="$emit('browserAction', 'download', entry)" class="context-menu-button">
|
||||
Download
|
||||
</button>
|
||||
</div>
|
||||
<div v-else-if="entry?.resolvedType === 'd'" class="flex flex-col items-stretch">
|
||||
<!-- directory actions -->
|
||||
<button @click="$emit('browserAction', 'cd', entry)" class="context-menu-button">
|
||||
Open
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="entry?.type === 'l'" class="flex flex-col items-stretch">
|
||||
<!-- link actions -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { inject } from 'vue'
|
||||
import { ArrowLeftIcon, ArrowRightIcon, ArrowUpIcon } from '@heroicons/vue/solid';
|
||||
import { pathHistoryInjectionKey } from '../keys';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
show: Boolean,
|
||||
event: Object,
|
||||
entry: Object,
|
||||
},
|
||||
setup(props, { emit }) {
|
||||
const pathHistory = inject(pathHistoryInjectionKey);
|
||||
|
||||
return {
|
||||
pathHistory,
|
||||
}
|
||||
},
|
||||
components: {
|
||||
ArrowLeftIcon,
|
||||
ArrowRightIcon,
|
||||
ArrowUpIcon,
|
||||
},
|
||||
emits: [
|
||||
'hide',
|
||||
'browserAction',
|
||||
]
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
div.position-contextmenu {
|
||||
top: v-bind(`${event?.clientY ?? 0}px`);
|
||||
left: v-bind(`${event?.clientX ?? 0}px`);
|
||||
}
|
||||
|
||||
button.context-menu-button {
|
||||
@apply text-default hover:bg-red-600/10 font-normal px-4 py-2 text-sm text-left;
|
||||
}
|
||||
</style>
|
@ -4,14 +4,14 @@
|
||||
:class="{ 'select-none dir-entry': true, '!bg-red-600/10': hover && !entry.selected, '!bg-red-600/20': hover && entry.selected, 'dir-entry-selected': entry.selected, 'suppress-border-t': suppressBorderT, 'suppress-border-b': suppressBorderB }">
|
||||
<td class="!pl-1 relative">
|
||||
<div :class="[entry.cut ? 'line-through' : '', 'flex items-center gap-1']">
|
||||
<div class="w-6" v-for="i in Array(level).fill(0)" v-memo="[level]"></div>
|
||||
<div class="w-6" v-for="i in Array(level).fill(0).keys()" :key="i" v-memo="[level]"></div>
|
||||
<div class="relative w-6" :class="[entry.cut ? 'text-gray-500/50' : 'icon-default']">
|
||||
<FolderIcon v-if="entry.resolvedType === 'd'" class="size-icon" />
|
||||
<DocumentIcon v-else class="size-icon" />
|
||||
<LinkIcon v-if="entry.type === 'l'" class="w-2 h-2 absolute right-0 bottom-0 text-default" />
|
||||
</div>
|
||||
<button class="z-10 icon-default" v-if="entry.resolvedType === 'd'" @click.stop="toggleShowEntries" @mouseenter="hover = true"
|
||||
@mouseleave="hover = false">
|
||||
<button class="z-10 icon-default" v-if="entry.resolvedType === 'd'" @click.stop="toggleShowEntries"
|
||||
@mouseenter="hover = true" @mouseleave="hover = false">
|
||||
<ChevronDownIcon v-if="!showEntries" class="size-icon" />
|
||||
<ChevronUpIcon v-else class="size-icon" />
|
||||
</button>
|
||||
@ -27,31 +27,21 @@
|
||||
</div>
|
||||
<div class="absolute left-0 top-0 bottom-0 w-full max-w-[50vw]" @mouseup.stop
|
||||
@click.prevent="$emit('directoryViewAction', 'toggleSelected', entry, $event)"
|
||||
@contextmenu.prevent.stop="$emit('browserAction', 'contextMenu', entry, $event)"
|
||||
@dblclick="doubleClickCallback" @mouseenter="hover = true" @mouseleave="hover = false"
|
||||
ref="selectIntersectElement" />
|
||||
</td>
|
||||
<td v-if="settings?.directoryView?.cols?.mode" class="font-mono">{{ entry.modeStr
|
||||
}}</td>
|
||||
<td v-if="settings?.directoryView?.cols?.owner">{{ entry.owner }}
|
||||
</td>
|
||||
<td v-if="settings?.directoryView?.cols?.group">{{ entry.group }}
|
||||
</td>
|
||||
<td v-if="settings?.directoryView?.cols?.size" class="font-mono text-right">{{
|
||||
entry.sizeHuman
|
||||
}}</td>
|
||||
<td v-if="settings?.directoryView?.cols?.btime">{{
|
||||
entry.btimeStr
|
||||
}}</td>
|
||||
<td v-if="settings?.directoryView?.cols?.mtime">{{
|
||||
entry.mtimeStr
|
||||
}}</td>
|
||||
<td v-if="settings?.directoryView?.cols?.atime">{{
|
||||
entry.atimeStr
|
||||
}}</td>
|
||||
<td v-if="settings?.directoryView?.cols?.mode" class="font-mono">{{ entry.modeStr }}</td>
|
||||
<td v-if="settings?.directoryView?.cols?.owner">{{ entry.owner }}</td>
|
||||
<td v-if="settings?.directoryView?.cols?.group">{{ entry.group }} </td>
|
||||
<td v-if="settings?.directoryView?.cols?.size" class="font-mono text-right">{{ entry.sizeHuman }}</td>
|
||||
<td v-if="settings?.directoryView?.cols?.btime">{{ entry.btimeStr }}</td>
|
||||
<td v-if="settings?.directoryView?.cols?.mtime">{{ entry.mtimeStr }}</td>
|
||||
<td v-if="settings?.directoryView?.cols?.atime">{{ entry.atimeStr }}</td>
|
||||
</tr>
|
||||
<component :is="DirectoryEntryList" v-if="entry.resolvedType === 'd' && showEntries" :host="host" :path="entry.path"
|
||||
:isChild="true" :sortCallback="inheritedSortCallback" :searchFilterRegExp="searchFilterRegExp"
|
||||
@startProcessing="(...args) => $emit('startProcessing', ...args)"
|
||||
<component :is="DirectoryEntryList" v-if="entry.resolvedType === 'd' && showEntries" :host="host"
|
||||
:path="entry.path" :isChild="true" :sortCallback="inheritedSortCallback"
|
||||
:searchFilterRegExp="searchFilterRegExp" @startProcessing="(...args) => $emit('startProcessing', ...args)"
|
||||
@stopProcessing="(...args) => $emit('stopProcessing', ...args)" @cancelShowEntries="showEntries = false"
|
||||
ref="directoryEntryListRef" :level="level + 1" :selectedCount="selectedCount"
|
||||
@browserAction="(...args) => $emit('browserAction', ...args)"
|
||||
@ -59,8 +49,10 @@
|
||||
</template>
|
||||
<div v-else class="select-none dir-entry flex flex-col items-center overflow-hidden dir-entry-width p-2"
|
||||
:class="{ '!bg-red-600/10': hover && !entry.selected, '!bg-red-600/20': hover && entry.selected, 'dir-entry-selected': entry.selected, '!border-t-transparent': suppressBorderT, '!border-b-transparent': suppressBorderB, '!border-l-transparent': suppressBorderL, '!border-r-transparent': suppressBorderR }">
|
||||
<div class="w-full" @dblclick="doubleClickCallback" @click.prevent="$emit('directoryViewAction', 'toggleSelected', entry, $event)"
|
||||
@mouseup.stop @mouseenter="hover = true" @mouseleave="hover = false" ref="selectIntersectElement">
|
||||
<div class="w-full" @dblclick="doubleClickCallback"
|
||||
@click.prevent="$emit('directoryViewAction', 'toggleSelected', entry, $event)" @mouseup.stop
|
||||
@contextmenu.prevent.stop="$emit('browserAction', 'contextMenu', entry, $event)"
|
||||
@mouseenter="hover = true" @mouseleave="hover = false" ref="selectIntersectElement">
|
||||
<div class="relative w-full" :class="[entry.cut ? 'text-gray-500/50' : 'icon-default']">
|
||||
<FolderIcon v-if="entry.resolvedType === 'd'" class="w-full h-auto" />
|
||||
<DocumentIcon v-else class="w-full h-auto" />
|
||||
|
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<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" @contextmenu.prevent="$emit('browserAction', 'contextMenu', { host, path, name: `Current directory (${path.split('/').pop() || '/'})` }, $event)">
|
||||
<Table :key="host + path" v-if="settings.directoryView?.view === 'list'" emptyText="No entries." noHeader stickyHeaders
|
||||
noShrink noShrinkHeight="h-full">
|
||||
<template #thead>
|
||||
|
@ -16,20 +16,19 @@ If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<ModalPopup :showModal="show" :headerText="entry?.nameHTML" @apply="apply"
|
||||
@cancel="$emit('hide')">
|
||||
<ModalPopup :showModal="show" :headerText="entry?.nameHTML ?? entry?.name" @apply="apply" @cancel="$emit('hide')">
|
||||
<div class="flex flex-col space-y-content items-start">
|
||||
<FileModeMatrix v-model="mode" />
|
||||
<div>
|
||||
<label class="block text-sm font-medium">Owner</label>
|
||||
<select class="input-textlike" v-model="owner">
|
||||
<option v-for="user in users" :value="user.user">{{ user.pretty }}</option>
|
||||
<option v-for="user in users" :key="user.pretty" :value="user.user">{{ user.pretty }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium">Group</label>
|
||||
<select class="input-textlike" v-model="group">
|
||||
<option v-for="group in groups" :value="group.group">{{ group.pretty }}</option>
|
||||
<option v-for="group in groups" :key="group.pretty" :value="group.group">{{ group.pretty }}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -19,9 +19,10 @@ If not, see <https://www.gnu.org/licenses/>.
|
||||
<TransitionRoot as="div" class="fixed inset-0 z-10 overflow-visible" :show="showModal">
|
||||
<TransitionChild as="template" enter="ease-out duration-500" enter-from="opacity-0" enter-to="opacity-100"
|
||||
leave="ease-in duration-500" leave-from="opacity-100" leave-to="opacity-0">
|
||||
<div class="fixed z-10 inset-0 bg-neutral-500/75 dark:bg-black/50 transition-opacity" />
|
||||
<div class="fixed z-10 inset-0 bg-neutral-500/75 dark:bg-black/50 transition-opacity pointer" />
|
||||
</TransitionChild>
|
||||
<div @click.self="$emit('close')" class="fixed z-10 inset-0 overflow-hidden flex items-end sm:items-center justify-center px-4 pb-20 sm:p-0">
|
||||
<div @click.self="$emit('close')"
|
||||
class="fixed z-10 inset-0 overflow-hidden flex items-end sm:items-center justify-center px-4 pb-20 sm:p-0">
|
||||
<TransitionChild as="template" enter="ease-out duration-300"
|
||||
enter-from="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-90"
|
||||
enter-to="opacity-100 translate-y-0 sm:scale-100" leave="ease-in duration-100"
|
||||
|
@ -18,7 +18,7 @@ export async function getUsers() {
|
||||
return;
|
||||
users.push({ user: record.replace(/^[^\\]+\\/, ""), domain: true, pretty: record.replace(/^[^\\]+\\/, "") + " (domain)" });
|
||||
})
|
||||
} catch {}
|
||||
} catch { }
|
||||
users.sort((a, b) => a.pretty.localeCompare(b.pretty));
|
||||
return users;
|
||||
}
|
||||
|
@ -93,6 +93,7 @@
|
||||
</template>
|
||||
</ModalPopup>
|
||||
<FilePermissions :show="filePermissions.show" @hide="filePermissions.close" :entry="filePermissions.entry" />
|
||||
<ContextMenu @browserAction="handleAction" :show="contextMenu.show" @hide="contextMenu.close" :entry="contextMenu.entry" :event="contextMenu.event" />
|
||||
<Teleport to="#footer-buttons">
|
||||
<IconToggle v-model="darkMode" v-slot="{ value }">
|
||||
<MoonIcon v-if="value" class="size-icon icon-default" />
|
||||
@ -120,6 +121,7 @@ import IconToggle from '../components/IconToggle.vue';
|
||||
import ModalPopup from '../components/ModalPopup.vue';
|
||||
import { fileDownload } from '@45drives/cockpit-helpers';
|
||||
import FilePermissions from '../components/FilePermissions.vue';
|
||||
import ContextMenu from '../components/ContextMenu.vue';
|
||||
|
||||
const encodePartial = (string) =>
|
||||
encodeURIComponent(string)
|
||||
@ -200,6 +202,25 @@ export default {
|
||||
filePermissions.resetTimeoutHandle = setTimeout(() => filePermissions.resetTimeoutHandle = filePermissions.entry = null, 500);
|
||||
},
|
||||
});
|
||||
const contextMenu = reactive({
|
||||
show: false,
|
||||
entry: null,
|
||||
event: null,
|
||||
resetTimeoutHandle: null,
|
||||
open: (entry, event) => {
|
||||
clearTimeout(contextMenu.resetTimeoutHandle);
|
||||
contextMenu.entry = entry;
|
||||
contextMenu.event = event;
|
||||
contextMenu.show = true;
|
||||
},
|
||||
close: () => {
|
||||
contextMenu.show = false;
|
||||
contextMenu.resetTimeoutHandle = setTimeout(() => {
|
||||
contextMenu.resetTimeoutHandle = contextMenu.entry = null;
|
||||
contextMenu.resetTimeoutHandle = contextMenu.event = null;
|
||||
}, 500);
|
||||
},
|
||||
});
|
||||
|
||||
const cd = ({ path, host }) => {
|
||||
const newHost = host ?? (pathHistory.current().host);
|
||||
@ -230,14 +251,6 @@ export default {
|
||||
console.log('download', `${host}:${path}`);
|
||||
}
|
||||
|
||||
const openFilePrompt = (entry) => {
|
||||
openFilePromptModal.open(entry);
|
||||
}
|
||||
|
||||
const openFilePermissions = (entry) => {
|
||||
filePermissions.open(entry);
|
||||
}
|
||||
|
||||
const getSelected = () => directoryViewRef.value?.getSelected?.() ?? [];
|
||||
|
||||
const handleAction = (action, ...args) => {
|
||||
@ -249,14 +262,26 @@ export default {
|
||||
openEditor(...args);
|
||||
break;
|
||||
case 'editPermissions':
|
||||
openFilePermissions(...args);
|
||||
filePermissions.open(...args);
|
||||
break;
|
||||
case 'openFilePrompt':
|
||||
openFilePrompt(...args);
|
||||
openFilePromptModal.open(...args);
|
||||
break;
|
||||
case 'download':
|
||||
download(...args);
|
||||
break;
|
||||
case 'contextMenu':
|
||||
contextMenu.open(...args);
|
||||
break;
|
||||
case 'back':
|
||||
back();
|
||||
break;
|
||||
case 'forward':
|
||||
forward();
|
||||
break;
|
||||
case 'up':
|
||||
up();
|
||||
break;
|
||||
default:
|
||||
console.error('Unknown browserAction:', action, args);
|
||||
break;
|
||||
@ -296,14 +321,13 @@ export default {
|
||||
forwardHistoryDropdown,
|
||||
openFilePromptModal,
|
||||
filePermissions,
|
||||
contextMenu,
|
||||
cd,
|
||||
back,
|
||||
forward,
|
||||
up,
|
||||
openEditor,
|
||||
download,
|
||||
openFilePrompt,
|
||||
openFilePermissions,
|
||||
getSelected,
|
||||
handleAction,
|
||||
}
|
||||
@ -326,6 +350,7 @@ export default {
|
||||
ViewGridIcon,
|
||||
ModalPopup,
|
||||
FilePermissions,
|
||||
ContextMenu,
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
Loading…
x
Reference in New Issue
Block a user