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:
Azri Kahar
2021-11-25 06:09:29 +08:00
committed by GitHub
parent 86155d4683
commit e88ed96e5c
9 changed files with 81 additions and 71 deletions

View File

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

View 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);
};
}

View File

@@ -0,0 +1,4 @@
import ContextMenu from './context-menu';
export { ContextMenu };
export default ContextMenu;

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

View File

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

View File

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

View File

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

View File

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

View File

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