vue-easytable
Version:
732 lines (662 loc) • 26.1 kB
JSX
import { COMPS_NAME } from "./util/constant";
import { clsName } from "./util/index";
import VeIcon from "vue-easytable/packages/ve-icon";
import { ICON_NAMES } from "../../src/utils/constant";
import { getMousePosition, getViewportOffset } from "../../src/utils/dom";
import {
INIT_DATA,
EMIT_EVENTS,
CONTEXTMENU_NODE_TYPES,
INSTANCE_METHODS,
} from "./util/constant";
import { getRandomId } from "../../src/utils/random";
import { debounce, cloneDeep } from "lodash";
import eventsOutside from "../../src/directives/events-outside";
export default {
name: COMPS_NAME.VE_CONTEXTMENU,
directives: {
"events-outside": eventsOutside,
},
props: {
/*
options(contextmenu)
[
{
id: 1,
label: "菜单1",
disabled:true
},
{
id: 2,
label: "菜单2",
children: [
{
id: "2-1",
label: "菜单2-1",
},
{
id: "2-2",
label: "菜单2-2",
},
],
},
]
*/
options: {
type: Array,
required: true,
},
/*
event target
contextmenu event will register on it
*/
eventTarget: {
type: [String, HTMLElement],
required: true,
},
},
data() {
return {
/*
internal options:
[
{
id: 1,
deep: 0,
hasChildren: false,
label: "菜单1",
},
{
id: 2,
label: "菜单2",
deep: 0,
hasChildren: true,
children: [
{
id: "2-1",
deep: 1,
hasChildren: false,
label: "菜单2-1",
},
{
id: "2-2",
deep: 1,
hasChildren: false,
label: "菜单2-2",
},
],
},
]
*/
internalOptions: [],
/*
panels option
{
id: 1,
menus: [
{
id: "",
deep: 0,
label: "菜单1",
hasChildren: true,
},
{
id: "",
deep: 0,
label: "菜单2",
},
],
},
{
id: 2,
menus: [
{
id: "",
deep: 1,
label: "菜单1",
hasChildren: true,
},
{
id: "",
deep: 1,
label: "菜单2",
},
],
},
*/
panelOptions: [],
// event target element
eventTargetEl: "",
// root contextmenu id
rootContextmenuId: "",
/*
is children panels clicked
如果点击了则不关闭 panels
*/
isChildrenPanelsClicked: false,
/*
is panel right direction
决定了子 panel 默认展示方向
*/
isPanelRightDirection: true,
/*
is panels remove
防止hover后菜单被移除,仍然显示子集菜单的问题
*/
isPanelsEmptyed: true,
};
},
computed: {
// active menus ids
activeMenuIds() {
const { panelOptions } = this;
return panelOptions.map((x) => x.parentId);
},
},
watch: {
options: {
handler: function (val) {
if (Array.isArray(val) && val.length > 0) {
/*
如果配置项修改,则重新销毁并创建
*/
this.removeOrEmptyPanels(true);
this.rootContextmenuId = this.getRandomIdWithPrefix();
this.createInternalOptions();
this.createPanelOptions({ options: this.internalOptions });
this.resetContextmenu();
this.addRootContextmenuPanelToBody();
}
},
immediate: true,
},
eventTarget: {
handler: function (val) {
if (val) {
this.registerContextmenuEvent();
}
},
immediate: true,
},
},
methods: {
// get random id
getRandomIdWithPrefix() {
return clsName(getRandomId());
},
// has children
hasChildren(option) {
return Array.isArray(option.children) && option.children.length;
},
/*
get panel option by menu id
*/
getPanelOptionByMenuId(options, menuId) {
for (let i = 0; i < options.length; i++) {
if (options[i].id === menuId) {
return options[i].children;
}
if (options[i].children) {
const panelOption = this.getPanelOptionByMenuId(
options[i].children,
menuId,
);
if (panelOption) return panelOption;
}
}
},
// get parent contextmenu panel element
getParentContextmenuPanelEl(contextmenuPanelId) {
let result;
const { panelOptions } = this;
const panelIndex = panelOptions.findIndex(
(x) => x.parentId === contextmenuPanelId,
);
if (panelIndex > 0) {
// preview panel's panelId
const parentPanelId = panelOptions[panelIndex - 1].parentId;
result = document.querySelector(`#${parentPanelId}`);
}
return result;
},
// create panel by hover
createPanelByHover({ event, menu }) {
const { internalOptions, panelOptions } = this;
// 如果被移除则不创建
if (this.isPanelsEmptyed) {
return false;
}
// has already exists
if (panelOptions.findIndex((x) => x.parentId === menu.id) > -1) {
return false;
}
/*
移除 panel 深度大于等于当前悬浮菜单的。从后往前删除
remove panels
*/
const deletePanelDeeps = panelOptions
.filter((x) => x.parentDeep >= menu.deep)
.map((x) => x.parentDeep)
.reverse();
if (deletePanelDeeps.length) {
for (let i = deletePanelDeeps.length - 1; i >= 0; i--) {
const delIndex = panelOptions.findIndex(
(x) => x.parentDeep === deletePanelDeeps[i],
);
if (delIndex > -1) {
this.panelOptions.splice(delIndex, 1);
}
}
}
const panelOption = this.getPanelOptionByMenuId(
internalOptions,
menu.id,
);
if (panelOption) {
this.createPanelOptions({
options: panelOption,
currentMenu: menu,
});
this.$nextTick(() => {
this.addContextmenuPanelToBody({
contextmenuId: menu.id,
});
this.showContextmenuPanel({
event,
contextmenuId: menu.id,
});
});
}
},
// create panels option
createPanelOptions({ options, currentMenu }) {
const { hasChildren, rootContextmenuId } = this;
if (Array.isArray(options)) {
//
let menus = options.map((option) => {
return {
hasChildren: hasChildren(option),
...option,
};
});
this.panelOptions.push({
parentId: currentMenu ? currentMenu.id : rootContextmenuId,
parentDeep: currentMenu
? currentMenu.deep
: INIT_DATA.PARENT_DEEP,
menus: menus,
});
}
},
// create internal options recursion
createInternalOptionsRecursion(options, deep = 0) {
options.id = this.getRandomIdWithPrefix();
options.deep = deep;
deep++;
if (Array.isArray(options.children)) {
options.children.map((option) => {
return this.createInternalOptionsRecursion(option, deep);
});
}
return options;
},
// create internal options
createInternalOptions() {
this.internalOptions = cloneDeep(this.options).map((option) => {
return this.createInternalOptionsRecursion(option);
});
},
// show root contextmenu panel
showRootContextmenuPanel(event) {
event.preventDefault();
const { rootContextmenuId } = this;
if (rootContextmenuId) {
// refresh contextmenu
this.resetContextmenu();
this.showContextmenuPanel({
event,
contextmenuId: rootContextmenuId,
isRootContextmenu: true,
});
this.isPanelsEmptyed = false;
}
},
// show contextmenu panel
showContextmenuPanel({ event, contextmenuId, isRootContextmenu }) {
const { getParentContextmenuPanelEl } = this;
let contextmenuPanelEl = document.querySelector(
`#${contextmenuId}`,
);
if (contextmenuPanelEl) {
// remove first
contextmenuPanelEl.innerHTML = "";
contextmenuPanelEl.appendChild(this.$refs[contextmenuId]);
contextmenuPanelEl.style.position = "absolute";
contextmenuPanelEl.classList.add(clsName("popper"));
const { width: currentPanelWidth, height: currentPanelHeight } =
contextmenuPanelEl.getBoundingClientRect();
if (isRootContextmenu) {
const {
left: clickLeft,
top: clickTop,
right: clickRight,
bottom: clickBottom,
} = getMousePosition(event);
let panelX = 0;
let panelY = 0;
// 右方宽度够显示
if (clickRight >= currentPanelWidth) {
panelX = clickLeft;
this.isPanelRightDirection = true;
}
// 右方宽度不够显示在鼠标点击左方
else {
panelX = clickLeft - currentPanelWidth;
this.isPanelRightDirection = false;
}
// 下方高度够显示
if (clickBottom >= currentPanelHeight) {
panelY = clickTop;
}
// 下方高度不够显示在鼠标点击上方
else {
panelY = clickTop - currentPanelHeight;
}
contextmenuPanelEl.style.left = panelX + "px";
contextmenuPanelEl.style.top = panelY + "px";
} else {
const parentContextmenuPanelEl =
getParentContextmenuPanelEl(contextmenuId);
if (parentContextmenuPanelEl) {
const {
left: parentPanelLeft,
right: parentPanelRight,
} = getViewportOffset(parentContextmenuPanelEl);
const { top: clickTop, bottom: clickBottom } =
getMousePosition(event);
const { width: parentPanelWidth } =
parentContextmenuPanelEl.getBoundingClientRect();
let panelX = 0;
let panelY = 0;
// 如果默认展示在右方向
if (this.isPanelRightDirection) {
// 右方宽度够显示
if (parentPanelRight >= currentPanelWidth) {
panelX = parentPanelLeft + parentPanelWidth;
}
// 右方宽度不够显示在鼠标点击左方
else {
panelX = parentPanelLeft - parentPanelWidth;
}
}
// 如果默认展示在左方向
else {
// 左方宽度够显示
if (parentPanelLeft >= currentPanelWidth) {
panelX = parentPanelLeft - parentPanelWidth;
}
// 左方宽度不够显示在鼠标点击右方
else {
panelX = parentPanelLeft + parentPanelWidth;
}
}
// 下方高度够显示
if (clickBottom >= currentPanelHeight) {
panelY = clickTop;
}
// 下方高度不够显示在鼠标点击上方
else {
panelY = clickTop - currentPanelHeight;
}
contextmenuPanelEl.style.left = panelX + "px";
contextmenuPanelEl.style.top = panelY + "px";
}
}
}
},
// empty contextmenu panels
emptyContextmenuPanels() {
/*
wait for children panel clicked by setTimeout
如果点击的是非 root panel 不关闭
*/
setTimeout(() => {
if (this.isChildrenPanelsClicked) {
this.isChildrenPanelsClicked = false;
} else {
this.removeOrEmptyPanels();
this.isPanelsEmptyed = true;
}
});
},
// remove or empty panels
removeOrEmptyPanels(isRemove) {
const { panelOptions } = this;
panelOptions.forEach((panelOption) => {
let contextmenuPanelEl = document.querySelector(
`#${panelOption.parentId}`,
);
if (contextmenuPanelEl) {
if (isRemove) {
contextmenuPanelEl.remove();
} else {
contextmenuPanelEl.innerHTML = "";
}
}
});
},
// reset contextmeny
resetContextmenu() {
this.panelOptions = [];
this.createPanelOptions({ options: this.internalOptions });
},
// add context menu panel to body
addContextmenuPanelToBody({ contextmenuId }) {
let contextmenuPanelEl = document.querySelector(
`#${contextmenuId}`,
);
if (contextmenuPanelEl) {
return false;
} else {
let containerEl = document.createElement("div");
containerEl.setAttribute("id", contextmenuId);
document.body.appendChild(containerEl);
}
},
// add root contextmenu panel to body
addRootContextmenuPanelToBody() {
if (this.rootContextmenuId) {
this.addContextmenuPanelToBody({
contextmenuId: this.rootContextmenuId,
});
}
},
// register contextmenu event
registerContextmenuEvent() {
const { eventTarget } = this;
if (typeof eventTarget === "string" && eventTarget.length > 0) {
this.eventTargetEl = document.querySelector(eventTarget);
} else {
this.eventTargetEl = eventTarget;
}
if (this.eventTargetEl) {
// contextmenu is on the current element
this.eventTargetEl.addEventListener(
"contextmenu",
this.showRootContextmenuPanel,
);
}
},
// unregister contextmen event
removeContextmenuEvent() {
if (this.eventTargetEl) {
this.eventTargetEl.removeEventListener(
"contextmenu",
this.showRootContextmenuPanel,
);
}
},
// hide contextmenu
[INSTANCE_METHODS.HIDE_CONTEXTMENU]() {
this.emptyContextmenuPanels();
},
},
created() {
// bug fixed #467
this.debounceCreatePanelByHover = debounce(
this.createPanelByHover,
300,
);
},
mounted() {
this.addRootContextmenuPanelToBody();
},
destroyed() {
this.removeContextmenuEvent();
this.removeOrEmptyPanels(true);
},
render() {
const {
panelOptions,
activeMenuIds,
hasChildren,
emptyContextmenuPanels,
debounceCreatePanelByHover,
} = this;
const contextmenuProps = {
class: ["ve-contextmenu"],
style: {
display: "none",
},
};
return (
<div {...contextmenuProps}>
{panelOptions.map((panelOption, panelIndex) => {
const contextmenuPanelProps = {
ref: panelOption.parentId,
class: {
[clsName("panel")]: true,
},
directives: [
{
name: "events-outside",
value: {
events: ["click"],
callback: (e) => {
// only for root panel
if (panelIndex === 0) {
emptyContextmenuPanels();
}
},
},
},
],
on: {
click: () => {
if (panelIndex !== 0) {
this.isChildrenPanelsClicked = true;
}
},
contextmenu: (e) => {
e.preventDefault();
},
},
};
return (
<div {...contextmenuPanelProps}>
<ul class={clsName("list")}>
{panelOption.menus.map((menu) => {
let contextmenuNodeProps;
if (
menu.type !==
CONTEXTMENU_NODE_TYPES.SEPARATOR
) {
contextmenuNodeProps = {
class: {
[clsName("node")]: true,
[clsName("node-active")]:
activeMenuIds.includes(
menu.id,
),
[clsName("node-disabled")]:
menu.disabled,
},
on: {
mouseover: (event) => {
// disable
if (!menu.disabled) {
debounceCreatePanelByHover(
{
event,
menu,
},
);
}
},
click: () => {
if (
!menu.disabled &&
!hasChildren(menu)
) {
this.$emit(
EMIT_EVENTS.ON_NODE_CLICK,
menu.type,
);
setTimeout(() => {
emptyContextmenuPanels();
}, 50);
}
},
},
};
}
// separator
else {
//
contextmenuNodeProps = {
class: {
[clsName(
"node-separator",
)]: true,
},
};
}
if (
menu.type !==
CONTEXTMENU_NODE_TYPES.SEPARATOR
) {
return (
<li {...contextmenuNodeProps}>
<span
class={clsName(
"node-label",
)}
>
{menu.label}
</span>
{menu.hasChildren && (
<VeIcon
class={clsName(
"node-icon-postfix",
)}
name={
ICON_NAMES.RIGHT_ARROW
}
/>
)}
</li>
);
} else {
return (
<li {...contextmenuNodeProps}></li>
);
}
})}
</ul>
</div>
);
})}
</div>
);
},
};