vue3-context-menu-plus 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,45 @@
1
+ <!--
2
+ * @Author: eggYolkegg
3
+ * @Date: 2025-12-11 15:41:57
4
+ * @LastEditors: eggYolkegg
5
+ * @LastEditTime: 2025-12-11 16:11:26
6
+ * @Description:
7
+ -->
8
+ # Vue3 Context Menu
9
+
10
+ 一个 Vue 3 右键菜单组件,可以根据不同的组件显示不同的右键菜单。
11
+
12
+ ## 安装
13
+
14
+ ```bash
15
+ npm install vue3-context-menu
16
+ # 或
17
+ yarn add vue3-context-menu
18
+
19
+
20
+
21
+ # 属性 类型 说明 默认值
22
+ # id string | number 菜单项唯一标识 必填
23
+ # label string 显示文本 必填
24
+ # icon string 图标类名 可选
25
+ # disabled boolean 是否禁用 false
26
+ # divider boolean 是否为分隔线 false
27
+ # children MenuItem[] 子菜单项 可选
28
+ #handler Function 点击处理函数 可选
29
+
30
+
31
+ # 基本用法
32
+ # <div v-contextmenu="menus"></div>
33
+
34
+ # 设置z-index
35
+ # <div v-contextmenu:10000="menus"></div>
36
+
37
+ # 使用修饰符
38
+ # <div v-contextmenu.auto-close="menus"></div>
39
+
40
+ # 全局使用
41
+ # import { createApp } from 'vue'
42
+ # import VueContextMenu from 'vue3-context-menu'
43
+
44
+ # const app = createApp(App)
45
+ # app.use(VueContextMenu)
@@ -0,0 +1,349 @@
1
+ 'use strict';
2
+
3
+ Object.defineProperty(exports, '__esModule', { value: true });
4
+
5
+ var vue = require('vue');
6
+
7
+ const _hoisted_1 = { class: "context-menu-content" };
8
+ const _hoisted_2 = {
9
+ key: 0,
10
+ class: "context-menu-divider"
11
+ };
12
+ const _hoisted_3 = ["onClick", "onMouseenter"];
13
+ const _hoisted_4 = {
14
+ key: 0,
15
+ class: "menu-icon"
16
+ };
17
+ const _hoisted_5 = { class: "menu-label" };
18
+ const _hoisted_6 = {
19
+ key: 1,
20
+ class: "menu-arrow"
21
+ };
22
+ const _hoisted_7 = ["onClick"];
23
+ const _hoisted_8 = {
24
+ key: 0,
25
+ class: "menu-icon"
26
+ };
27
+ const _hoisted_9 = { class: "menu-label" };
28
+ var _sfc_main = /*@__PURE__*/ vue.defineComponent({
29
+ __name: 'ContextMenu',
30
+ props: {
31
+ visible: { type: Boolean, required: true },
32
+ x: { type: Number, required: true },
33
+ y: { type: Number, required: true },
34
+ menus: { type: Array, required: true },
35
+ zIndex: { type: Number, required: false, default: 9999 },
36
+ maxWidth: { type: Number, required: false, default: 300 },
37
+ minWidth: { type: Number, required: false, default: 150 }
38
+ },
39
+ emits: ["update:visible", "item-click"],
40
+ setup(__props, { emit: __emit }) {
41
+ const props = __props;
42
+ const emit = __emit;
43
+ const menuRef = vue.ref();
44
+ const subMenuVisible = vue.ref({});
45
+ const subMenuStyle = vue.ref({ top: "0px", left: "0px" });
46
+ // 菜单样式
47
+ const menuStyle = vue.computed(() => ({
48
+ left: `${props.x}px`,
49
+ top: `${props.y}px`,
50
+ zIndex: props.zIndex,
51
+ maxWidth: `${props.maxWidth}px`,
52
+ minWidth: `${props.minWidth}px`,
53
+ }));
54
+ // 当前显示的菜单
55
+ const currentMenus = vue.computed(() => props.menus);
56
+ // 点击菜单项
57
+ const handleClick = (item) => {
58
+ if (item.disabled)
59
+ return;
60
+ if (item.handler) {
61
+ item.handler(menuRef.value, null);
62
+ }
63
+ emit("item-click", item);
64
+ emit("update:visible", false);
65
+ subMenuVisible.value = {};
66
+ };
67
+ // 鼠标进入显示子菜单
68
+ const handleMouseEnter = (item) => {
69
+ if (item.children && item.children.length > 0) {
70
+ subMenuVisible.value = { [item.id]: true };
71
+ // 计算子菜单位置
72
+ if (menuRef.value) {
73
+ const rect = menuRef.value.getBoundingClientRect();
74
+ subMenuStyle.value = {
75
+ top: "0px",
76
+ left: `${rect.width}px`,
77
+ };
78
+ }
79
+ }
80
+ };
81
+ // 点击外部关闭菜单
82
+ const handleClickOutside = (event) => {
83
+ if (menuRef.value && !menuRef.value.contains(event.target)) {
84
+ emit("update:visible", false);
85
+ subMenuVisible.value = {};
86
+ }
87
+ };
88
+ // 监听全局点击
89
+ vue.onMounted(() => {
90
+ document.addEventListener("click", handleClickOutside);
91
+ document.addEventListener("contextmenu", handleClickOutside);
92
+ });
93
+ vue.onUnmounted(() => {
94
+ document.removeEventListener("click", handleClickOutside);
95
+ document.removeEventListener("contextmenu", handleClickOutside);
96
+ });
97
+ // 监听visible变化
98
+ vue.watch(() => props.visible, (newVal) => {
99
+ if (!newVal) {
100
+ subMenuVisible.value = {};
101
+ }
102
+ });
103
+ return (_ctx, _cache) => {
104
+ return (vue.openBlock(), vue.createBlock(vue.Transition, { name: "context-menu-fade" }, {
105
+ default: vue.withCtx(() => [
106
+ (__props.visible)
107
+ ? (vue.openBlock(), vue.createElementBlock("div", {
108
+ key: 0,
109
+ ref_key: "menuRef",
110
+ ref: menuRef,
111
+ class: "vue-context-menu",
112
+ style: vue.normalizeStyle(menuStyle.value),
113
+ onClick: _cache[0] || (_cache[0] = vue.withModifiers(() => { }, ["stop"])),
114
+ onContextmenu: _cache[1] || (_cache[1] = vue.withModifiers(() => { }, ["stop", "prevent"]))
115
+ }, [
116
+ vue.createElementVNode("div", _hoisted_1, [
117
+ (vue.openBlock(true), vue.createElementBlock(vue.Fragment, null, vue.renderList(currentMenus.value, (item) => {
118
+ return (vue.openBlock(), vue.createElementBlock(vue.Fragment, {
119
+ key: item.id
120
+ }, [
121
+ vue.createCommentVNode(" 分隔线 "),
122
+ (item.divider)
123
+ ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_2))
124
+ : (vue.openBlock(), vue.createElementBlock(vue.Fragment, { key: 1 }, [
125
+ vue.createCommentVNode(" 菜单项 "),
126
+ vue.createElementVNode("div", {
127
+ class: vue.normalizeClass(["context-menu-item", {
128
+ disabled: item.disabled,
129
+ 'has-children': item.children && item.children.length > 0,
130
+ }]),
131
+ onClick: ($event) => (handleClick(item)),
132
+ onMouseenter: ($event) => (handleMouseEnter(item))
133
+ }, [
134
+ vue.createCommentVNode(" 图标 "),
135
+ (item.icon)
136
+ ? (vue.openBlock(), vue.createElementBlock("span", _hoisted_4, [
137
+ vue.createElementVNode("i", {
138
+ class: vue.normalizeClass(item.icon)
139
+ }, null, 2 /* CLASS */)
140
+ ]))
141
+ : vue.createCommentVNode("v-if", true),
142
+ vue.createCommentVNode(" 标签 "),
143
+ vue.createElementVNode("span", _hoisted_5, vue.toDisplayString(item.label), 1 /* TEXT */),
144
+ vue.createCommentVNode(" 子菜单箭头 "),
145
+ (item.children)
146
+ ? (vue.openBlock(), vue.createElementBlock("span", _hoisted_6, "▶"))
147
+ : vue.createCommentVNode("v-if", true),
148
+ vue.createCommentVNode(" 子菜单 "),
149
+ (item.children && subMenuVisible.value[item.id])
150
+ ? (vue.openBlock(), vue.createElementBlock("div", {
151
+ key: 2,
152
+ class: "context-submenu",
153
+ style: vue.normalizeStyle(subMenuStyle.value)
154
+ }, [
155
+ (vue.openBlock(true), vue.createElementBlock(vue.Fragment, null, vue.renderList(item.children, (child) => {
156
+ return (vue.openBlock(), vue.createElementBlock("div", {
157
+ key: child.id,
158
+ class: vue.normalizeClass(["context-menu-item", { disabled: child.disabled }]),
159
+ onClick: vue.withModifiers(($event) => (handleClick(child)), ["stop"])
160
+ }, [
161
+ (child.icon)
162
+ ? (vue.openBlock(), vue.createElementBlock("span", _hoisted_8, [
163
+ vue.createElementVNode("i", {
164
+ class: vue.normalizeClass(child.icon)
165
+ }, null, 2 /* CLASS */)
166
+ ]))
167
+ : vue.createCommentVNode("v-if", true),
168
+ vue.createElementVNode("span", _hoisted_9, vue.toDisplayString(child.label), 1 /* TEXT */)
169
+ ], 10 /* CLASS, PROPS */, _hoisted_7));
170
+ }), 128 /* KEYED_FRAGMENT */))
171
+ ], 4 /* STYLE */))
172
+ : vue.createCommentVNode("v-if", true)
173
+ ], 42 /* CLASS, PROPS, NEED_HYDRATION */, _hoisted_3)
174
+ ], 2112 /* STABLE_FRAGMENT, DEV_ROOT_FRAGMENT */))
175
+ ], 64 /* STABLE_FRAGMENT */));
176
+ }), 128 /* KEYED_FRAGMENT */))
177
+ ])
178
+ ], 36 /* STYLE, NEED_HYDRATION */))
179
+ : vue.createCommentVNode("v-if", true)
180
+ ]),
181
+ _: 1 /* STABLE */
182
+ }));
183
+ };
184
+ }
185
+ });
186
+
187
+ function styleInject(css, ref) {
188
+ if ( ref === void 0 ) ref = {};
189
+ var insertAt = ref.insertAt;
190
+
191
+ if (!css || typeof document === 'undefined') { return; }
192
+
193
+ var head = document.head || document.getElementsByTagName('head')[0];
194
+ var style = document.createElement('style');
195
+ style.type = 'text/css';
196
+
197
+ if (insertAt === 'top') {
198
+ if (head.firstChild) {
199
+ head.insertBefore(style, head.firstChild);
200
+ } else {
201
+ head.appendChild(style);
202
+ }
203
+ } else {
204
+ head.appendChild(style);
205
+ }
206
+
207
+ if (style.styleSheet) {
208
+ style.styleSheet.cssText = css;
209
+ } else {
210
+ style.appendChild(document.createTextNode(css));
211
+ }
212
+ }
213
+
214
+ var css_248z = "\n.vue-context-menu[data-v-cbb365ae] {\r\n position: fixed;\r\n background: white;\r\n border: 1px solid #dcdfe6;\r\n border-radius: 4px;\r\n box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);\r\n padding: 5px 0;\r\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif;\r\n font-size: 14px;\r\n color: #606266;\n}\n.context-menu-content[data-v-cbb365ae] {\r\n width: 100%;\n}\n.context-menu-item[data-v-cbb365ae] {\r\n padding: 8px 20px;\r\n cursor: pointer;\r\n display: flex;\r\n align-items: center;\r\n position: relative;\r\n transition: background-color 0.2s;\n}\n.context-menu-item[data-v-cbb365ae]:hover:not(.disabled) {\r\n background-color: #f5f7fa;\n}\n.context-menu-item.disabled[data-v-cbb365ae] {\r\n color: #c0c4cc;\r\n cursor: not-allowed;\n}\n.context-menu-divider[data-v-cbb365ae] {\r\n height: 1px;\r\n background-color: #ebeef5;\r\n margin: 5px 0;\n}\n.menu-icon[data-v-cbb365ae] {\r\n margin-right: 8px;\r\n font-size: 14px;\r\n width: 14px;\r\n display: inline-flex;\r\n justify-content: center;\n}\n.menu-label[data-v-cbb365ae] {\r\n flex: 1;\r\n white-space: nowrap;\r\n overflow: hidden;\r\n text-overflow: ellipsis;\n}\n.menu-arrow[data-v-cbb365ae] {\r\n margin-left: 8px;\r\n font-size: 12px;\r\n color: #909399;\n}\n.context-submenu[data-v-cbb365ae] {\r\n position: absolute;\r\n background: white;\r\n border: 1px solid #dcdfe6;\r\n border-radius: 4px;\r\n box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);\r\n min-width: 150px;\r\n z-index: 10000;\n}\r\n\r\n/* 动画效果 */\n.context-menu-fade-enter-active[data-v-cbb365ae],\r\n.context-menu-fade-leave-active[data-v-cbb365ae] {\r\n transition: opacity 0.2s, transform 0.2s;\n}\n.context-menu-fade-enter-from[data-v-cbb365ae],\r\n.context-menu-fade-leave-to[data-v-cbb365ae] {\r\n opacity: 0;\r\n transform: scale(0.95);\n}\r\n";
215
+ styleInject(css_248z);
216
+
217
+ var _export_sfc = (sfc, props) => {
218
+ const target = sfc.__vccOpts || sfc;
219
+ for (const [key, val] of props) {
220
+ target[key] = val;
221
+ }
222
+ return target;
223
+ };
224
+
225
+ var ContextMenu = /* @__PURE__ */ _export_sfc(_sfc_main, [["__scopeId", "data-v-cbb365ae"]]);
226
+
227
+ // 全局菜单实例
228
+ let menuInstance = null;
229
+ let container = null;
230
+ // 获取组件名
231
+ function getComponentName(el) {
232
+ // 优先从data-component获取
233
+ const dataComponent = el.getAttribute("data-component");
234
+ if (dataComponent)
235
+ return dataComponent;
236
+ // 从class中查找组件类名
237
+ const classList = el.classList;
238
+ for (let i = 0; i < classList.length; i++) {
239
+ const className = classList[i];
240
+ if (className.startsWith("component-")) {
241
+ return className.replace("component-", "");
242
+ }
243
+ }
244
+ // 默认返回标签名
245
+ return el.tagName.toLowerCase();
246
+ }
247
+ // 创建菜单实例
248
+ function createMenuInstance() {
249
+ if (menuInstance)
250
+ return;
251
+ container = document.createElement("div");
252
+ document.body.appendChild(container);
253
+ menuInstance = vue.createApp({
254
+ data() {
255
+ return {
256
+ visible: false,
257
+ x: 0,
258
+ y: 0,
259
+ menus: [],
260
+ };
261
+ },
262
+ render() {
263
+ return vue.h(ContextMenu, {
264
+ visible: this.visible,
265
+ x: this.x,
266
+ y: this.y,
267
+ menus: this.menus,
268
+ "onUpdate:visible": (val) => {
269
+ this.visible = val;
270
+ },
271
+ onItemClick: (item) => {
272
+ // 菜单项点击处理
273
+ },
274
+ });
275
+ },
276
+ });
277
+ menuInstance.mount(container);
278
+ }
279
+ // 显示菜单
280
+ function showContextMenu(event, menus, options) {
281
+ if (!menuInstance)
282
+ createMenuInstance();
283
+ const vm = menuInstance._instance.proxy;
284
+ vm.x = event.clientX;
285
+ vm.y = event.clientY;
286
+ vm.menus = menus;
287
+ vm.visible = true;
288
+ // 防止菜单超出屏幕
289
+ requestAnimationFrame(() => {
290
+ const menuEl = container === null || container === void 0 ? void 0 : container.querySelector(".vue-context-menu");
291
+ if (menuEl) {
292
+ const rect = menuEl.getBoundingClientRect();
293
+ const viewportWidth = window.innerWidth;
294
+ const viewportHeight = window.innerHeight;
295
+ if (rect.right > viewportWidth) {
296
+ vm.x = viewportWidth - rect.width - 5;
297
+ }
298
+ if (rect.bottom > viewportHeight) {
299
+ vm.y = viewportHeight - rect.height - 5;
300
+ }
301
+ }
302
+ });
303
+ event.preventDefault();
304
+ event.stopPropagation();
305
+ }
306
+ // 获取对应组件的菜单
307
+ function getMenusForComponent(componentName, options) {
308
+ if (typeof options.menus === "function") {
309
+ return options.menus(componentName);
310
+ }
311
+ else if (Array.isArray(options.menus)) {
312
+ return options.menus;
313
+ }
314
+ else {
315
+ return options.menus[componentName] || [];
316
+ }
317
+ }
318
+ // 指令定义
319
+ const contextmenuDirective = {
320
+ mounted(el, binding) {
321
+ const options = {
322
+ menus: binding.value,
323
+ zIndex: binding.arg ? parseInt(binding.arg) : 9999,
324
+ ...binding.modifiers,
325
+ };
326
+ el.addEventListener("contextmenu", (event) => {
327
+ const componentName = getComponentName(el);
328
+ const menus = getMenusForComponent(componentName, options);
329
+ if (menus.length > 0) {
330
+ showContextMenu(event, menus);
331
+ }
332
+ });
333
+ },
334
+ };
335
+
336
+ // 默认导出插件
337
+ const VueContextMenu = {
338
+ install(app) {
339
+ // 注册全局指令
340
+ app.directive("contextmenu", contextmenuDirective);
341
+ // 注册全局组件(可选)
342
+ app.component("ContextMenu", ContextMenu);
343
+ },
344
+ };
345
+
346
+ exports.ContextMenu = ContextMenu;
347
+ exports.contextmenuDirective = contextmenuDirective;
348
+ exports.default = VueContextMenu;
349
+ //# sourceMappingURL=index.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.cjs.js","sources":["../node_modules/style-inject/dist/style-inject.es.js"],"sourcesContent":["function styleInject(css, ref) {\n if ( ref === void 0 ) ref = {};\n var insertAt = ref.insertAt;\n\n if (!css || typeof document === 'undefined') { return; }\n\n var head = document.head || document.getElementsByTagName('head')[0];\n var style = document.createElement('style');\n style.type = 'text/css';\n\n if (insertAt === 'top') {\n if (head.firstChild) {\n head.insertBefore(style, head.firstChild);\n } else {\n head.appendChild(style);\n }\n } else {\n head.appendChild(style);\n }\n\n if (style.styleSheet) {\n style.styleSheet.cssText = css;\n } else {\n style.appendChild(document.createTextNode(css));\n }\n}\n\nexport default styleInject;\n"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,SAAS,WAAW,CAAC,GAAG,EAAE,GAAG,EAAE;AAC/B,EAAE,KAAK,GAAG,KAAK,KAAK,CAAC,GAAG,GAAG,GAAG,EAAE,CAAC;AACjC,EAAE,IAAI,QAAQ,GAAG,GAAG,CAAC,QAAQ,CAAC;AAC9B;AACA,EAAE,IAAI,CAAC,GAAG,IAAI,OAAO,QAAQ,KAAK,WAAW,EAAE,EAAE,OAAO,EAAE;AAC1D;AACA,EAAE,IAAI,IAAI,GAAG,QAAQ,CAAC,IAAI,IAAI,QAAQ,CAAC,oBAAoB,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;AACvE,EAAE,IAAI,KAAK,GAAG,QAAQ,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC;AAC9C,EAAE,KAAK,CAAC,IAAI,GAAG,UAAU,CAAC;AAC1B;AACA,EAAE,IAAI,QAAQ,KAAK,KAAK,EAAE;AAC1B,IAAI,IAAI,IAAI,CAAC,UAAU,EAAE;AACzB,MAAM,IAAI,CAAC,YAAY,CAAC,KAAK,EAAE,IAAI,CAAC,UAAU,CAAC,CAAC;AAChD,KAAK,MAAM;AACX,MAAM,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC;AAC9B,KAAK;AACL,GAAG,MAAM;AACT,IAAI,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC;AAC5B,GAAG;AACH;AACA,EAAE,IAAI,KAAK,CAAC,UAAU,EAAE;AACxB,IAAI,KAAK,CAAC,UAAU,CAAC,OAAO,GAAG,GAAG,CAAC;AACnC,GAAG,MAAM;AACT,IAAI,KAAK,CAAC,WAAW,CAAC,QAAQ,CAAC,cAAc,CAAC,GAAG,CAAC,CAAC,CAAC;AACpD,GAAG;AACH;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;","x_google_ignoreList":[0]}
@@ -0,0 +1,343 @@
1
+ import { defineComponent, ref, computed, onMounted, onUnmounted, watch, openBlock, createBlock, Transition, withCtx, createElementBlock, normalizeStyle, withModifiers, createElementVNode, Fragment, renderList, createCommentVNode, normalizeClass, toDisplayString, createApp, h } from 'vue';
2
+
3
+ const _hoisted_1 = { class: "context-menu-content" };
4
+ const _hoisted_2 = {
5
+ key: 0,
6
+ class: "context-menu-divider"
7
+ };
8
+ const _hoisted_3 = ["onClick", "onMouseenter"];
9
+ const _hoisted_4 = {
10
+ key: 0,
11
+ class: "menu-icon"
12
+ };
13
+ const _hoisted_5 = { class: "menu-label" };
14
+ const _hoisted_6 = {
15
+ key: 1,
16
+ class: "menu-arrow"
17
+ };
18
+ const _hoisted_7 = ["onClick"];
19
+ const _hoisted_8 = {
20
+ key: 0,
21
+ class: "menu-icon"
22
+ };
23
+ const _hoisted_9 = { class: "menu-label" };
24
+ var _sfc_main = /*@__PURE__*/ defineComponent({
25
+ __name: 'ContextMenu',
26
+ props: {
27
+ visible: { type: Boolean, required: true },
28
+ x: { type: Number, required: true },
29
+ y: { type: Number, required: true },
30
+ menus: { type: Array, required: true },
31
+ zIndex: { type: Number, required: false, default: 9999 },
32
+ maxWidth: { type: Number, required: false, default: 300 },
33
+ minWidth: { type: Number, required: false, default: 150 }
34
+ },
35
+ emits: ["update:visible", "item-click"],
36
+ setup(__props, { emit: __emit }) {
37
+ const props = __props;
38
+ const emit = __emit;
39
+ const menuRef = ref();
40
+ const subMenuVisible = ref({});
41
+ const subMenuStyle = ref({ top: "0px", left: "0px" });
42
+ // 菜单样式
43
+ const menuStyle = computed(() => ({
44
+ left: `${props.x}px`,
45
+ top: `${props.y}px`,
46
+ zIndex: props.zIndex,
47
+ maxWidth: `${props.maxWidth}px`,
48
+ minWidth: `${props.minWidth}px`,
49
+ }));
50
+ // 当前显示的菜单
51
+ const currentMenus = computed(() => props.menus);
52
+ // 点击菜单项
53
+ const handleClick = (item) => {
54
+ if (item.disabled)
55
+ return;
56
+ if (item.handler) {
57
+ item.handler(menuRef.value, null);
58
+ }
59
+ emit("item-click", item);
60
+ emit("update:visible", false);
61
+ subMenuVisible.value = {};
62
+ };
63
+ // 鼠标进入显示子菜单
64
+ const handleMouseEnter = (item) => {
65
+ if (item.children && item.children.length > 0) {
66
+ subMenuVisible.value = { [item.id]: true };
67
+ // 计算子菜单位置
68
+ if (menuRef.value) {
69
+ const rect = menuRef.value.getBoundingClientRect();
70
+ subMenuStyle.value = {
71
+ top: "0px",
72
+ left: `${rect.width}px`,
73
+ };
74
+ }
75
+ }
76
+ };
77
+ // 点击外部关闭菜单
78
+ const handleClickOutside = (event) => {
79
+ if (menuRef.value && !menuRef.value.contains(event.target)) {
80
+ emit("update:visible", false);
81
+ subMenuVisible.value = {};
82
+ }
83
+ };
84
+ // 监听全局点击
85
+ onMounted(() => {
86
+ document.addEventListener("click", handleClickOutside);
87
+ document.addEventListener("contextmenu", handleClickOutside);
88
+ });
89
+ onUnmounted(() => {
90
+ document.removeEventListener("click", handleClickOutside);
91
+ document.removeEventListener("contextmenu", handleClickOutside);
92
+ });
93
+ // 监听visible变化
94
+ watch(() => props.visible, (newVal) => {
95
+ if (!newVal) {
96
+ subMenuVisible.value = {};
97
+ }
98
+ });
99
+ return (_ctx, _cache) => {
100
+ return (openBlock(), createBlock(Transition, { name: "context-menu-fade" }, {
101
+ default: withCtx(() => [
102
+ (__props.visible)
103
+ ? (openBlock(), createElementBlock("div", {
104
+ key: 0,
105
+ ref_key: "menuRef",
106
+ ref: menuRef,
107
+ class: "vue-context-menu",
108
+ style: normalizeStyle(menuStyle.value),
109
+ onClick: _cache[0] || (_cache[0] = withModifiers(() => { }, ["stop"])),
110
+ onContextmenu: _cache[1] || (_cache[1] = withModifiers(() => { }, ["stop", "prevent"]))
111
+ }, [
112
+ createElementVNode("div", _hoisted_1, [
113
+ (openBlock(true), createElementBlock(Fragment, null, renderList(currentMenus.value, (item) => {
114
+ return (openBlock(), createElementBlock(Fragment, {
115
+ key: item.id
116
+ }, [
117
+ createCommentVNode(" 分隔线 "),
118
+ (item.divider)
119
+ ? (openBlock(), createElementBlock("div", _hoisted_2))
120
+ : (openBlock(), createElementBlock(Fragment, { key: 1 }, [
121
+ createCommentVNode(" 菜单项 "),
122
+ createElementVNode("div", {
123
+ class: normalizeClass(["context-menu-item", {
124
+ disabled: item.disabled,
125
+ 'has-children': item.children && item.children.length > 0,
126
+ }]),
127
+ onClick: ($event) => (handleClick(item)),
128
+ onMouseenter: ($event) => (handleMouseEnter(item))
129
+ }, [
130
+ createCommentVNode(" 图标 "),
131
+ (item.icon)
132
+ ? (openBlock(), createElementBlock("span", _hoisted_4, [
133
+ createElementVNode("i", {
134
+ class: normalizeClass(item.icon)
135
+ }, null, 2 /* CLASS */)
136
+ ]))
137
+ : createCommentVNode("v-if", true),
138
+ createCommentVNode(" 标签 "),
139
+ createElementVNode("span", _hoisted_5, toDisplayString(item.label), 1 /* TEXT */),
140
+ createCommentVNode(" 子菜单箭头 "),
141
+ (item.children)
142
+ ? (openBlock(), createElementBlock("span", _hoisted_6, "▶"))
143
+ : createCommentVNode("v-if", true),
144
+ createCommentVNode(" 子菜单 "),
145
+ (item.children && subMenuVisible.value[item.id])
146
+ ? (openBlock(), createElementBlock("div", {
147
+ key: 2,
148
+ class: "context-submenu",
149
+ style: normalizeStyle(subMenuStyle.value)
150
+ }, [
151
+ (openBlock(true), createElementBlock(Fragment, null, renderList(item.children, (child) => {
152
+ return (openBlock(), createElementBlock("div", {
153
+ key: child.id,
154
+ class: normalizeClass(["context-menu-item", { disabled: child.disabled }]),
155
+ onClick: withModifiers(($event) => (handleClick(child)), ["stop"])
156
+ }, [
157
+ (child.icon)
158
+ ? (openBlock(), createElementBlock("span", _hoisted_8, [
159
+ createElementVNode("i", {
160
+ class: normalizeClass(child.icon)
161
+ }, null, 2 /* CLASS */)
162
+ ]))
163
+ : createCommentVNode("v-if", true),
164
+ createElementVNode("span", _hoisted_9, toDisplayString(child.label), 1 /* TEXT */)
165
+ ], 10 /* CLASS, PROPS */, _hoisted_7));
166
+ }), 128 /* KEYED_FRAGMENT */))
167
+ ], 4 /* STYLE */))
168
+ : createCommentVNode("v-if", true)
169
+ ], 42 /* CLASS, PROPS, NEED_HYDRATION */, _hoisted_3)
170
+ ], 2112 /* STABLE_FRAGMENT, DEV_ROOT_FRAGMENT */))
171
+ ], 64 /* STABLE_FRAGMENT */));
172
+ }), 128 /* KEYED_FRAGMENT */))
173
+ ])
174
+ ], 36 /* STYLE, NEED_HYDRATION */))
175
+ : createCommentVNode("v-if", true)
176
+ ]),
177
+ _: 1 /* STABLE */
178
+ }));
179
+ };
180
+ }
181
+ });
182
+
183
+ function styleInject(css, ref) {
184
+ if ( ref === void 0 ) ref = {};
185
+ var insertAt = ref.insertAt;
186
+
187
+ if (!css || typeof document === 'undefined') { return; }
188
+
189
+ var head = document.head || document.getElementsByTagName('head')[0];
190
+ var style = document.createElement('style');
191
+ style.type = 'text/css';
192
+
193
+ if (insertAt === 'top') {
194
+ if (head.firstChild) {
195
+ head.insertBefore(style, head.firstChild);
196
+ } else {
197
+ head.appendChild(style);
198
+ }
199
+ } else {
200
+ head.appendChild(style);
201
+ }
202
+
203
+ if (style.styleSheet) {
204
+ style.styleSheet.cssText = css;
205
+ } else {
206
+ style.appendChild(document.createTextNode(css));
207
+ }
208
+ }
209
+
210
+ var css_248z = "\n.vue-context-menu[data-v-cbb365ae] {\r\n position: fixed;\r\n background: white;\r\n border: 1px solid #dcdfe6;\r\n border-radius: 4px;\r\n box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);\r\n padding: 5px 0;\r\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif;\r\n font-size: 14px;\r\n color: #606266;\n}\n.context-menu-content[data-v-cbb365ae] {\r\n width: 100%;\n}\n.context-menu-item[data-v-cbb365ae] {\r\n padding: 8px 20px;\r\n cursor: pointer;\r\n display: flex;\r\n align-items: center;\r\n position: relative;\r\n transition: background-color 0.2s;\n}\n.context-menu-item[data-v-cbb365ae]:hover:not(.disabled) {\r\n background-color: #f5f7fa;\n}\n.context-menu-item.disabled[data-v-cbb365ae] {\r\n color: #c0c4cc;\r\n cursor: not-allowed;\n}\n.context-menu-divider[data-v-cbb365ae] {\r\n height: 1px;\r\n background-color: #ebeef5;\r\n margin: 5px 0;\n}\n.menu-icon[data-v-cbb365ae] {\r\n margin-right: 8px;\r\n font-size: 14px;\r\n width: 14px;\r\n display: inline-flex;\r\n justify-content: center;\n}\n.menu-label[data-v-cbb365ae] {\r\n flex: 1;\r\n white-space: nowrap;\r\n overflow: hidden;\r\n text-overflow: ellipsis;\n}\n.menu-arrow[data-v-cbb365ae] {\r\n margin-left: 8px;\r\n font-size: 12px;\r\n color: #909399;\n}\n.context-submenu[data-v-cbb365ae] {\r\n position: absolute;\r\n background: white;\r\n border: 1px solid #dcdfe6;\r\n border-radius: 4px;\r\n box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);\r\n min-width: 150px;\r\n z-index: 10000;\n}\r\n\r\n/* 动画效果 */\n.context-menu-fade-enter-active[data-v-cbb365ae],\r\n.context-menu-fade-leave-active[data-v-cbb365ae] {\r\n transition: opacity 0.2s, transform 0.2s;\n}\n.context-menu-fade-enter-from[data-v-cbb365ae],\r\n.context-menu-fade-leave-to[data-v-cbb365ae] {\r\n opacity: 0;\r\n transform: scale(0.95);\n}\r\n";
211
+ styleInject(css_248z);
212
+
213
+ var _export_sfc = (sfc, props) => {
214
+ const target = sfc.__vccOpts || sfc;
215
+ for (const [key, val] of props) {
216
+ target[key] = val;
217
+ }
218
+ return target;
219
+ };
220
+
221
+ var ContextMenu = /* @__PURE__ */ _export_sfc(_sfc_main, [["__scopeId", "data-v-cbb365ae"]]);
222
+
223
+ // 全局菜单实例
224
+ let menuInstance = null;
225
+ let container = null;
226
+ // 获取组件名
227
+ function getComponentName(el) {
228
+ // 优先从data-component获取
229
+ const dataComponent = el.getAttribute("data-component");
230
+ if (dataComponent)
231
+ return dataComponent;
232
+ // 从class中查找组件类名
233
+ const classList = el.classList;
234
+ for (let i = 0; i < classList.length; i++) {
235
+ const className = classList[i];
236
+ if (className.startsWith("component-")) {
237
+ return className.replace("component-", "");
238
+ }
239
+ }
240
+ // 默认返回标签名
241
+ return el.tagName.toLowerCase();
242
+ }
243
+ // 创建菜单实例
244
+ function createMenuInstance() {
245
+ if (menuInstance)
246
+ return;
247
+ container = document.createElement("div");
248
+ document.body.appendChild(container);
249
+ menuInstance = createApp({
250
+ data() {
251
+ return {
252
+ visible: false,
253
+ x: 0,
254
+ y: 0,
255
+ menus: [],
256
+ };
257
+ },
258
+ render() {
259
+ return h(ContextMenu, {
260
+ visible: this.visible,
261
+ x: this.x,
262
+ y: this.y,
263
+ menus: this.menus,
264
+ "onUpdate:visible": (val) => {
265
+ this.visible = val;
266
+ },
267
+ onItemClick: (item) => {
268
+ // 菜单项点击处理
269
+ },
270
+ });
271
+ },
272
+ });
273
+ menuInstance.mount(container);
274
+ }
275
+ // 显示菜单
276
+ function showContextMenu(event, menus, options) {
277
+ if (!menuInstance)
278
+ createMenuInstance();
279
+ const vm = menuInstance._instance.proxy;
280
+ vm.x = event.clientX;
281
+ vm.y = event.clientY;
282
+ vm.menus = menus;
283
+ vm.visible = true;
284
+ // 防止菜单超出屏幕
285
+ requestAnimationFrame(() => {
286
+ const menuEl = container === null || container === void 0 ? void 0 : container.querySelector(".vue-context-menu");
287
+ if (menuEl) {
288
+ const rect = menuEl.getBoundingClientRect();
289
+ const viewportWidth = window.innerWidth;
290
+ const viewportHeight = window.innerHeight;
291
+ if (rect.right > viewportWidth) {
292
+ vm.x = viewportWidth - rect.width - 5;
293
+ }
294
+ if (rect.bottom > viewportHeight) {
295
+ vm.y = viewportHeight - rect.height - 5;
296
+ }
297
+ }
298
+ });
299
+ event.preventDefault();
300
+ event.stopPropagation();
301
+ }
302
+ // 获取对应组件的菜单
303
+ function getMenusForComponent(componentName, options) {
304
+ if (typeof options.menus === "function") {
305
+ return options.menus(componentName);
306
+ }
307
+ else if (Array.isArray(options.menus)) {
308
+ return options.menus;
309
+ }
310
+ else {
311
+ return options.menus[componentName] || [];
312
+ }
313
+ }
314
+ // 指令定义
315
+ const contextmenuDirective = {
316
+ mounted(el, binding) {
317
+ const options = {
318
+ menus: binding.value,
319
+ zIndex: binding.arg ? parseInt(binding.arg) : 9999,
320
+ ...binding.modifiers,
321
+ };
322
+ el.addEventListener("contextmenu", (event) => {
323
+ const componentName = getComponentName(el);
324
+ const menus = getMenusForComponent(componentName, options);
325
+ if (menus.length > 0) {
326
+ showContextMenu(event, menus);
327
+ }
328
+ });
329
+ },
330
+ };
331
+
332
+ // 默认导出插件
333
+ const VueContextMenu = {
334
+ install(app) {
335
+ // 注册全局指令
336
+ app.directive("contextmenu", contextmenuDirective);
337
+ // 注册全局组件(可选)
338
+ app.component("ContextMenu", ContextMenu);
339
+ },
340
+ };
341
+
342
+ export { ContextMenu, contextmenuDirective, VueContextMenu as default };
343
+ //# sourceMappingURL=index.esm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.esm.js","sources":["../node_modules/style-inject/dist/style-inject.es.js"],"sourcesContent":["function styleInject(css, ref) {\n if ( ref === void 0 ) ref = {};\n var insertAt = ref.insertAt;\n\n if (!css || typeof document === 'undefined') { return; }\n\n var head = document.head || document.getElementsByTagName('head')[0];\n var style = document.createElement('style');\n style.type = 'text/css';\n\n if (insertAt === 'top') {\n if (head.firstChild) {\n head.insertBefore(style, head.firstChild);\n } else {\n head.appendChild(style);\n }\n } else {\n head.appendChild(style);\n }\n\n if (style.styleSheet) {\n style.styleSheet.cssText = css;\n } else {\n style.appendChild(document.createTextNode(css));\n }\n}\n\nexport default styleInject;\n"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,SAAS,WAAW,CAAC,GAAG,EAAE,GAAG,EAAE;AAC/B,EAAE,KAAK,GAAG,KAAK,KAAK,CAAC,GAAG,GAAG,GAAG,EAAE,CAAC;AACjC,EAAE,IAAI,QAAQ,GAAG,GAAG,CAAC,QAAQ,CAAC;AAC9B;AACA,EAAE,IAAI,CAAC,GAAG,IAAI,OAAO,QAAQ,KAAK,WAAW,EAAE,EAAE,OAAO,EAAE;AAC1D;AACA,EAAE,IAAI,IAAI,GAAG,QAAQ,CAAC,IAAI,IAAI,QAAQ,CAAC,oBAAoB,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;AACvE,EAAE,IAAI,KAAK,GAAG,QAAQ,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC;AAC9C,EAAE,KAAK,CAAC,IAAI,GAAG,UAAU,CAAC;AAC1B;AACA,EAAE,IAAI,QAAQ,KAAK,KAAK,EAAE;AAC1B,IAAI,IAAI,IAAI,CAAC,UAAU,EAAE;AACzB,MAAM,IAAI,CAAC,YAAY,CAAC,KAAK,EAAE,IAAI,CAAC,UAAU,CAAC,CAAC;AAChD,KAAK,MAAM;AACX,MAAM,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC;AAC9B,KAAK;AACL,GAAG,MAAM;AACT,IAAI,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC;AAC5B,GAAG;AACH;AACA,EAAE,IAAI,KAAK,CAAC,UAAU,EAAE;AACxB,IAAI,KAAK,CAAC,UAAU,CAAC,OAAO,GAAG,GAAG,CAAC;AACnC,GAAG,MAAM;AACT,IAAI,KAAK,CAAC,WAAW,CAAC,QAAQ,CAAC,cAAc,CAAC,GAAG,CAAC,CAAC,CAAC;AACpD,GAAG;AACH;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;","x_google_ignoreList":[0]}
@@ -0,0 +1,353 @@
1
+ (function (global, factory) {
2
+ typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('vue')) :
3
+ typeof define === 'function' && define.amd ? define(['exports', 'vue'], factory) :
4
+ (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.VueContextMenu = {}, global.Vue));
5
+ })(this, (function (exports, vue) { 'use strict';
6
+
7
+ const _hoisted_1 = { class: "context-menu-content" };
8
+ const _hoisted_2 = {
9
+ key: 0,
10
+ class: "context-menu-divider"
11
+ };
12
+ const _hoisted_3 = ["onClick", "onMouseenter"];
13
+ const _hoisted_4 = {
14
+ key: 0,
15
+ class: "menu-icon"
16
+ };
17
+ const _hoisted_5 = { class: "menu-label" };
18
+ const _hoisted_6 = {
19
+ key: 1,
20
+ class: "menu-arrow"
21
+ };
22
+ const _hoisted_7 = ["onClick"];
23
+ const _hoisted_8 = {
24
+ key: 0,
25
+ class: "menu-icon"
26
+ };
27
+ const _hoisted_9 = { class: "menu-label" };
28
+ var _sfc_main = /*@__PURE__*/ vue.defineComponent({
29
+ __name: 'ContextMenu',
30
+ props: {
31
+ visible: { type: Boolean, required: true },
32
+ x: { type: Number, required: true },
33
+ y: { type: Number, required: true },
34
+ menus: { type: Array, required: true },
35
+ zIndex: { type: Number, required: false, default: 9999 },
36
+ maxWidth: { type: Number, required: false, default: 300 },
37
+ minWidth: { type: Number, required: false, default: 150 }
38
+ },
39
+ emits: ["update:visible", "item-click"],
40
+ setup(__props, { emit: __emit }) {
41
+ const props = __props;
42
+ const emit = __emit;
43
+ const menuRef = vue.ref();
44
+ const subMenuVisible = vue.ref({});
45
+ const subMenuStyle = vue.ref({ top: "0px", left: "0px" });
46
+ // 菜单样式
47
+ const menuStyle = vue.computed(() => ({
48
+ left: `${props.x}px`,
49
+ top: `${props.y}px`,
50
+ zIndex: props.zIndex,
51
+ maxWidth: `${props.maxWidth}px`,
52
+ minWidth: `${props.minWidth}px`,
53
+ }));
54
+ // 当前显示的菜单
55
+ const currentMenus = vue.computed(() => props.menus);
56
+ // 点击菜单项
57
+ const handleClick = (item) => {
58
+ if (item.disabled)
59
+ return;
60
+ if (item.handler) {
61
+ item.handler(menuRef.value, null);
62
+ }
63
+ emit("item-click", item);
64
+ emit("update:visible", false);
65
+ subMenuVisible.value = {};
66
+ };
67
+ // 鼠标进入显示子菜单
68
+ const handleMouseEnter = (item) => {
69
+ if (item.children && item.children.length > 0) {
70
+ subMenuVisible.value = { [item.id]: true };
71
+ // 计算子菜单位置
72
+ if (menuRef.value) {
73
+ const rect = menuRef.value.getBoundingClientRect();
74
+ subMenuStyle.value = {
75
+ top: "0px",
76
+ left: `${rect.width}px`,
77
+ };
78
+ }
79
+ }
80
+ };
81
+ // 点击外部关闭菜单
82
+ const handleClickOutside = (event) => {
83
+ if (menuRef.value && !menuRef.value.contains(event.target)) {
84
+ emit("update:visible", false);
85
+ subMenuVisible.value = {};
86
+ }
87
+ };
88
+ // 监听全局点击
89
+ vue.onMounted(() => {
90
+ document.addEventListener("click", handleClickOutside);
91
+ document.addEventListener("contextmenu", handleClickOutside);
92
+ });
93
+ vue.onUnmounted(() => {
94
+ document.removeEventListener("click", handleClickOutside);
95
+ document.removeEventListener("contextmenu", handleClickOutside);
96
+ });
97
+ // 监听visible变化
98
+ vue.watch(() => props.visible, (newVal) => {
99
+ if (!newVal) {
100
+ subMenuVisible.value = {};
101
+ }
102
+ });
103
+ return (_ctx, _cache) => {
104
+ return (vue.openBlock(), vue.createBlock(vue.Transition, { name: "context-menu-fade" }, {
105
+ default: vue.withCtx(() => [
106
+ (__props.visible)
107
+ ? (vue.openBlock(), vue.createElementBlock("div", {
108
+ key: 0,
109
+ ref_key: "menuRef",
110
+ ref: menuRef,
111
+ class: "vue-context-menu",
112
+ style: vue.normalizeStyle(menuStyle.value),
113
+ onClick: _cache[0] || (_cache[0] = vue.withModifiers(() => { }, ["stop"])),
114
+ onContextmenu: _cache[1] || (_cache[1] = vue.withModifiers(() => { }, ["stop", "prevent"]))
115
+ }, [
116
+ vue.createElementVNode("div", _hoisted_1, [
117
+ (vue.openBlock(true), vue.createElementBlock(vue.Fragment, null, vue.renderList(currentMenus.value, (item) => {
118
+ return (vue.openBlock(), vue.createElementBlock(vue.Fragment, {
119
+ key: item.id
120
+ }, [
121
+ vue.createCommentVNode(" 分隔线 "),
122
+ (item.divider)
123
+ ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_2))
124
+ : (vue.openBlock(), vue.createElementBlock(vue.Fragment, { key: 1 }, [
125
+ vue.createCommentVNode(" 菜单项 "),
126
+ vue.createElementVNode("div", {
127
+ class: vue.normalizeClass(["context-menu-item", {
128
+ disabled: item.disabled,
129
+ 'has-children': item.children && item.children.length > 0,
130
+ }]),
131
+ onClick: ($event) => (handleClick(item)),
132
+ onMouseenter: ($event) => (handleMouseEnter(item))
133
+ }, [
134
+ vue.createCommentVNode(" 图标 "),
135
+ (item.icon)
136
+ ? (vue.openBlock(), vue.createElementBlock("span", _hoisted_4, [
137
+ vue.createElementVNode("i", {
138
+ class: vue.normalizeClass(item.icon)
139
+ }, null, 2 /* CLASS */)
140
+ ]))
141
+ : vue.createCommentVNode("v-if", true),
142
+ vue.createCommentVNode(" 标签 "),
143
+ vue.createElementVNode("span", _hoisted_5, vue.toDisplayString(item.label), 1 /* TEXT */),
144
+ vue.createCommentVNode(" 子菜单箭头 "),
145
+ (item.children)
146
+ ? (vue.openBlock(), vue.createElementBlock("span", _hoisted_6, "▶"))
147
+ : vue.createCommentVNode("v-if", true),
148
+ vue.createCommentVNode(" 子菜单 "),
149
+ (item.children && subMenuVisible.value[item.id])
150
+ ? (vue.openBlock(), vue.createElementBlock("div", {
151
+ key: 2,
152
+ class: "context-submenu",
153
+ style: vue.normalizeStyle(subMenuStyle.value)
154
+ }, [
155
+ (vue.openBlock(true), vue.createElementBlock(vue.Fragment, null, vue.renderList(item.children, (child) => {
156
+ return (vue.openBlock(), vue.createElementBlock("div", {
157
+ key: child.id,
158
+ class: vue.normalizeClass(["context-menu-item", { disabled: child.disabled }]),
159
+ onClick: vue.withModifiers(($event) => (handleClick(child)), ["stop"])
160
+ }, [
161
+ (child.icon)
162
+ ? (vue.openBlock(), vue.createElementBlock("span", _hoisted_8, [
163
+ vue.createElementVNode("i", {
164
+ class: vue.normalizeClass(child.icon)
165
+ }, null, 2 /* CLASS */)
166
+ ]))
167
+ : vue.createCommentVNode("v-if", true),
168
+ vue.createElementVNode("span", _hoisted_9, vue.toDisplayString(child.label), 1 /* TEXT */)
169
+ ], 10 /* CLASS, PROPS */, _hoisted_7));
170
+ }), 128 /* KEYED_FRAGMENT */))
171
+ ], 4 /* STYLE */))
172
+ : vue.createCommentVNode("v-if", true)
173
+ ], 42 /* CLASS, PROPS, NEED_HYDRATION */, _hoisted_3)
174
+ ], 2112 /* STABLE_FRAGMENT, DEV_ROOT_FRAGMENT */))
175
+ ], 64 /* STABLE_FRAGMENT */));
176
+ }), 128 /* KEYED_FRAGMENT */))
177
+ ])
178
+ ], 36 /* STYLE, NEED_HYDRATION */))
179
+ : vue.createCommentVNode("v-if", true)
180
+ ]),
181
+ _: 1 /* STABLE */
182
+ }));
183
+ };
184
+ }
185
+ });
186
+
187
+ function styleInject(css, ref) {
188
+ if ( ref === void 0 ) ref = {};
189
+ var insertAt = ref.insertAt;
190
+
191
+ if (!css || typeof document === 'undefined') { return; }
192
+
193
+ var head = document.head || document.getElementsByTagName('head')[0];
194
+ var style = document.createElement('style');
195
+ style.type = 'text/css';
196
+
197
+ if (insertAt === 'top') {
198
+ if (head.firstChild) {
199
+ head.insertBefore(style, head.firstChild);
200
+ } else {
201
+ head.appendChild(style);
202
+ }
203
+ } else {
204
+ head.appendChild(style);
205
+ }
206
+
207
+ if (style.styleSheet) {
208
+ style.styleSheet.cssText = css;
209
+ } else {
210
+ style.appendChild(document.createTextNode(css));
211
+ }
212
+ }
213
+
214
+ var css_248z = "\n.vue-context-menu[data-v-cbb365ae] {\r\n position: fixed;\r\n background: white;\r\n border: 1px solid #dcdfe6;\r\n border-radius: 4px;\r\n box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);\r\n padding: 5px 0;\r\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif;\r\n font-size: 14px;\r\n color: #606266;\n}\n.context-menu-content[data-v-cbb365ae] {\r\n width: 100%;\n}\n.context-menu-item[data-v-cbb365ae] {\r\n padding: 8px 20px;\r\n cursor: pointer;\r\n display: flex;\r\n align-items: center;\r\n position: relative;\r\n transition: background-color 0.2s;\n}\n.context-menu-item[data-v-cbb365ae]:hover:not(.disabled) {\r\n background-color: #f5f7fa;\n}\n.context-menu-item.disabled[data-v-cbb365ae] {\r\n color: #c0c4cc;\r\n cursor: not-allowed;\n}\n.context-menu-divider[data-v-cbb365ae] {\r\n height: 1px;\r\n background-color: #ebeef5;\r\n margin: 5px 0;\n}\n.menu-icon[data-v-cbb365ae] {\r\n margin-right: 8px;\r\n font-size: 14px;\r\n width: 14px;\r\n display: inline-flex;\r\n justify-content: center;\n}\n.menu-label[data-v-cbb365ae] {\r\n flex: 1;\r\n white-space: nowrap;\r\n overflow: hidden;\r\n text-overflow: ellipsis;\n}\n.menu-arrow[data-v-cbb365ae] {\r\n margin-left: 8px;\r\n font-size: 12px;\r\n color: #909399;\n}\n.context-submenu[data-v-cbb365ae] {\r\n position: absolute;\r\n background: white;\r\n border: 1px solid #dcdfe6;\r\n border-radius: 4px;\r\n box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);\r\n min-width: 150px;\r\n z-index: 10000;\n}\r\n\r\n/* 动画效果 */\n.context-menu-fade-enter-active[data-v-cbb365ae],\r\n.context-menu-fade-leave-active[data-v-cbb365ae] {\r\n transition: opacity 0.2s, transform 0.2s;\n}\n.context-menu-fade-enter-from[data-v-cbb365ae],\r\n.context-menu-fade-leave-to[data-v-cbb365ae] {\r\n opacity: 0;\r\n transform: scale(0.95);\n}\r\n";
215
+ styleInject(css_248z);
216
+
217
+ var _export_sfc = (sfc, props) => {
218
+ const target = sfc.__vccOpts || sfc;
219
+ for (const [key, val] of props) {
220
+ target[key] = val;
221
+ }
222
+ return target;
223
+ };
224
+
225
+ var ContextMenu = /* @__PURE__ */ _export_sfc(_sfc_main, [["__scopeId", "data-v-cbb365ae"]]);
226
+
227
+ // 全局菜单实例
228
+ let menuInstance = null;
229
+ let container = null;
230
+ // 获取组件名
231
+ function getComponentName(el) {
232
+ // 优先从data-component获取
233
+ const dataComponent = el.getAttribute("data-component");
234
+ if (dataComponent)
235
+ return dataComponent;
236
+ // 从class中查找组件类名
237
+ const classList = el.classList;
238
+ for (let i = 0; i < classList.length; i++) {
239
+ const className = classList[i];
240
+ if (className.startsWith("component-")) {
241
+ return className.replace("component-", "");
242
+ }
243
+ }
244
+ // 默认返回标签名
245
+ return el.tagName.toLowerCase();
246
+ }
247
+ // 创建菜单实例
248
+ function createMenuInstance() {
249
+ if (menuInstance)
250
+ return;
251
+ container = document.createElement("div");
252
+ document.body.appendChild(container);
253
+ menuInstance = vue.createApp({
254
+ data() {
255
+ return {
256
+ visible: false,
257
+ x: 0,
258
+ y: 0,
259
+ menus: [],
260
+ };
261
+ },
262
+ render() {
263
+ return vue.h(ContextMenu, {
264
+ visible: this.visible,
265
+ x: this.x,
266
+ y: this.y,
267
+ menus: this.menus,
268
+ "onUpdate:visible": (val) => {
269
+ this.visible = val;
270
+ },
271
+ onItemClick: (item) => {
272
+ // 菜单项点击处理
273
+ },
274
+ });
275
+ },
276
+ });
277
+ menuInstance.mount(container);
278
+ }
279
+ // 显示菜单
280
+ function showContextMenu(event, menus, options) {
281
+ if (!menuInstance)
282
+ createMenuInstance();
283
+ const vm = menuInstance._instance.proxy;
284
+ vm.x = event.clientX;
285
+ vm.y = event.clientY;
286
+ vm.menus = menus;
287
+ vm.visible = true;
288
+ // 防止菜单超出屏幕
289
+ requestAnimationFrame(() => {
290
+ const menuEl = container === null || container === void 0 ? void 0 : container.querySelector(".vue-context-menu");
291
+ if (menuEl) {
292
+ const rect = menuEl.getBoundingClientRect();
293
+ const viewportWidth = window.innerWidth;
294
+ const viewportHeight = window.innerHeight;
295
+ if (rect.right > viewportWidth) {
296
+ vm.x = viewportWidth - rect.width - 5;
297
+ }
298
+ if (rect.bottom > viewportHeight) {
299
+ vm.y = viewportHeight - rect.height - 5;
300
+ }
301
+ }
302
+ });
303
+ event.preventDefault();
304
+ event.stopPropagation();
305
+ }
306
+ // 获取对应组件的菜单
307
+ function getMenusForComponent(componentName, options) {
308
+ if (typeof options.menus === "function") {
309
+ return options.menus(componentName);
310
+ }
311
+ else if (Array.isArray(options.menus)) {
312
+ return options.menus;
313
+ }
314
+ else {
315
+ return options.menus[componentName] || [];
316
+ }
317
+ }
318
+ // 指令定义
319
+ const contextmenuDirective = {
320
+ mounted(el, binding) {
321
+ const options = {
322
+ menus: binding.value,
323
+ zIndex: binding.arg ? parseInt(binding.arg) : 9999,
324
+ ...binding.modifiers,
325
+ };
326
+ el.addEventListener("contextmenu", (event) => {
327
+ const componentName = getComponentName(el);
328
+ const menus = getMenusForComponent(componentName, options);
329
+ if (menus.length > 0) {
330
+ showContextMenu(event, menus);
331
+ }
332
+ });
333
+ },
334
+ };
335
+
336
+ // 默认导出插件
337
+ const VueContextMenu = {
338
+ install(app) {
339
+ // 注册全局指令
340
+ app.directive("contextmenu", contextmenuDirective);
341
+ // 注册全局组件(可选)
342
+ app.component("ContextMenu", ContextMenu);
343
+ },
344
+ };
345
+
346
+ exports.ContextMenu = ContextMenu;
347
+ exports.contextmenuDirective = contextmenuDirective;
348
+ exports.default = VueContextMenu;
349
+
350
+ Object.defineProperty(exports, '__esModule', { value: true });
351
+
352
+ }));
353
+ //# sourceMappingURL=index.umd.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.umd.js","sources":["../node_modules/style-inject/dist/style-inject.es.js"],"sourcesContent":["function styleInject(css, ref) {\n if ( ref === void 0 ) ref = {};\n var insertAt = ref.insertAt;\n\n if (!css || typeof document === 'undefined') { return; }\n\n var head = document.head || document.getElementsByTagName('head')[0];\n var style = document.createElement('style');\n style.type = 'text/css';\n\n if (insertAt === 'top') {\n if (head.firstChild) {\n head.insertBefore(style, head.firstChild);\n } else {\n head.appendChild(style);\n }\n } else {\n head.appendChild(style);\n }\n\n if (style.styleSheet) {\n style.styleSheet.cssText = css;\n } else {\n style.appendChild(document.createTextNode(css));\n }\n}\n\nexport default styleInject;\n"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAAA,SAAS,WAAW,CAAC,GAAG,EAAE,GAAG,EAAE;EAC/B,EAAE,KAAK,GAAG,KAAK,KAAK,CAAC,GAAG,GAAG,GAAG,EAAE,CAAC;EACjC,EAAE,IAAI,QAAQ,GAAG,GAAG,CAAC,QAAQ,CAAC;AAC9B;EACA,EAAE,IAAI,CAAC,GAAG,IAAI,OAAO,QAAQ,KAAK,WAAW,EAAE,EAAE,OAAO,EAAE;AAC1D;EACA,EAAE,IAAI,IAAI,GAAG,QAAQ,CAAC,IAAI,IAAI,QAAQ,CAAC,oBAAoB,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;EACvE,EAAE,IAAI,KAAK,GAAG,QAAQ,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC;EAC9C,EAAE,KAAK,CAAC,IAAI,GAAG,UAAU,CAAC;AAC1B;EACA,EAAE,IAAI,QAAQ,KAAK,KAAK,EAAE;EAC1B,IAAI,IAAI,IAAI,CAAC,UAAU,EAAE;EACzB,MAAM,IAAI,CAAC,YAAY,CAAC,KAAK,EAAE,IAAI,CAAC,UAAU,CAAC,CAAC;EAChD,KAAK,MAAM;EACX,MAAM,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC;EAC9B,KAAK;EACL,GAAG,MAAM;EACT,IAAI,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC;EAC5B,GAAG;AACH;EACA,EAAE,IAAI,KAAK,CAAC,UAAU,EAAE;EACxB,IAAI,KAAK,CAAC,UAAU,CAAC,OAAO,GAAG,GAAG,CAAC;EACnC,GAAG,MAAM;EACT,IAAI,KAAK,CAAC,WAAW,CAAC,QAAQ,CAAC,cAAc,CAAC,GAAG,CAAC,CAAC,CAAC;EACpD,GAAG;EACH;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;","x_google_ignoreList":[0]}
@@ -0,0 +1,75 @@
1
+ declare const _default: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
2
+ visible: {
3
+ type: BooleanConstructor;
4
+ required: true;
5
+ };
6
+ x: {
7
+ type: NumberConstructor;
8
+ required: true;
9
+ };
10
+ y: {
11
+ type: NumberConstructor;
12
+ required: true;
13
+ };
14
+ menus: {
15
+ type: ArrayConstructor;
16
+ required: true;
17
+ };
18
+ zIndex: {
19
+ type: NumberConstructor;
20
+ required: false;
21
+ default: number;
22
+ };
23
+ maxWidth: {
24
+ type: NumberConstructor;
25
+ required: false;
26
+ default: number;
27
+ };
28
+ minWidth: {
29
+ type: NumberConstructor;
30
+ required: false;
31
+ default: number;
32
+ };
33
+ }>, (_ctx: any, _cache: any) => import("vue").VNode<import("vue").RendererNode, import("vue").RendererElement, {
34
+ [key: string]: any;
35
+ }>, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, ("update:visible" | "item-click")[], "update:visible" | "item-click", import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
36
+ visible: {
37
+ type: BooleanConstructor;
38
+ required: true;
39
+ };
40
+ x: {
41
+ type: NumberConstructor;
42
+ required: true;
43
+ };
44
+ y: {
45
+ type: NumberConstructor;
46
+ required: true;
47
+ };
48
+ menus: {
49
+ type: ArrayConstructor;
50
+ required: true;
51
+ };
52
+ zIndex: {
53
+ type: NumberConstructor;
54
+ required: false;
55
+ default: number;
56
+ };
57
+ maxWidth: {
58
+ type: NumberConstructor;
59
+ required: false;
60
+ default: number;
61
+ };
62
+ minWidth: {
63
+ type: NumberConstructor;
64
+ required: false;
65
+ default: number;
66
+ };
67
+ }>> & Readonly<{
68
+ "onUpdate:visible"?: ((...args: any[]) => any) | undefined;
69
+ "onItem-click"?: ((...args: any[]) => any) | undefined;
70
+ }>, {
71
+ zIndex: number;
72
+ maxWidth: number;
73
+ minWidth: number;
74
+ }, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
75
+ export default _default;
@@ -0,0 +1,4 @@
1
+ declare const contextmenuDirective: {
2
+ mounted(el: HTMLElement, binding: any): void;
3
+ };
4
+ export { contextmenuDirective };
@@ -0,0 +1,11 @@
1
+ import { App } from "vue";
2
+ import { contextmenuDirective } from "./directives/contextmenu";
3
+ import ContextMenu from "./components/ContextMenu.vue";
4
+ import type { MenuItem, ComponentMenuMap, ContextMenuOptions } from "./types";
5
+ export { ContextMenu };
6
+ export type { MenuItem, ComponentMenuMap, ContextMenuOptions };
7
+ export { contextmenuDirective };
8
+ declare const VueContextMenu: {
9
+ install(app: App): void;
10
+ };
11
+ export default VueContextMenu;
@@ -0,0 +1,18 @@
1
+ export interface MenuItem {
2
+ id: string | number;
3
+ label: string;
4
+ icon?: string;
5
+ disabled?: boolean;
6
+ divider?: boolean;
7
+ children?: MenuItem[];
8
+ handler?: (el: HTMLElement, component: any) => void;
9
+ }
10
+ export interface ComponentMenuMap {
11
+ [componentName: string]: MenuItem[];
12
+ }
13
+ export interface ContextMenuOptions {
14
+ menus: ComponentMenuMap | MenuItem[] | ((component: any) => MenuItem[]);
15
+ zIndex?: number;
16
+ maxWidth?: number;
17
+ minWidth?: number;
18
+ }
@@ -0,0 +1 @@
1
+ export declare const defaultUtils: {};
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "type": "module",
3
+ "name": "vue3-context-menu-plus",
4
+ "version": "1.0.1",
5
+ "description": "A Vue 3 context menu component that shows different menus based on different components",
6
+ "main": "dist/index.cjs.js",
7
+ "module": "dist/index.esm.js",
8
+ "unpkg": "dist/index.umd.js",
9
+ "types": "dist/types/index.d.ts",
10
+ "publishConfig": {
11
+ "registry": "https://registry.npmjs.org/"
12
+ },
13
+ "files": [
14
+ "dist",
15
+ "README.md"
16
+ ],
17
+ "scripts": {
18
+ "build": "rollup -c",
19
+ "dev": "rollup -c -w",
20
+ "prepublishOnly": "npm run build",
21
+ "test": "echo \"Error: no test specified\" && exit 1"
22
+ },
23
+ "keywords": [
24
+ "vue3",
25
+ "contextmenu",
26
+ "right-click",
27
+ "menu",
28
+ "component"
29
+ ],
30
+ "author": "Your Name",
31
+ "license": "MIT",
32
+ "peerDependencies": {
33
+ "vue": "^3.0.0"
34
+ },
35
+ "devDependencies": {
36
+ "@rollup/plugin-commonjs": "^24.0.0",
37
+ "@rollup/plugin-node-resolve": "^15.0.0",
38
+ "@types/node": "^25.0.0",
39
+ "@vitejs/plugin-vue": "^6.0.2",
40
+ "@vue/compiler-sfc": "^3.2.0",
41
+ "postcss": "^8.5.6",
42
+ "rollup": "^3.0.0",
43
+ "rollup-plugin-postcss": "^4.0.2",
44
+ "rollup-plugin-typescript2": "^0.34.0",
45
+ "rollup-plugin-vue": "^6.0.0",
46
+ "typescript": "~5.9.0",
47
+ "vue": "^3.5.22"
48
+ },
49
+ "repository": {
50
+ "type": "git",
51
+ "url": "git+https://github.com/yourusername/vue3-context-menu.git"
52
+ },
53
+ "bugs": {
54
+ "url": "https://github.com/yourusername/vue3-context-menu/issues"
55
+ },
56
+ "homepage": "https://github.com/yourusername/vue3-context-menu#readme"
57
+ }