vue3-context-menu-plus 1.0.1 → 2.0.0

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 CHANGED
@@ -2,12 +2,19 @@
2
2
  * @Author: eggYolkegg
3
3
  * @Date: 2025-12-11 15:41:57
4
4
  * @LastEditors: eggYolkegg
5
- * @LastEditTime: 2025-12-11 16:11:26
6
- * @Description:
5
+ * @LastEditTime: 2025-12-17 15:13:45
6
+ * @Description:
7
7
  -->
8
+
8
9
  # Vue3 Context Menu
9
10
 
10
- 一个 Vue 3 右键菜单组件,可以根据不同的组件显示不同的右键菜单。
11
+ 一个轻量、灵活的 Vue 3 右键菜单组件,支持根据不同组件标识配置差异化右键菜单,内置多级子菜单、分隔线、禁用状态等常用特性,指令式调用简单易用。
12
+ 特性
13
+ 🚀 适配 Vue 3 生态(支持 Options API / Composition API)
14
+ 🎨 支持按组件标识(data-component)区分菜单
15
+ 📋 支持多级子菜单、分隔线、禁用菜单项
16
+ 🔧 自定义菜单点击回调,携带完整上下文信息
17
+ ⚡ 指令式调用,接入成本低
11
18
 
12
19
  ## 安装
13
20
 
@@ -16,30 +23,87 @@ npm install vue3-context-menu
16
23
  # 或
17
24
  yarn add vue3-context-menu
18
25
 
26
+ ```
19
27
 
28
+ ## 使用用法
20
29
 
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 点击处理函数 可选
30
+ 一、
31
+ main.js / main.ts 文件中
32
+ import { createApp } from 'vue'
33
+ import App from './App.vue'
34
+ import VueContextMenu from 'vue3-context-menu'
29
35
 
36
+ const app = createApp(App)
37
+ app.use(VueContextMenu) // 注册全局指令
38
+ app.mount('#app')
30
39
 
31
- # 基本用法
32
- # <div v-contextmenu="menus"></div>
40
+ ## 模版
33
41
 
34
- # 设置z-index
35
- # <div v-contextmenu:10000="menus"></div>
42
+ <template>
43
+ <!-- 用法1:v-contextmenu:10(10:当前组件的层级可以调成任何纯数字,如果不写默认9999) + 当第一参数是对象是,需要配置配置data-component匹配菜单 -->
44
+ <div
45
+ data-component="component-a"
46
+ v-contextmenu:10="{ menus: componentMenus,onItemClick: handleClick }"
47
+ >
48
+ 右键点击(组件A)
49
+ </div>
36
50
 
37
- # 使用修饰符
38
- # <div v-contextmenu.auto-close="menus"></div>
51
+ <div
52
+ data-component="component-b"
53
+ v-contextmenu="{ componentMenus, onItemClick: handleClick }"
54
+ >
55
+ 右键点击(组件B)
56
+ </div>
39
57
 
40
- # 全局使用
41
- # import { createApp } from 'vue'
42
- # import VueContextMenu from 'vue3-context-menu'
58
+ <!-- 用法2:自定义菜单(不依赖data-component) -->
59
+ <div
60
+ v-contextmenu="{customMenus, handleClick }"
61
+ >
62
+ 右键点击(自定义菜单)
63
+ </div>
64
+ </template>
43
65
 
44
- # const app = createApp(App)
45
- # app.use(VueContextMenu)
66
+ <script>
67
+ export default {
68
+ data() {
69
+ return {
70
+ // 按组件分类的菜单配置
71
+ componentMenus: {
72
+ 'component-a': [
73
+ { id: 1, label: '编辑组件A', icon: 'icon-edit' },
74
+ { id: 2, label: '删除组件A', icon: 'icon-delete' },
75
+ { id: 3, divider: true }, // 分隔线
76
+ {
77
+ id: 4,
78
+ label: '更多操作',
79
+ children: [ // 多级子菜单
80
+ { id: 5, label: '操作1' },
81
+ { id: 6, label: '操作2' }
82
+ ]
83
+ }
84
+ ],
85
+ 'component-b': [
86
+ { id: 7, label: '查看组件B' },
87
+ { id: 8, label: '导出组件B' }
88
+ ]
89
+ },
90
+ // 自定义菜单配置
91
+ customMenus: [
92
+ { id: 9, label: '自定义菜单1' },
93
+ { id: 10, label: '自定义菜单2', disabled: true }, // 禁用项
94
+ { id: 11, divider: true }, // 分隔线
95
+ { id: 12, label: '刷新' }
96
+ ]
97
+ }
98
+ },
99
+ methods: {
100
+ // 菜单点击回调
101
+ handleClick(item, event, context) {
102
+ console.log('点击的菜单项:', item)
103
+ console.log('原生事件对象:', event)
104
+ console.log('上下文信息:', context)
105
+ // 业务逻辑处理
106
+ }
107
+ }
108
+ }
109
+ </script>
package/dist/index.cjs.js CHANGED
@@ -31,29 +31,35 @@ var _sfc_main = /*@__PURE__*/ vue.defineComponent({
31
31
  visible: { type: Boolean, required: true },
32
32
  x: { type: Number, required: true },
33
33
  y: { type: Number, required: true },
34
- menus: { type: Array, required: true },
34
+ menus: { type: [Object, Array], required: true },
35
35
  zIndex: { type: Number, required: false, default: 9999 },
36
36
  maxWidth: { type: Number, required: false, default: 300 },
37
37
  minWidth: { type: Number, required: false, default: 150 }
38
38
  },
39
39
  emits: ["update:visible", "item-click"],
40
40
  setup(__props, { emit: __emit }) {
41
- const props = __props;
42
41
  const emit = __emit;
43
- const menuRef = vue.ref();
42
+ vue.watch(() => __props.visible, (newVal) => {
43
+ if (!newVal)
44
+ subMenuVisible.value = {};
45
+ });
46
+ const menuRef = vue.useTemplateRef("menuRef");
44
47
  const subMenuVisible = vue.ref({});
45
48
  const subMenuStyle = vue.ref({ top: "0px", left: "0px" });
46
49
  // 菜单样式
47
50
  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`,
51
+ left: `${__props.x}px`,
52
+ top: `${__props.y}px`,
53
+ zIndex: __props.zIndex,
54
+ maxWidth: `${__props.maxWidth}px`,
55
+ minWidth: `${__props.minWidth}px`,
53
56
  }));
54
57
  // 当前显示的菜单
55
- const currentMenus = vue.computed(() => props.menus);
56
- // 点击菜单项
58
+ const currentMenus = vue.computed(() => __props.menus);
59
+ /**
60
+ * 点击菜单项
61
+ * @param item 点击菜单对象
62
+ */
57
63
  const handleClick = (item) => {
58
64
  if (item.disabled)
59
65
  return;
@@ -64,29 +70,35 @@ var _sfc_main = /*@__PURE__*/ vue.defineComponent({
64
70
  emit("update:visible", false);
65
71
  subMenuVisible.value = {};
66
72
  };
67
- // 鼠标进入显示子菜单
73
+ /**
74
+ * 鼠标进入显示子菜单
75
+ * @param item 选中的子菜单
76
+ */
68
77
  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
- }
78
+ if (!(item.children && item.children.length > 0))
79
+ return;
80
+ subMenuVisible.value = { [item.id]: true };
81
+ // 计算子菜单位置
82
+ if (!menuRef.value)
83
+ return;
84
+ const rect = menuRef.value.getBoundingClientRect();
85
+ subMenuStyle.value = {
86
+ top: "0px",
87
+ left: `${rect.width}px`,
88
+ };
80
89
  };
81
- // 点击外部关闭菜单
90
+ /**
91
+ * 点击外部关闭菜单
92
+ * @param event
93
+ */
82
94
  const handleClickOutside = (event) => {
83
- if (menuRef.value && !menuRef.value.contains(event.target)) {
84
- emit("update:visible", false);
85
- subMenuVisible.value = {};
86
- }
95
+ if (!(menuRef.value && !menuRef.value.contains(event.target)))
96
+ return;
97
+ emit("update:visible", false);
98
+ subMenuVisible.value = {};
87
99
  };
88
- // 监听全局点击
89
100
  vue.onMounted(() => {
101
+ // 监听全局点击
90
102
  document.addEventListener("click", handleClickOutside);
91
103
  document.addEventListener("contextmenu", handleClickOutside);
92
104
  });
@@ -94,12 +106,6 @@ var _sfc_main = /*@__PURE__*/ vue.defineComponent({
94
106
  document.removeEventListener("click", handleClickOutside);
95
107
  document.removeEventListener("contextmenu", handleClickOutside);
96
108
  });
97
- // 监听visible变化
98
- vue.watch(() => props.visible, (newVal) => {
99
- if (!newVal) {
100
- subMenuVisible.value = {};
101
- }
102
- });
103
109
  return (_ctx, _cache) => {
104
110
  return (vue.openBlock(), vue.createBlock(vue.Transition, { name: "context-menu-fade" }, {
105
111
  default: vue.withCtx(() => [
@@ -124,10 +130,13 @@ var _sfc_main = /*@__PURE__*/ vue.defineComponent({
124
130
  : (vue.openBlock(), vue.createElementBlock(vue.Fragment, { key: 1 }, [
125
131
  vue.createCommentVNode(" 菜单项 "),
126
132
  vue.createElementVNode("div", {
127
- class: vue.normalizeClass(["context-menu-item", {
133
+ class: vue.normalizeClass([
134
+ 'context-menu-item',
135
+ {
128
136
  disabled: item.disabled,
129
137
  'has-children': item.children && item.children.length > 0,
130
- }]),
138
+ },
139
+ ]),
131
140
  onClick: ($event) => (handleClick(item)),
132
141
  onMouseenter: ($event) => (handleMouseEnter(item))
133
142
  }, [
@@ -227,27 +236,13 @@ var ContextMenu = /* @__PURE__ */ _export_sfc(_sfc_main, [["__scopeId", "data-v-
227
236
  // 全局菜单实例
228
237
  let menuInstance = null;
229
238
  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;
239
+ let currentElement = null;
240
+ let mouseEvent = null;
241
+ let currentOnItemClick = null;
242
+ /**
243
+ * 创建vue模版实例
244
+ */
245
+ const createMenuInstance = () => {
251
246
  container = document.createElement("div");
252
247
  document.body.appendChild(container);
253
248
  menuInstance = vue.createApp({
@@ -269,66 +264,94 @@ function createMenuInstance() {
269
264
  this.visible = val;
270
265
  },
271
266
  onItemClick: (item) => {
272
- // 菜单项点击处理
267
+ if (currentOnItemClick && currentElement && mouseEvent)
268
+ currentOnItemClick(item, currentElement, mouseEvent);
273
269
  },
274
270
  });
275
271
  },
276
272
  });
277
273
  menuInstance.mount(container);
278
- }
279
- // 显示菜单
280
- function showContextMenu(event, menus, options) {
274
+ };
275
+ /**
276
+ * 展示菜单
277
+ * @param event 鼠标事件
278
+ * @param menus 菜单数据
279
+ * @param element 触发菜单的元素
280
+ * @param onItemClick 用户自定义的回调函数
281
+ */
282
+ function showContextMenu(event, menus, element, onItemClick) {
281
283
  if (!menuInstance)
282
284
  createMenuInstance();
285
+ currentOnItemClick = onItemClick || null;
286
+ currentElement = element || event.currentTarget;
287
+ mouseEvent = event;
283
288
  const vm = menuInstance._instance.proxy;
289
+ vm.visible = true;
290
+ vm.menus = menus;
284
291
  vm.x = event.clientX;
285
292
  vm.y = event.clientY;
286
- vm.menus = menus;
287
- vm.visible = true;
288
293
  // 防止菜单超出屏幕
289
294
  requestAnimationFrame(() => {
290
295
  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
- }
296
+ if (!menuEl)
297
+ return;
298
+ const rect = menuEl.getBoundingClientRect();
299
+ const viewportWidth = window.innerWidth;
300
+ const viewportHeight = window.innerHeight;
301
+ if (rect.right > viewportWidth)
302
+ vm.x = viewportWidth - rect.width - 5;
303
+ if (rect.bottom > viewportHeight)
304
+ vm.y = viewportHeight - rect.height - 5;
302
305
  });
303
306
  event.preventDefault();
304
307
  event.stopPropagation();
305
308
  }
306
- // 获取对应组件的菜单
309
+ /**
310
+ * 提取菜单数据
311
+ * @param componentName attribute 标签
312
+ * @param options 数据源
313
+ * @returns 返回组件所需要的数据
314
+ */
307
315
  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
+ const menus = options.menus;
317
+ if (Array.isArray(menus))
318
+ return menus;
319
+ if (!componentName) {
320
+ console.warn("检测到当前实例不存在data-component的标识");
316
321
  }
322
+ return menus[componentName] || [];
317
323
  }
318
- // 指令定义
324
+ /**
325
+ * Vue 自定义指令
326
+ */
319
327
  const contextmenuDirective = {
320
328
  mounted(el, binding) {
329
+ const value = binding.value;
330
+ let menus;
331
+ let onItemClick;
332
+ if (!value || typeof value !== "object") {
333
+ console.warn("v-contextmenu: 需要绑定 object");
334
+ return;
335
+ }
336
+ for (const [key, val] of Object.entries(value)) {
337
+ if (Array.isArray(val) || typeof val === "object") {
338
+ menus = val;
339
+ }
340
+ else if (typeof val === "function") {
341
+ onItemClick = val;
342
+ }
343
+ }
344
+ console.log(binding);
321
345
  const options = {
322
- menus: binding.value,
346
+ menus: menus,
323
347
  zIndex: binding.arg ? parseInt(binding.arg) : 9999,
324
348
  ...binding.modifiers,
325
349
  };
326
350
  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
- }
351
+ const dataComponent = el.getAttribute("data-component");
352
+ const menuList = getMenusForComponent(dataComponent !== null && dataComponent !== void 0 ? dataComponent : "", options);
353
+ if (menuList.length > 0)
354
+ showContextMenu(event, menuList, el, onItemClick);
332
355
  });
333
356
  },
334
357
  };
@@ -1 +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]}
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]}
package/dist/index.esm.js CHANGED
@@ -1,4 +1,4 @@
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';
1
+ import { defineComponent, watch, useTemplateRef, ref, computed, onMounted, onUnmounted, openBlock, createBlock, Transition, withCtx, createElementBlock, normalizeStyle, withModifiers, createElementVNode, Fragment, renderList, createCommentVNode, normalizeClass, toDisplayString, createApp, h } from 'vue';
2
2
 
3
3
  const _hoisted_1 = { class: "context-menu-content" };
4
4
  const _hoisted_2 = {
@@ -27,29 +27,35 @@ var _sfc_main = /*@__PURE__*/ defineComponent({
27
27
  visible: { type: Boolean, required: true },
28
28
  x: { type: Number, required: true },
29
29
  y: { type: Number, required: true },
30
- menus: { type: Array, required: true },
30
+ menus: { type: [Object, Array], required: true },
31
31
  zIndex: { type: Number, required: false, default: 9999 },
32
32
  maxWidth: { type: Number, required: false, default: 300 },
33
33
  minWidth: { type: Number, required: false, default: 150 }
34
34
  },
35
35
  emits: ["update:visible", "item-click"],
36
36
  setup(__props, { emit: __emit }) {
37
- const props = __props;
38
37
  const emit = __emit;
39
- const menuRef = ref();
38
+ watch(() => __props.visible, (newVal) => {
39
+ if (!newVal)
40
+ subMenuVisible.value = {};
41
+ });
42
+ const menuRef = useTemplateRef("menuRef");
40
43
  const subMenuVisible = ref({});
41
44
  const subMenuStyle = ref({ top: "0px", left: "0px" });
42
45
  // 菜单样式
43
46
  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`,
47
+ left: `${__props.x}px`,
48
+ top: `${__props.y}px`,
49
+ zIndex: __props.zIndex,
50
+ maxWidth: `${__props.maxWidth}px`,
51
+ minWidth: `${__props.minWidth}px`,
49
52
  }));
50
53
  // 当前显示的菜单
51
- const currentMenus = computed(() => props.menus);
52
- // 点击菜单项
54
+ const currentMenus = computed(() => __props.menus);
55
+ /**
56
+ * 点击菜单项
57
+ * @param item 点击菜单对象
58
+ */
53
59
  const handleClick = (item) => {
54
60
  if (item.disabled)
55
61
  return;
@@ -60,29 +66,35 @@ var _sfc_main = /*@__PURE__*/ defineComponent({
60
66
  emit("update:visible", false);
61
67
  subMenuVisible.value = {};
62
68
  };
63
- // 鼠标进入显示子菜单
69
+ /**
70
+ * 鼠标进入显示子菜单
71
+ * @param item 选中的子菜单
72
+ */
64
73
  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
- }
74
+ if (!(item.children && item.children.length > 0))
75
+ return;
76
+ subMenuVisible.value = { [item.id]: true };
77
+ // 计算子菜单位置
78
+ if (!menuRef.value)
79
+ return;
80
+ const rect = menuRef.value.getBoundingClientRect();
81
+ subMenuStyle.value = {
82
+ top: "0px",
83
+ left: `${rect.width}px`,
84
+ };
76
85
  };
77
- // 点击外部关闭菜单
86
+ /**
87
+ * 点击外部关闭菜单
88
+ * @param event
89
+ */
78
90
  const handleClickOutside = (event) => {
79
- if (menuRef.value && !menuRef.value.contains(event.target)) {
80
- emit("update:visible", false);
81
- subMenuVisible.value = {};
82
- }
91
+ if (!(menuRef.value && !menuRef.value.contains(event.target)))
92
+ return;
93
+ emit("update:visible", false);
94
+ subMenuVisible.value = {};
83
95
  };
84
- // 监听全局点击
85
96
  onMounted(() => {
97
+ // 监听全局点击
86
98
  document.addEventListener("click", handleClickOutside);
87
99
  document.addEventListener("contextmenu", handleClickOutside);
88
100
  });
@@ -90,12 +102,6 @@ var _sfc_main = /*@__PURE__*/ defineComponent({
90
102
  document.removeEventListener("click", handleClickOutside);
91
103
  document.removeEventListener("contextmenu", handleClickOutside);
92
104
  });
93
- // 监听visible变化
94
- watch(() => props.visible, (newVal) => {
95
- if (!newVal) {
96
- subMenuVisible.value = {};
97
- }
98
- });
99
105
  return (_ctx, _cache) => {
100
106
  return (openBlock(), createBlock(Transition, { name: "context-menu-fade" }, {
101
107
  default: withCtx(() => [
@@ -120,10 +126,13 @@ var _sfc_main = /*@__PURE__*/ defineComponent({
120
126
  : (openBlock(), createElementBlock(Fragment, { key: 1 }, [
121
127
  createCommentVNode(" 菜单项 "),
122
128
  createElementVNode("div", {
123
- class: normalizeClass(["context-menu-item", {
129
+ class: normalizeClass([
130
+ 'context-menu-item',
131
+ {
124
132
  disabled: item.disabled,
125
133
  'has-children': item.children && item.children.length > 0,
126
- }]),
134
+ },
135
+ ]),
127
136
  onClick: ($event) => (handleClick(item)),
128
137
  onMouseenter: ($event) => (handleMouseEnter(item))
129
138
  }, [
@@ -223,27 +232,13 @@ var ContextMenu = /* @__PURE__ */ _export_sfc(_sfc_main, [["__scopeId", "data-v-
223
232
  // 全局菜单实例
224
233
  let menuInstance = null;
225
234
  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;
235
+ let currentElement = null;
236
+ let mouseEvent = null;
237
+ let currentOnItemClick = null;
238
+ /**
239
+ * 创建vue模版实例
240
+ */
241
+ const createMenuInstance = () => {
247
242
  container = document.createElement("div");
248
243
  document.body.appendChild(container);
249
244
  menuInstance = createApp({
@@ -265,66 +260,94 @@ function createMenuInstance() {
265
260
  this.visible = val;
266
261
  },
267
262
  onItemClick: (item) => {
268
- // 菜单项点击处理
263
+ if (currentOnItemClick && currentElement && mouseEvent)
264
+ currentOnItemClick(item, currentElement, mouseEvent);
269
265
  },
270
266
  });
271
267
  },
272
268
  });
273
269
  menuInstance.mount(container);
274
- }
275
- // 显示菜单
276
- function showContextMenu(event, menus, options) {
270
+ };
271
+ /**
272
+ * 展示菜单
273
+ * @param event 鼠标事件
274
+ * @param menus 菜单数据
275
+ * @param element 触发菜单的元素
276
+ * @param onItemClick 用户自定义的回调函数
277
+ */
278
+ function showContextMenu(event, menus, element, onItemClick) {
277
279
  if (!menuInstance)
278
280
  createMenuInstance();
281
+ currentOnItemClick = onItemClick || null;
282
+ currentElement = element || event.currentTarget;
283
+ mouseEvent = event;
279
284
  const vm = menuInstance._instance.proxy;
285
+ vm.visible = true;
286
+ vm.menus = menus;
280
287
  vm.x = event.clientX;
281
288
  vm.y = event.clientY;
282
- vm.menus = menus;
283
- vm.visible = true;
284
289
  // 防止菜单超出屏幕
285
290
  requestAnimationFrame(() => {
286
291
  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
- }
292
+ if (!menuEl)
293
+ return;
294
+ const rect = menuEl.getBoundingClientRect();
295
+ const viewportWidth = window.innerWidth;
296
+ const viewportHeight = window.innerHeight;
297
+ if (rect.right > viewportWidth)
298
+ vm.x = viewportWidth - rect.width - 5;
299
+ if (rect.bottom > viewportHeight)
300
+ vm.y = viewportHeight - rect.height - 5;
298
301
  });
299
302
  event.preventDefault();
300
303
  event.stopPropagation();
301
304
  }
302
- // 获取对应组件的菜单
305
+ /**
306
+ * 提取菜单数据
307
+ * @param componentName attribute 标签
308
+ * @param options 数据源
309
+ * @returns 返回组件所需要的数据
310
+ */
303
311
  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
+ const menus = options.menus;
313
+ if (Array.isArray(menus))
314
+ return menus;
315
+ if (!componentName) {
316
+ console.warn("检测到当前实例不存在data-component的标识");
312
317
  }
318
+ return menus[componentName] || [];
313
319
  }
314
- // 指令定义
320
+ /**
321
+ * Vue 自定义指令
322
+ */
315
323
  const contextmenuDirective = {
316
324
  mounted(el, binding) {
325
+ const value = binding.value;
326
+ let menus;
327
+ let onItemClick;
328
+ if (!value || typeof value !== "object") {
329
+ console.warn("v-contextmenu: 需要绑定 object");
330
+ return;
331
+ }
332
+ for (const [key, val] of Object.entries(value)) {
333
+ if (Array.isArray(val) || typeof val === "object") {
334
+ menus = val;
335
+ }
336
+ else if (typeof val === "function") {
337
+ onItemClick = val;
338
+ }
339
+ }
340
+ console.log(binding);
317
341
  const options = {
318
- menus: binding.value,
342
+ menus: menus,
319
343
  zIndex: binding.arg ? parseInt(binding.arg) : 9999,
320
344
  ...binding.modifiers,
321
345
  };
322
346
  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
- }
347
+ const dataComponent = el.getAttribute("data-component");
348
+ const menuList = getMenusForComponent(dataComponent !== null && dataComponent !== void 0 ? dataComponent : "", options);
349
+ if (menuList.length > 0)
350
+ showContextMenu(event, menuList, el, onItemClick);
328
351
  });
329
352
  },
330
353
  };
@@ -1 +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]}
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]}
package/dist/index.umd.js CHANGED
@@ -31,29 +31,35 @@
31
31
  visible: { type: Boolean, required: true },
32
32
  x: { type: Number, required: true },
33
33
  y: { type: Number, required: true },
34
- menus: { type: Array, required: true },
34
+ menus: { type: [Object, Array], required: true },
35
35
  zIndex: { type: Number, required: false, default: 9999 },
36
36
  maxWidth: { type: Number, required: false, default: 300 },
37
37
  minWidth: { type: Number, required: false, default: 150 }
38
38
  },
39
39
  emits: ["update:visible", "item-click"],
40
40
  setup(__props, { emit: __emit }) {
41
- const props = __props;
42
41
  const emit = __emit;
43
- const menuRef = vue.ref();
42
+ vue.watch(() => __props.visible, (newVal) => {
43
+ if (!newVal)
44
+ subMenuVisible.value = {};
45
+ });
46
+ const menuRef = vue.useTemplateRef("menuRef");
44
47
  const subMenuVisible = vue.ref({});
45
48
  const subMenuStyle = vue.ref({ top: "0px", left: "0px" });
46
49
  // 菜单样式
47
50
  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`,
51
+ left: `${__props.x}px`,
52
+ top: `${__props.y}px`,
53
+ zIndex: __props.zIndex,
54
+ maxWidth: `${__props.maxWidth}px`,
55
+ minWidth: `${__props.minWidth}px`,
53
56
  }));
54
57
  // 当前显示的菜单
55
- const currentMenus = vue.computed(() => props.menus);
56
- // 点击菜单项
58
+ const currentMenus = vue.computed(() => __props.menus);
59
+ /**
60
+ * 点击菜单项
61
+ * @param item 点击菜单对象
62
+ */
57
63
  const handleClick = (item) => {
58
64
  if (item.disabled)
59
65
  return;
@@ -64,29 +70,35 @@
64
70
  emit("update:visible", false);
65
71
  subMenuVisible.value = {};
66
72
  };
67
- // 鼠标进入显示子菜单
73
+ /**
74
+ * 鼠标进入显示子菜单
75
+ * @param item 选中的子菜单
76
+ */
68
77
  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
- }
78
+ if (!(item.children && item.children.length > 0))
79
+ return;
80
+ subMenuVisible.value = { [item.id]: true };
81
+ // 计算子菜单位置
82
+ if (!menuRef.value)
83
+ return;
84
+ const rect = menuRef.value.getBoundingClientRect();
85
+ subMenuStyle.value = {
86
+ top: "0px",
87
+ left: `${rect.width}px`,
88
+ };
80
89
  };
81
- // 点击外部关闭菜单
90
+ /**
91
+ * 点击外部关闭菜单
92
+ * @param event
93
+ */
82
94
  const handleClickOutside = (event) => {
83
- if (menuRef.value && !menuRef.value.contains(event.target)) {
84
- emit("update:visible", false);
85
- subMenuVisible.value = {};
86
- }
95
+ if (!(menuRef.value && !menuRef.value.contains(event.target)))
96
+ return;
97
+ emit("update:visible", false);
98
+ subMenuVisible.value = {};
87
99
  };
88
- // 监听全局点击
89
100
  vue.onMounted(() => {
101
+ // 监听全局点击
90
102
  document.addEventListener("click", handleClickOutside);
91
103
  document.addEventListener("contextmenu", handleClickOutside);
92
104
  });
@@ -94,12 +106,6 @@
94
106
  document.removeEventListener("click", handleClickOutside);
95
107
  document.removeEventListener("contextmenu", handleClickOutside);
96
108
  });
97
- // 监听visible变化
98
- vue.watch(() => props.visible, (newVal) => {
99
- if (!newVal) {
100
- subMenuVisible.value = {};
101
- }
102
- });
103
109
  return (_ctx, _cache) => {
104
110
  return (vue.openBlock(), vue.createBlock(vue.Transition, { name: "context-menu-fade" }, {
105
111
  default: vue.withCtx(() => [
@@ -124,10 +130,13 @@
124
130
  : (vue.openBlock(), vue.createElementBlock(vue.Fragment, { key: 1 }, [
125
131
  vue.createCommentVNode(" 菜单项 "),
126
132
  vue.createElementVNode("div", {
127
- class: vue.normalizeClass(["context-menu-item", {
133
+ class: vue.normalizeClass([
134
+ 'context-menu-item',
135
+ {
128
136
  disabled: item.disabled,
129
137
  'has-children': item.children && item.children.length > 0,
130
- }]),
138
+ },
139
+ ]),
131
140
  onClick: ($event) => (handleClick(item)),
132
141
  onMouseenter: ($event) => (handleMouseEnter(item))
133
142
  }, [
@@ -227,27 +236,13 @@
227
236
  // 全局菜单实例
228
237
  let menuInstance = null;
229
238
  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;
239
+ let currentElement = null;
240
+ let mouseEvent = null;
241
+ let currentOnItemClick = null;
242
+ /**
243
+ * 创建vue模版实例
244
+ */
245
+ const createMenuInstance = () => {
251
246
  container = document.createElement("div");
252
247
  document.body.appendChild(container);
253
248
  menuInstance = vue.createApp({
@@ -269,66 +264,94 @@
269
264
  this.visible = val;
270
265
  },
271
266
  onItemClick: (item) => {
272
- // 菜单项点击处理
267
+ if (currentOnItemClick && currentElement && mouseEvent)
268
+ currentOnItemClick(item, currentElement, mouseEvent);
273
269
  },
274
270
  });
275
271
  },
276
272
  });
277
273
  menuInstance.mount(container);
278
- }
279
- // 显示菜单
280
- function showContextMenu(event, menus, options) {
274
+ };
275
+ /**
276
+ * 展示菜单
277
+ * @param event 鼠标事件
278
+ * @param menus 菜单数据
279
+ * @param element 触发菜单的元素
280
+ * @param onItemClick 用户自定义的回调函数
281
+ */
282
+ function showContextMenu(event, menus, element, onItemClick) {
281
283
  if (!menuInstance)
282
284
  createMenuInstance();
285
+ currentOnItemClick = onItemClick || null;
286
+ currentElement = element || event.currentTarget;
287
+ mouseEvent = event;
283
288
  const vm = menuInstance._instance.proxy;
289
+ vm.visible = true;
290
+ vm.menus = menus;
284
291
  vm.x = event.clientX;
285
292
  vm.y = event.clientY;
286
- vm.menus = menus;
287
- vm.visible = true;
288
293
  // 防止菜单超出屏幕
289
294
  requestAnimationFrame(() => {
290
295
  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
- }
296
+ if (!menuEl)
297
+ return;
298
+ const rect = menuEl.getBoundingClientRect();
299
+ const viewportWidth = window.innerWidth;
300
+ const viewportHeight = window.innerHeight;
301
+ if (rect.right > viewportWidth)
302
+ vm.x = viewportWidth - rect.width - 5;
303
+ if (rect.bottom > viewportHeight)
304
+ vm.y = viewportHeight - rect.height - 5;
302
305
  });
303
306
  event.preventDefault();
304
307
  event.stopPropagation();
305
308
  }
306
- // 获取对应组件的菜单
309
+ /**
310
+ * 提取菜单数据
311
+ * @param componentName attribute 标签
312
+ * @param options 数据源
313
+ * @returns 返回组件所需要的数据
314
+ */
307
315
  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
+ const menus = options.menus;
317
+ if (Array.isArray(menus))
318
+ return menus;
319
+ if (!componentName) {
320
+ console.warn("检测到当前实例不存在data-component的标识");
316
321
  }
322
+ return menus[componentName] || [];
317
323
  }
318
- // 指令定义
324
+ /**
325
+ * Vue 自定义指令
326
+ */
319
327
  const contextmenuDirective = {
320
328
  mounted(el, binding) {
329
+ const value = binding.value;
330
+ let menus;
331
+ let onItemClick;
332
+ if (!value || typeof value !== "object") {
333
+ console.warn("v-contextmenu: 需要绑定 object");
334
+ return;
335
+ }
336
+ for (const [key, val] of Object.entries(value)) {
337
+ if (Array.isArray(val) || typeof val === "object") {
338
+ menus = val;
339
+ }
340
+ else if (typeof val === "function") {
341
+ onItemClick = val;
342
+ }
343
+ }
344
+ console.log(binding);
321
345
  const options = {
322
- menus: binding.value,
346
+ menus: menus,
323
347
  zIndex: binding.arg ? parseInt(binding.arg) : 9999,
324
348
  ...binding.modifiers,
325
349
  };
326
350
  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
- }
351
+ const dataComponent = el.getAttribute("data-component");
352
+ const menuList = getMenusForComponent(dataComponent !== null && dataComponent !== void 0 ? dataComponent : "", options);
353
+ if (menuList.length > 0)
354
+ showContextMenu(event, menuList, el, onItemClick);
332
355
  });
333
356
  },
334
357
  };
@@ -1 +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]}
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]}
@@ -12,7 +12,7 @@ declare const _default: import("vue").DefineComponent<import("vue").ExtractPropT
12
12
  required: true;
13
13
  };
14
14
  menus: {
15
- type: ArrayConstructor;
15
+ type: (ObjectConstructor | ArrayConstructor)[];
16
16
  required: true;
17
17
  };
18
18
  zIndex: {
@@ -46,7 +46,7 @@ declare const _default: import("vue").DefineComponent<import("vue").ExtractPropT
46
46
  required: true;
47
47
  };
48
48
  menus: {
49
- type: ArrayConstructor;
49
+ type: (ObjectConstructor | ArrayConstructor)[];
50
50
  required: true;
51
51
  };
52
52
  zIndex: {
@@ -1,3 +1,6 @@
1
+ /**
2
+ * Vue 自定义指令
3
+ */
1
4
  declare const contextmenuDirective: {
2
5
  mounted(el: HTMLElement, binding: any): void;
3
6
  };
@@ -11,8 +11,13 @@ export interface ComponentMenuMap {
11
11
  [componentName: string]: MenuItem[];
12
12
  }
13
13
  export interface ContextMenuOptions {
14
- menus: ComponentMenuMap | MenuItem[] | ((component: any) => MenuItem[]);
14
+ menus: ComponentMenuMap | MenuItem[];
15
15
  zIndex?: number;
16
16
  maxWidth?: number;
17
17
  minWidth?: number;
18
18
  }
19
+ export interface Props extends ContextMenuOptions {
20
+ visible: boolean;
21
+ x: number;
22
+ y: number;
23
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "vue3-context-menu-plus",
4
- "version": "1.0.1",
4
+ "version": "2.0.0",
5
5
  "description": "A Vue 3 context menu component that shows different menus based on different components",
6
6
  "main": "dist/index.cjs.js",
7
7
  "module": "dist/index.esm.js",
@@ -23,11 +23,10 @@
23
23
  "keywords": [
24
24
  "vue3",
25
25
  "contextmenu",
26
- "right-click",
27
26
  "menu",
28
27
  "component"
29
28
  ],
30
- "author": "Your Name",
29
+ "author": "eggyolkegg",
31
30
  "license": "MIT",
32
31
  "peerDependencies": {
33
32
  "vue": "^3.0.0"
@@ -44,7 +43,7 @@
44
43
  "rollup-plugin-typescript2": "^0.34.0",
45
44
  "rollup-plugin-vue": "^6.0.0",
46
45
  "typescript": "~5.9.0",
47
- "vue": "^3.5.22"
46
+ "vue": "^3.5.25"
48
47
  },
49
48
  "repository": {
50
49
  "type": "git",
@@ -1 +0,0 @@
1
- export declare const defaultUtils: {};