UNPKG

vue-easytable

Version:
732 lines (662 loc) 26.1 kB
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> ); }, };