返回 slidev
ContextMenu.vue
根目录 / packages / client / internals / ContextMenu.vue
1 <script setup lang="ts">
2 import { onClickOutside, useElementBounding, useEventListener, useWindowFocus } from '@vueuse/core'
3 import { computed, ref, watch } from 'vue'
4 import { useDynamicSlideInfo } from '../composables/useSlideInfo'
5 import { configs } from '../env'
6 import { closeContextMenu, currentContextMenu } from '../logic/contextMenu'
7 import { windowSize } from '../state'
8
9 const container = ref<HTMLElement>()
10
11 onClickOutside(container, closeContextMenu)
12 useEventListener(document, 'mousedown', (ev) => {
13 if (ev.buttons & 2)
14 closeContextMenu()
15 }, {
16 passive: true,
17 capture: true,
18 })
19
20 const isExplicitEnabled = computed(() => configs.contextMenu != null)
21
22 const windowFocus = useWindowFocus()
23 watch(windowFocus, (hasFocus) => {
24 if (!hasFocus)
25 closeContextMenu()
26 })
27
28 const firstSlide = useDynamicSlideInfo(1)
29 function disableContextMenu() {
30 const info = firstSlide.info.value
31 if (!info)
32 return
33 firstSlide.update({
34 frontmatter: {
35 contextMenu: false,
36 },
37 })
38 }
39
40 const { width, height } = useElementBounding(container)
41 const left = computed(() => {
42 const x = currentContextMenu.value?.x
43 if (!x)
44 return 0
45 if (x + width.value > windowSize.width.value)
46 return windowSize.width.value - width.value
47 return x
48 })
49 const top = computed(() => {
50 const y = currentContextMenu.value?.y
51 if (!y)
52 return 0
53 if (y + height.value > windowSize.height.value)
54 return windowSize.height.value - height.value
55 return y
56 })
57 </script>
58
59 <template>
60 <div
61 v-if="currentContextMenu"
62 ref="container"
63 :style="`left:${left}px;top:${top}px`"
64 class="slidev-glass-effect fixed z-context-menu w-60 flex flex-wrap justify-items-start p-1 animate-fade-in animate-duration-100 rounded-md shadow overflow-hidden select-none"
65 @contextmenu.prevent=""
66 @click="closeContextMenu"
67 >
68 <template v-for="item, index of currentContextMenu.items.value" :key="index">
69 <div v-if="item === 'separator'" :key="index" class="w-full my1 border-t border-main" />
70 <template v-else-if="item.show ?? true">
71 <div
72 v-if="item.small"
73 class="p-2 w-[40px] h-[40px] inline-block text-center cursor-pointer rounded flex"
74 :class="item.disabled ? `op40` : `hover:bg-active`"
75 :title="(item.label as string)"
76 @click="item.action"
77 >
78 <div v-if="typeof item.icon === 'string'" :class="item.icon" class="text-1.2em ma" />
79 <component :is="item.icon" v-else />
80 </div>
81 <div
82 v-else
83 class="w-full grid grid-cols-[35px_1fr] p-2 pl-0 cursor-pointer rounded"
84 :class="item.disabled ? `op40` : `hover:bg-active`"
85 @click="item.action"
86 >
87 <div class="mx-auto flex">
88 <div v-if="typeof item.icon === 'string'" :class="item.icon" class="text-1.2em ma" />
89 <component :is="item.icon" v-else />
90 </div>
91 <div v-if="typeof item.label === 'string'">
92 {{ item.label }}
93 </div>
94 <component :is="item.label" v-else />
95 </div>
96 </template>
97 </template>
98 <template v-if="!isExplicitEnabled">
99 <div class="w-full my1 border-t border-main" />
100 <div class="w-full text-xs p2">
101 <div class="text-main text-opacity-50!">
102 Hold <kbd class="border px1 py0.5 border-main rounded text-primary">Shift</kbd> and right click to open the native context menu
103 <button
104 v-if="__DEV__"
105 class="underline op50 hover:op100 mt1 block"
106 @click="disableContextMenu()"
107 >
108 Disable custom context menu
109 </button>
110 </div>
111 </div>
112 </template>
113 </div>
114 </template>
115
115 lines Plain Text