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 +86 -22
- package/dist/index.cjs.js +113 -90
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.esm.js +114 -91
- package/dist/index.esm.js.map +1 -1
- package/dist/index.umd.js +113 -90
- package/dist/index.umd.js.map +1 -1
- package/dist/types/components/ContextMenu.vue.d.ts +2 -2
- package/dist/types/directives/contextmenu.d.ts +3 -0
- package/dist/types/types/index.d.ts +6 -1
- package/package.json +3 -4
- package/dist/types/utils/index.d.ts +0 -1
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-
|
|
6
|
-
* @Description:
|
|
5
|
+
* @LastEditTime: 2025-12-17 15:13:45
|
|
6
|
+
* @Description:
|
|
7
7
|
-->
|
|
8
|
+
|
|
8
9
|
# Vue3 Context Menu
|
|
9
10
|
|
|
10
|
-
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
35
|
-
|
|
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
|
-
|
|
51
|
+
<div
|
|
52
|
+
data-component="component-b"
|
|
53
|
+
v-contextmenu="{ componentMenus, onItemClick: handleClick }"
|
|
54
|
+
>
|
|
55
|
+
右键点击(组件B)
|
|
56
|
+
</div>
|
|
39
57
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
58
|
+
<!-- 用法2:自定义菜单(不依赖data-component) -->
|
|
59
|
+
<div
|
|
60
|
+
v-contextmenu="{customMenus, handleClick }"
|
|
61
|
+
>
|
|
62
|
+
右键点击(自定义菜单)
|
|
63
|
+
</div>
|
|
64
|
+
</template>
|
|
43
65
|
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
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: `${
|
|
49
|
-
top: `${
|
|
50
|
-
zIndex:
|
|
51
|
-
maxWidth: `${
|
|
52
|
-
minWidth: `${
|
|
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(() =>
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
85
|
-
|
|
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([
|
|
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
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
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
|
-
|
|
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
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
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
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
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:
|
|
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
|
|
328
|
-
const
|
|
329
|
-
if (
|
|
330
|
-
showContextMenu(event,
|
|
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
|
};
|
package/dist/index.cjs.js.map
CHANGED
|
@@ -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":"
|
|
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,
|
|
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
|
-
|
|
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: `${
|
|
45
|
-
top: `${
|
|
46
|
-
zIndex:
|
|
47
|
-
maxWidth: `${
|
|
48
|
-
minWidth: `${
|
|
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(() =>
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
|
|
81
|
-
|
|
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([
|
|
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
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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
|
-
|
|
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
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
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
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
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:
|
|
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
|
|
324
|
-
const
|
|
325
|
-
if (
|
|
326
|
-
showContextMenu(event,
|
|
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
|
};
|
package/dist/index.esm.js.map
CHANGED
|
@@ -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":"
|
|
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
|
-
|
|
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: `${
|
|
49
|
-
top: `${
|
|
50
|
-
zIndex:
|
|
51
|
-
maxWidth: `${
|
|
52
|
-
minWidth: `${
|
|
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(() =>
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
85
|
-
|
|
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([
|
|
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
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
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
|
-
|
|
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
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
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
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
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:
|
|
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
|
|
328
|
-
const
|
|
329
|
-
if (
|
|
330
|
-
showContextMenu(event,
|
|
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
|
};
|
package/dist/index.umd.js.map
CHANGED
|
@@ -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":"
|
|
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: {
|
|
@@ -11,8 +11,13 @@ export interface ComponentMenuMap {
|
|
|
11
11
|
[componentName: string]: MenuItem[];
|
|
12
12
|
}
|
|
13
13
|
export interface ContextMenuOptions {
|
|
14
|
-
menus: ComponentMenuMap | 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": "
|
|
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": "
|
|
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.
|
|
46
|
+
"vue": "^3.5.25"
|
|
48
47
|
},
|
|
49
48
|
"repository": {
|
|
50
49
|
"type": "git",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare const defaultUtils: {};
|