mirror of
https://github.com/directus/directus.git
synced 2026-04-25 03:00:53 -04:00
Add context menu directive (#9334)
* Add context menu directive * require specifying a ref name * fix empty context menu @azrikahar * listen contextmenu even if no menu attached In some cases we have an area where would like to not open the browser context menu. For example, an area where the context menu does not have any options because of conditions like permissions * only 'Show hidden...' if hidden collections exists * fix arrow chunk overlapping items on v-menu Co-authored-by: Jose Varela <joselcvarela@gmail.com> Co-authored-by: rijkvanzanten <rijkvanzanten@me.com>
This commit is contained in:
@@ -326,6 +326,7 @@ body {
|
||||
z-index: 1;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
overflow: hidden;
|
||||
border-radius: 2px;
|
||||
box-shadow: none;
|
||||
}
|
||||
@@ -349,7 +350,7 @@ body {
|
||||
bottom: -6px;
|
||||
|
||||
&::after {
|
||||
bottom: 2px;
|
||||
bottom: 3px;
|
||||
box-shadow: 2px 2px 4px -2px rgba(var(--card-shadow-color), 0.2);
|
||||
}
|
||||
}
|
||||
@@ -358,7 +359,7 @@ body {
|
||||
top: -6px;
|
||||
|
||||
&::after {
|
||||
top: 2px;
|
||||
top: 3px;
|
||||
box-shadow: -2px -2px 4px -2px rgba(var(--card-shadow-color), 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
37
app/src/directives/context-menu/context-menu.ts
Normal file
37
app/src/directives/context-menu/context-menu.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { Directive, DirectiveBinding } from 'vue';
|
||||
|
||||
function mounted(element: HTMLElement, binding: DirectiveBinding): void {
|
||||
const contextMenu = binding.instance?.$refs[binding.value];
|
||||
|
||||
element.addEventListener('contextmenu', activateContextMenu(contextMenu));
|
||||
element.addEventListener('focusout', deactivateContextMenu(contextMenu));
|
||||
}
|
||||
|
||||
function unmounted(element: HTMLElement, binding: DirectiveBinding): void {
|
||||
const contextMenu = binding.instance?.$refs[binding.value];
|
||||
|
||||
element.removeEventListener('contextmenu', activateContextMenu(contextMenu));
|
||||
element.removeEventListener('focusout', deactivateContextMenu(contextMenu));
|
||||
}
|
||||
|
||||
const ContextMenu: Directive = {
|
||||
mounted,
|
||||
unmounted,
|
||||
};
|
||||
|
||||
export default ContextMenu;
|
||||
|
||||
function activateContextMenu(contextMenu: any) {
|
||||
return (e: Event) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
if (contextMenu) contextMenu.activate(e);
|
||||
};
|
||||
}
|
||||
|
||||
function deactivateContextMenu(contextMenu: any) {
|
||||
return (e: Event) => {
|
||||
if (contextMenu) contextMenu.deactivate(e);
|
||||
};
|
||||
}
|
||||
4
app/src/directives/context-menu/index.ts
Normal file
4
app/src/directives/context-menu/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import ContextMenu from './context-menu';
|
||||
|
||||
export { ContextMenu };
|
||||
export default ContextMenu;
|
||||
18
app/src/directives/context-menu/readme.md
Normal file
18
app/src/directives/context-menu/readme.md
Normal file
@@ -0,0 +1,18 @@
|
||||
# Context Menu
|
||||
|
||||
## Usage
|
||||
|
||||
This allows the element to open a context menu with the specified ref. It adds `contextmenu` event (right click) to
|
||||
activate/open the context menu, and `focusout` event to deactivate/close it. .
|
||||
|
||||
```html
|
||||
<element v-context-menu="'contextMenu'"></element>
|
||||
```
|
||||
|
||||
Somewhere in the same component:
|
||||
|
||||
```html
|
||||
<v-menu ref="contextMenu">
|
||||
<!-- menu items here -->
|
||||
</v-menu>
|
||||
```
|
||||
@@ -1,12 +1,14 @@
|
||||
import { App } from 'vue';
|
||||
import ClickOutside from './click-outside/click-outside';
|
||||
import ContextMenu from './context-menu/context-menu';
|
||||
import Focus from './focus/focus';
|
||||
import Tooltip from './tooltip/tooltip';
|
||||
import Markdown from './markdown';
|
||||
|
||||
export function registerDirectives(app: App): void {
|
||||
app.directive('click-outside', ClickOutside);
|
||||
app.directive('context-menu', ContextMenu);
|
||||
app.directive('focus', Focus);
|
||||
app.directive('tooltip', Tooltip);
|
||||
app.directive('click-outside', ClickOutside);
|
||||
app.directive('md', Markdown);
|
||||
}
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
<template>
|
||||
<v-list-item
|
||||
v-context-menu="'contextMenu'"
|
||||
:to="`/content/${bookmark.collection}?bookmark=${bookmark.id}`"
|
||||
query
|
||||
class="bookmark"
|
||||
clickable
|
||||
@contextmenu.prevent.stop="activateContextMenu"
|
||||
@focusout="deactivateContextMenu"
|
||||
>
|
||||
<v-list-item-icon><v-icon name="bookmark_outline" /></v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
@@ -83,7 +82,6 @@ export default defineComponent({
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
|
||||
const contextMenu = ref();
|
||||
const userStore = useUserStore();
|
||||
const presetsStore = usePresetsStore();
|
||||
|
||||
@@ -94,7 +92,6 @@ export default defineComponent({
|
||||
|
||||
return {
|
||||
t,
|
||||
contextMenu,
|
||||
isMine,
|
||||
renameActive,
|
||||
renameValue,
|
||||
@@ -104,8 +101,6 @@ export default defineComponent({
|
||||
deleteValue,
|
||||
deleteSave,
|
||||
deleteSaving,
|
||||
activateContextMenu,
|
||||
deactivateContextMenu,
|
||||
};
|
||||
|
||||
function useRenameBookmark() {
|
||||
@@ -163,14 +158,6 @@ export default defineComponent({
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function activateContextMenu(event: PointerEvent) {
|
||||
contextMenu.value.activate(event);
|
||||
}
|
||||
|
||||
function deactivateContextMenu() {
|
||||
contextMenu.value.deactivate();
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
<template>
|
||||
<v-list-group
|
||||
v-if="isGroup && matchesSearch"
|
||||
v-context-menu="'contextMenu'"
|
||||
:to="to"
|
||||
scope="content-navigation"
|
||||
:value="collection.collection"
|
||||
query
|
||||
:arrow-placement="collection.meta?.collapse === 'locked' ? false : 'after'"
|
||||
@contextmenu.prevent.stop="activateContextMenu"
|
||||
@focusout="deactivateContextMenu"
|
||||
>
|
||||
<template #activator>
|
||||
<navigation-item-content
|
||||
@@ -29,12 +28,11 @@
|
||||
|
||||
<v-list-item
|
||||
v-else-if="matchesSearch"
|
||||
v-context-menu="hasContextMenu ? 'contextMenu' : null"
|
||||
:to="to"
|
||||
:value="collection.collection"
|
||||
:class="{ hidden: collection.meta?.hidden }"
|
||||
query
|
||||
@contextmenu.prevent.stop="activateContextMenu"
|
||||
@focusout="deactivateContextMenu"
|
||||
>
|
||||
<navigation-item-content
|
||||
:search="search"
|
||||
@@ -44,7 +42,7 @@
|
||||
/>
|
||||
</v-list-item>
|
||||
|
||||
<v-menu ref="contextMenu" show-arrow placement="bottom-start">
|
||||
<v-menu v-if="hasContextMenu" ref="contextMenu" show-arrow placement="bottom-start">
|
||||
<v-list>
|
||||
<v-list-item v-if="hasArchive" clickable :to="`/content/${collection.collection}?archive`" exact query>
|
||||
<v-list-item-icon>
|
||||
@@ -67,7 +65,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, PropType, computed, ref } from 'vue';
|
||||
import { defineComponent, PropType, computed } from 'vue';
|
||||
import { Collection } from '@/types';
|
||||
import { Preset } from '@directus/shared/types';
|
||||
import { useUserStore, useCollectionsStore, usePresetsStore } from '@/stores';
|
||||
@@ -104,8 +102,6 @@ export default defineComponent({
|
||||
|
||||
const childBookmarks = computed(() => getChildBookmarks(props.collection));
|
||||
|
||||
const contextMenu = ref();
|
||||
|
||||
const hasArchive = computed(
|
||||
() => props.collection.meta?.archive_field && props.collection.meta?.archive_app_filter
|
||||
);
|
||||
@@ -141,18 +137,18 @@ export default defineComponent({
|
||||
}
|
||||
});
|
||||
|
||||
const hasContextMenu = computed(() => hasArchive.value || isAdmin);
|
||||
|
||||
return {
|
||||
childCollections,
|
||||
childBookmarks,
|
||||
isGroup,
|
||||
to,
|
||||
matchesSearch,
|
||||
contextMenu,
|
||||
activateContextMenu,
|
||||
deactivateContextMenu,
|
||||
isAdmin,
|
||||
t,
|
||||
hasArchive,
|
||||
hasContextMenu,
|
||||
};
|
||||
|
||||
function getChildCollections(collection: Collection) {
|
||||
@@ -170,16 +166,6 @@ export default defineComponent({
|
||||
function getChildBookmarks(collection: Collection) {
|
||||
return presetsStore.bookmarks.filter((bookmark) => bookmark.collection === collection.collection);
|
||||
}
|
||||
|
||||
function activateContextMenu(event: PointerEvent) {
|
||||
if (hasArchive.value || props.collection.schema) {
|
||||
contextMenu.value.activate(event);
|
||||
}
|
||||
}
|
||||
|
||||
function deactivateContextMenu() {
|
||||
contextMenu.value.deactivate();
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -6,14 +6,13 @@
|
||||
|
||||
<v-list
|
||||
v-model="activeGroups"
|
||||
v-context-menu="'contextMenu'"
|
||||
scope="content-navigation"
|
||||
class="content-navigation"
|
||||
tabindex="-1"
|
||||
nav
|
||||
:mandatory="false"
|
||||
:dense="dense"
|
||||
@contextmenu.prevent.stop="activateContextMenu"
|
||||
@focusout="deactivateContextMenu"
|
||||
>
|
||||
<navigation-item
|
||||
v-for="collection in rootItems"
|
||||
@@ -23,7 +22,7 @@
|
||||
:search="search"
|
||||
/>
|
||||
|
||||
<v-menu ref="contextMenu" show-arrow placement="bottom-start">
|
||||
<v-menu v-if="hasHiddenCollections" ref="contextMenu" show-arrow placement="bottom-start">
|
||||
<v-list-item clickable @click="showHidden = !showHidden">
|
||||
<v-list-item-icon>
|
||||
<v-icon :name="showHidden ? 'visibility_off' : 'visibility'" />
|
||||
@@ -62,8 +61,6 @@ export default defineComponent({
|
||||
|
||||
const collectionsStore = useCollectionsStore();
|
||||
|
||||
const contextMenu = ref();
|
||||
|
||||
const rootItems = computed(() => {
|
||||
const shownCollections = showHidden.value ? collectionsStore.allCollections : collectionsStore.visibleCollections;
|
||||
return orderBy(
|
||||
@@ -76,6 +73,9 @@ export default defineComponent({
|
||||
|
||||
const dense = computed(() => collectionsStore.visibleCollections.length > 5);
|
||||
const showSearch = computed(() => collectionsStore.visibleCollections.length > 20);
|
||||
const hasHiddenCollections = computed(
|
||||
() => collectionsStore.allCollections.length > collectionsStore.visibleCollections.length
|
||||
);
|
||||
|
||||
return {
|
||||
t,
|
||||
@@ -83,20 +83,10 @@ export default defineComponent({
|
||||
showHidden,
|
||||
rootItems,
|
||||
dense,
|
||||
activateContextMenu,
|
||||
deactivateContextMenu,
|
||||
contextMenu,
|
||||
search,
|
||||
showSearch,
|
||||
hasHiddenCollections,
|
||||
};
|
||||
|
||||
function activateContextMenu(event: PointerEvent) {
|
||||
contextMenu.value.activate(event);
|
||||
}
|
||||
|
||||
function deactivateContextMenu() {
|
||||
contextMenu.value.deactivate();
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -2,10 +2,9 @@
|
||||
<div>
|
||||
<v-list-item
|
||||
v-if="folder.children === undefined"
|
||||
v-context-menu="'contextMenu'"
|
||||
:to="`/files/folders/${folder.id}`"
|
||||
:active="currentFolder === folder.id"
|
||||
@contextmenu.prevent.stop="activateContextMenu"
|
||||
@focusout="deactivateContextMenu"
|
||||
>
|
||||
<v-list-item-icon><v-icon name="folder" /></v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
@@ -15,13 +14,12 @@
|
||||
|
||||
<v-list-group
|
||||
v-else
|
||||
v-context-menu="'contextMenu'"
|
||||
:to="`/files/folders/${folder.id}`"
|
||||
:active="currentFolder === folder.id"
|
||||
:value="folder.id"
|
||||
scope="files-navigation"
|
||||
disable-groupable-parent
|
||||
@contextmenu.prevent.stop="activateContextMenu"
|
||||
@focusout="deactivateContextMenu"
|
||||
>
|
||||
<template #activator>
|
||||
<v-list-item-icon>
|
||||
@@ -146,8 +144,6 @@ export default defineComponent({
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const contextMenu = ref();
|
||||
|
||||
const { renameActive, renameValue, renameSave, renameSaving } = useRenameFolder();
|
||||
const { moveActive, moveValue, moveSave, moveSaving } = useMoveFolder();
|
||||
const { deleteActive, deleteSave, deleteSaving } = useDeleteFolder();
|
||||
@@ -167,9 +163,6 @@ export default defineComponent({
|
||||
deleteActive,
|
||||
deleteSave,
|
||||
deleteSaving,
|
||||
contextMenu,
|
||||
activateContextMenu,
|
||||
deactivateContextMenu,
|
||||
};
|
||||
|
||||
function useRenameFolder() {
|
||||
@@ -290,14 +283,6 @@ export default defineComponent({
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function activateContextMenu(event: PointerEvent) {
|
||||
contextMenu.value.activate(event);
|
||||
}
|
||||
|
||||
function deactivateContextMenu() {
|
||||
contextMenu.value.deactivate();
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user