vxe-pc-ui
Version:
A vue based PC component library
636 lines (635 loc) • 25.4 kB
JavaScript
import { ref, h, reactive, computed, watch, onMounted, onBeforeUnmount, nextTick } from 'vue';
import { defineVxeComponent } from '../../ui/src/comp';
import XEUtils from 'xe-utils';
import { getConfig, getIcon, getI18n, createEvent, useSize, globalEvents, renderEmptyElement, GLOBAL_EVENT_KEYS } from '../../ui';
import { getLastZIndex, nextSubZIndex, nextZIndex, getFuncText } from '../../ui/src/utils';
import { getDomNode, getEventTargetNode, toCssUnit } from '../../ui/src/dom';
import { getSlotVNs } from '../../ui/src/vn';
function createInternalData() {
return {
// leaveTime: null
};
}
function createReactData() {
return {
visible: false,
activeOption: null,
activeChildOption: null,
popupStyle: {
top: '',
left: '',
zIndex: 0
},
childOffsetX: 0
};
}
export default defineVxeComponent({
name: 'VxeContextMenu',
props: {
modelValue: Boolean,
className: String,
size: {
type: String,
default: () => getConfig().contextMenu.size || getConfig().size
},
options: Array,
x: [Number, String],
y: [Number, String],
autoLocate: {
type: Boolean,
default: () => getConfig().contextMenu.autoLocate
},
zIndex: [Number, String],
position: {
type: String,
default: () => getConfig().contextMenu.position
},
destroyOnClose: {
type: Boolean,
default: () => getConfig().contextMenu.destroyOnClose
},
transfer: {
type: Boolean,
default: () => getConfig().contextMenu.transfer
}
},
emits: [
'update:modelValue',
'option-click',
'change',
'show',
'hide'
],
setup(props, context) {
const { emit } = context;
const xID = XEUtils.uniqueId();
const refElem = ref();
const { computeSize } = useSize(props);
const internalData = createInternalData();
const reactData = reactive(createReactData());
const refMaps = {
refElem
};
const computeMenuGroups = computed(() => {
const { options } = props;
return options || [];
});
const computeAllFirstMenuList = computed(() => {
const menuGroups = computeMenuGroups.value;
const firstList = [];
for (let i = 0; i < menuGroups.length; i++) {
const list = menuGroups[i];
for (let j = 0; j < list.length; j++) {
const firstItem = list[j];
if (hasValidItem(firstItem)) {
firstList.push(firstItem);
}
}
}
return firstList;
});
const computeTopAndLeft = computed(() => {
const { x, y } = props;
return `${x}${y}`;
});
const computeMaps = {};
const $xeContextMenu = {
xID,
props,
context,
reactData,
getRefMaps: () => refMaps,
getComputeMaps: () => computeMaps
};
const dispatchEvent = (type, params, evnt) => {
emit(type, createEvent(evnt, { $contextMenu: $xeContextMenu }, params));
};
const emitModel = (value) => {
emit('update:modelValue', value);
};
const openMenu = () => {
const { modelValue } = props;
const { visible } = reactData;
const value = true;
reactData.visible = value;
handleLocate();
updateZindex();
if (modelValue !== value) {
emitModel(value);
dispatchEvent('change', { value }, null);
}
if (visible !== value) {
dispatchEvent('show', { visible: value }, null);
}
return nextTick().then(() => {
updateLocate();
});
};
const closeMenu = () => {
const { modelValue } = props;
const { visible } = reactData;
const value = false;
reactData.visible = value;
if (modelValue !== value) {
emitModel(value);
dispatchEvent('change', { value }, null);
}
if (visible !== value) {
dispatchEvent('hide', { visible: value }, null);
}
return nextTick();
};
const handleLocate = () => {
const { x, y } = props;
const { popupStyle } = reactData;
popupStyle.left = toCssUnit(x || 0);
popupStyle.top = toCssUnit(y || 0);
updateLocate();
};
const updateZindex = () => {
const { zIndex, transfer } = props;
const { popupStyle } = reactData;
const menuZIndex = popupStyle.zIndex;
if (zIndex) {
popupStyle.zIndex = XEUtils.toNumber(zIndex);
}
else {
if (menuZIndex < getLastZIndex()) {
popupStyle.zIndex = transfer ? nextSubZIndex() : nextZIndex();
}
}
};
const updateLocate = () => {
const { autoLocate, position } = props;
const { popupStyle } = reactData;
if (autoLocate) {
const wrapperEl = refElem.value;
if (wrapperEl) {
const { visibleWidth, visibleHeight } = getDomNode();
const wrapperStyle = getComputedStyle(wrapperEl);
const offsetTop = XEUtils.toNumber(wrapperStyle.top);
const offsetLeft = XEUtils.toNumber(wrapperStyle.left);
const wrapperWidth = wrapperEl.offsetWidth;
const wrapperHeight = wrapperEl.offsetHeight;
if (position === 'absolute') {
//
}
else {
if (offsetTop + wrapperHeight > visibleHeight) {
popupStyle.top = `${Math.max(0, offsetTop - wrapperHeight)}px`;
}
if (offsetLeft + wrapperWidth > visibleWidth) {
popupStyle.left = `${Math.max(0, offsetLeft - wrapperWidth)}px`;
}
}
}
}
updateChildLocate();
};
const updateChildLocate = () => {
const wrapperEl = refElem.value;
if (wrapperEl) {
const { visibleWidth } = getDomNode();
const owSize = 4;
const handleStyle = () => {
const wrapperStyle = getComputedStyle(wrapperEl);
const offsetLeft = XEUtils.toNumber(wrapperStyle.left);
const wrapperWidth = wrapperEl.offsetWidth;
const childEl = wrapperEl.querySelector('.vxe-context-menu--children-wrapper');
const childWidth = childEl ? childEl.offsetWidth : wrapperWidth;
if ((offsetLeft + wrapperWidth) > (visibleWidth - childWidth)) {
// 往左
reactData.childOffsetX = -childWidth + owSize;
}
else {
// 往右
reactData.childOffsetX = wrapperWidth - owSize;
}
};
handleStyle();
nextTick(() => {
handleStyle();
});
}
};
const handleVisible = () => {
const { modelValue } = props;
if (modelValue) {
openMenu();
}
else {
closeMenu();
}
};
const tagMethods = {
dispatchEvent,
open: openMenu,
close: closeMenu
};
const hasChildMenu = (item) => {
const { children } = item;
return children && children.some((child) => child.visible !== false);
};
const handleItemClickEvent = (evnt, item) => {
evnt.preventDefault();
evnt.stopPropagation();
if (!item.disabled && !item.loading && !hasChildMenu(item)) {
dispatchEvent('option-click', { option: item }, evnt);
closeMenu();
}
};
const handleItemMouseenterEvent = (evnt, item, parentitem) => {
const { leaveTime } = internalData;
if (leaveTime) {
clearTimeout(leaveTime);
}
reactData.activeOption = parentitem || item;
if (parentitem) {
reactData.activeOption = parentitem;
reactData.activeChildOption = item;
}
else {
reactData.activeOption = item;
if (hasChildMenu(item)) {
reactData.activeChildOption = findFirstChildItem(item);
nextTick(() => {
updateChildLocate();
});
}
else {
reactData.activeChildOption = null;
}
}
};
const handleItemMouseleaveEvent = () => {
const { leaveTime } = internalData;
if (leaveTime) {
clearTimeout(leaveTime);
}
internalData.leaveTime = setTimeout(() => {
internalData.leaveTime = null;
reactData.activeOption = null;
reactData.activeChildOption = null;
}, 300);
};
const hasValidItem = (item) => {
return !item.loading && !item.disabled && item.visible !== false;
};
const findNextFirstItem = (allFirstList, firstItem) => {
for (let i = 0; i < allFirstList.length; i++) {
const item = allFirstList[i];
if (firstItem === item) {
const nextItem = allFirstList[i + 1];
if (nextItem) {
return nextItem;
}
}
}
return XEUtils.first(allFirstList);
};
const findPrevFirstItem = (allFirstList, firstItem) => {
for (let i = 0; i < allFirstList.length; i++) {
const item = allFirstList[i];
if (firstItem === item) {
if (i > 0) {
return allFirstList[i - 1];
}
}
}
return XEUtils.last(allFirstList);
};
const findFirstChildItem = (firstItem) => {
const { children } = firstItem;
if (children) {
for (let i = 0; i < children.length; i++) {
const item = children[i];
if (hasValidItem(item)) {
return item;
}
}
}
return null;
};
const findPrevChildItem = (firstItem, childItem) => {
const { children } = firstItem;
let prevValidItem = null;
if (children) {
for (let i = 0; i < children.length; i++) {
const item = children[i];
if (childItem === item) {
break;
}
if (hasValidItem(item)) {
prevValidItem = item;
}
}
if (!prevValidItem) {
for (let len = children.length - 1; len >= 0; len--) {
const item = children[len];
if (hasValidItem(item)) {
return item;
}
}
}
}
return prevValidItem;
};
const findNextChildItem = (firstItem, childItem) => {
const { children } = firstItem;
let firstValidItem = null;
if (children) {
let isMetch = false;
for (let i = 0; i < children.length; i++) {
const item = children[i];
if (!firstValidItem) {
if (hasValidItem(item)) {
firstValidItem = item;
}
}
if (isMetch) {
if (hasValidItem(item)) {
return item;
}
}
else {
isMetch = childItem === item;
}
}
}
return firstValidItem;
};
const handleGlobalMousewheelEvent = (evnt) => {
const { visible } = reactData;
if (visible) {
const el = refElem.value;
if (!getEventTargetNode(evnt, el, '').flag) {
closeMenu();
}
}
};
const handleGlobalKeydownEvent = (evnt) => {
const { visible, activeOption, activeChildOption } = reactData;
const allFirstMenuList = computeAllFirstMenuList.value;
if (visible) {
const isUpArrow = globalEvents.hasKey(evnt, GLOBAL_EVENT_KEYS.ARROW_UP);
const isDwArrow = globalEvents.hasKey(evnt, GLOBAL_EVENT_KEYS.ARROW_DOWN);
const isLeftArrow = globalEvents.hasKey(evnt, GLOBAL_EVENT_KEYS.ARROW_LEFT);
const isRightArrow = globalEvents.hasKey(evnt, GLOBAL_EVENT_KEYS.ARROW_RIGHT);
const isEnter = globalEvents.hasKey(evnt, GLOBAL_EVENT_KEYS.ENTER);
const isEsc = globalEvents.hasKey(evnt, GLOBAL_EVENT_KEYS.ESCAPE);
if (isEsc) {
closeMenu();
return;
}
// 回车选中
if (isEnter) {
if (activeOption || activeChildOption) {
evnt.preventDefault();
evnt.stopPropagation();
if (!activeChildOption && hasChildMenu(activeOption)) {
reactData.activeChildOption = findFirstChildItem(activeOption);
updateChildLocate();
return;
}
handleItemClickEvent(evnt, activeChildOption || activeOption);
return;
}
}
// 方向键操作
if (activeChildOption) {
if (isUpArrow) {
evnt.preventDefault();
reactData.activeChildOption = findPrevChildItem(activeOption, activeChildOption);
updateChildLocate();
}
else if (isDwArrow) {
evnt.preventDefault();
reactData.activeChildOption = findNextChildItem(activeOption, activeChildOption);
updateChildLocate();
}
else if (isLeftArrow) {
evnt.preventDefault();
reactData.activeChildOption = null;
}
}
else if (activeOption) {
evnt.preventDefault();
if (isUpArrow) {
reactData.activeOption = findPrevFirstItem(allFirstMenuList, activeOption);
}
else if (isDwArrow) {
reactData.activeOption = findNextFirstItem(allFirstMenuList, activeOption);
}
else {
if (hasChildMenu(activeOption)) {
if (isRightArrow) {
reactData.activeChildOption = findFirstChildItem(activeOption);
updateChildLocate();
}
}
}
}
else {
evnt.preventDefault();
reactData.activeOption = XEUtils.first(allFirstMenuList);
}
}
};
const handleGlobalMousedownEvent = (evnt) => {
const { visible } = reactData;
if (visible) {
const el = refElem.value;
if (!getEventTargetNode(evnt, el, '').flag) {
closeMenu();
}
}
};
const handleGlobalBlurEvent = () => {
const { visible } = reactData;
if (visible) {
closeMenu();
}
};
const tagPrivateMethods = {};
Object.assign($xeContextMenu, tagMethods, tagPrivateMethods);
const renderMenuItem = (item, parentItem, hasChildMenus) => {
const { visible, disabled, loading } = item;
if (visible === false) {
return renderEmptyElement($xeContextMenu);
}
const prefixOpts = Object.assign({}, item.prefixConfig);
const prefixIcon = prefixOpts.icon || item.prefixIcon;
const suffixOpts = Object.assign({}, item.suffixConfig);
const suffixIcon = suffixOpts.icon || item.suffixIcon;
const menuContent = loading ? getI18n('vxe.contextMenu.loadingText') : getFuncText(item.name);
return h('div', {
class: ['vxe-context-menu--item-inner', {
'is--disabled': disabled,
'is--loading': loading
}],
onClick(evnt) {
handleItemClickEvent(evnt, item);
},
onMouseenter(evnt) {
handleItemMouseenterEvent(evnt, item, parentItem);
},
onMouseleave: handleItemMouseleaveEvent
}, [
h('div', {
class: ['vxe-context-menu--item-prefix', prefixOpts.className || '']
}, loading
? [
h('span', {
key: '1'
}, [
h('i', {
class: getIcon().CONTEXT_MENU_OPTION_LOADING
})
])
]
: [
prefixIcon && XEUtils.isFunction(prefixIcon)
? h('span', {
key: '2'
}, getSlotVNs(prefixIcon({})))
: h('span', {
key: '3'
}, [
h('i', {
class: prefixIcon
})
]),
prefixOpts.content
? h('span', {
key: '4'
}, `${prefixOpts.content || ''}`)
: renderEmptyElement($xeContextMenu)
]),
h('div', {
class: 'vxe-context-menu--item-label'
}, menuContent),
!loading && (suffixIcon || suffixOpts.content)
? h('div', {
class: ['vxe-context-menu--item-suffix', suffixOpts.className || '']
}, [
suffixIcon && XEUtils.isFunction(suffixIcon)
? h('span', {
key: '2'
}, getSlotVNs(suffixIcon({})))
: h('span', {
key: '3'
}, [
h('i', {
class: suffixIcon
})
]),
suffixOpts.content
? h('span', {
key: '4'
}, `${suffixOpts.content || ''}`)
: renderEmptyElement($xeContextMenu)
])
: renderEmptyElement($xeContextMenu),
hasChildMenus
? h('div', {
class: 'vxe-context-menu--item-subicon'
}, [
h('i', {
class: getIcon().CONTEXT_MENU_CHILDREN
})
])
: renderEmptyElement($xeContextMenu)
]);
};
const renderMenus = () => {
const { activeOption, activeChildOption, childOffsetX } = reactData;
const menuGroups = computeMenuGroups.value;
const mgVNs = [];
menuGroups.forEach((menuList, gIndex) => {
const moVNs = [];
menuList.forEach((firstItem, i) => {
const { children } = firstItem;
const hasChildMenus = children && children.some((child) => child.visible !== false);
const isActiveFirst = activeOption === firstItem;
const showChild = isActiveFirst && !!activeChildOption;
moVNs.push(h('div', {
key: `${gIndex}_${i}`,
class: ['vxe-context-menu--item-wrapper vxe-context-menu--first-item', firstItem.className || '', {
'is--active': isActiveFirst,
'is--subactive': isActiveFirst && !!activeChildOption
}]
}, [
hasChildMenus && showChild
? h('div', {
class: 'vxe-context-menu--children-wrapper',
style: {
transform: `translate(${childOffsetX}px, -2px)`
}
}, children.map(twoItem => {
return h('div', {
class: ['vxe-context-menu--item-wrapper vxe-context-menu--child-item', twoItem.className || '', {
'is--active': activeChildOption === twoItem
}]
}, [
renderMenuItem(twoItem, firstItem)
]);
}))
: renderEmptyElement($xeContextMenu),
renderMenuItem(firstItem, null, hasChildMenus)
]));
});
mgVNs.push(h('div', {
key: gIndex,
class: 'vxe-context-menu--group-wrapper'
}, moVNs));
});
return mgVNs;
};
const renderVN = () => {
const { className, position, destroyOnClose } = props;
const { visible, popupStyle } = reactData;
const vSize = computeSize.value;
return h('div', {
ref: refElem,
class: ['vxe-context-menu vxe-context-menu--wrapper', position === 'absolute' ? ('is--' + position) : 'is--fixed', className || '', {
[`size--${vSize}`]: vSize,
'is--visible': visible
}],
style: popupStyle
}, (destroyOnClose ? visible : true) ? renderMenus() : []);
};
watch(computeTopAndLeft, () => {
handleLocate();
nextTick(() => {
updateLocate();
});
});
watch(() => props.modelValue, () => {
handleVisible();
});
handleVisible();
onMounted(() => {
globalEvents.on($xeContextMenu, 'mousewheel', handleGlobalMousewheelEvent);
globalEvents.on($xeContextMenu, 'keydown', handleGlobalKeydownEvent);
globalEvents.on($xeContextMenu, 'mousedown', handleGlobalMousedownEvent);
globalEvents.on($xeContextMenu, 'blur', handleGlobalBlurEvent);
});
onBeforeUnmount(() => {
const { leaveTime } = internalData;
if (leaveTime) {
clearTimeout(leaveTime);
}
globalEvents.off($xeContextMenu, 'mousewheel');
globalEvents.off($xeContextMenu, 'keydown');
globalEvents.off($xeContextMenu, 'mousedown');
globalEvents.off($xeContextMenu, 'blur');
XEUtils.assign(reactData, createReactData());
XEUtils.assign(internalData, createInternalData());
});
$xeContextMenu.renderVN = renderVN;
return $xeContextMenu;
},
render() {
return this.renderVN();
}
});