add context menu

This commit is contained in:
joshuaboud 2022-06-17 17:34:09 -03:00
parent b96902391e
commit 116a422320
No known key found for this signature in database
GPG Key ID: 17EFB59E2A8BF50E
7 changed files with 149 additions and 46 deletions

View 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>

View File

@ -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" />

View File

@ -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>

View File

@ -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>

View File

@ -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"

View File

@ -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;
}

View File

@ -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>