e-virt-table
Version:
A powerful data table based on canvas. You can use it as data grid、Microsoft Excel or Google sheets. It supports virtual scroll、cell edit etc.
277 lines • 11.6 kB
JavaScript
import { DOMTreeMenu } from './DOMTreeMenu';
import { toLeaf } from './util';
import { autoUpdate, computePosition, flip, offset, shift } from '@floating-ui/dom';
export default class ContextMenu {
constructor(ctx) {
Object.defineProperty(this, "ctx", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "contextMenuEl", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "currentDOMTreeMenu", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "isCustom", {
enumerable: true,
configurable: true,
writable: true,
value: false
});
this.ctx = ctx;
if (this.ctx.contextMenuElement) {
this.contextMenuEl = this.ctx.contextMenuElement;
this.contextMenuEl.className = 'e-virt-table-main-menu';
this.isCustom = true;
}
else {
this.contextMenuEl = document.createElement('div');
this.isCustom = false;
}
this.ctx.containerElement.appendChild(this.contextMenuEl);
this.init();
}
init() {
this.ctx.on('outsideMousedown', () => {
this.hide();
});
this.ctx.on('cellContextMenuClick', (cell, e) => {
if (this.isCustom) {
this.contextMenuEl.style.display = 'block';
this.positionMenu(e);
return;
}
const { ENABLE_CONTEXT_MENU, CUSTOM_BODY_CONTEXT_MENU, CONTEXT_MENU } = this.ctx.config;
const list = [...CONTEXT_MENU, ...CUSTOM_BODY_CONTEXT_MENU];
if (!ENABLE_CONTEXT_MENU || list.length === 0)
return;
e.preventDefault();
const { xArr, yArr } = this.ctx.selector;
const [minX, maxX] = xArr;
const [minY, maxY] = yArr;
const { rowIndex, colIndex } = cell;
//判断是否在范围内
const isInRange = rowIndex >= minY && rowIndex <= maxY && colIndex >= minX && colIndex <= maxX;
if (!isInRange) {
this.ctx.emit('setSelectorCell', cell, e);
}
// 先清理之前的菜单实例
if (this.currentDOMTreeMenu) {
this.currentDOMTreeMenu.destroy();
}
this.ctx.contextMenuIng = true;
this.currentDOMTreeMenu = new DOMTreeMenu(this.contextMenuEl, list, {
onClick: (_itemData, value) => {
if (value === 'copy') {
this.ctx.emit('contextMenuCopy');
this.hide();
}
else if (value === 'paste') {
this.ctx.emit('contextMenuPaste');
this.hide();
}
else if (value === 'cut') {
this.ctx.emit('contextMenuCut');
this.hide();
}
else if (value === 'clearSelected') {
this.ctx.emit('contextMenuClearSelected');
this.hide();
}
},
});
this.currentDOMTreeMenu.positionMenu(e);
});
this.ctx.on('cellHeaderContextMenuClick', (cell, e) => {
if (this.isCustom) {
this.contextMenuEl.style.display = 'block';
this.positionMenu(e);
return;
}
// 判断是否在范围内
const { SELECTOR_AREA_MIN_X, SELECTOR_AREA_MAX_X, SELECTOR_AREA_MAX_X_OFFSET } = this.ctx.config;
const areaMinX = SELECTOR_AREA_MIN_X;
const areaMaxX = SELECTOR_AREA_MAX_X || this.ctx.maxColIndex - SELECTOR_AREA_MAX_X_OFFSET;
if (cell.colIndex < areaMinX || cell.colIndex > areaMaxX) {
return;
}
const { HEADER_CONTEXT_MENU, CUSTOM_HEADER_CONTEXT_MENU, ENABLE_HEADER_CONTEXT_MENU } = this.ctx.config;
const list = [...HEADER_CONTEXT_MENU, ...CUSTOM_HEADER_CONTEXT_MENU];
if (!ENABLE_HEADER_CONTEXT_MENU || list.length === 0)
return;
e.preventDefault();
const { xArr } = this.ctx.selector;
const [minX, maxX] = xArr;
const { colIndex } = cell;
//判断是否在范围内
const isInRange = colIndex >= minX && colIndex <= maxX;
if (!isInRange) {
this.ctx.focusCellHeader = cell;
this.ctx.emit('selectCols', cell);
}
if (this.currentDOMTreeMenu) {
this.currentDOMTreeMenu.destroy();
}
const columns = this.ctx.database.getColumns();
const menuData = list.map((item) => {
if (item.value === 'visible') {
return {
...item,
children: this.filterColumns(columns),
};
}
return item;
});
this.ctx.contextMenuIng = true;
this.currentDOMTreeMenu = new DOMTreeMenu(this.contextMenuEl, menuData, {
onClick: (_itemData, value) => {
const { xArr } = this.ctx.selector;
const [minX, maxX] = xArr;
if (value === 'fixedLeft' || value === 'fixedRight' || value === 'fixedNone') {
const fixedKeys = this.ctx.header.allCellHeaders
.filter((item) => item.colIndex >= minX && item.colIndex <= maxX)
.filter((item) => item.level === 0)
.filter((item) => !item.column.fixedDisabled)
.map((item) => item.key);
this.ctx.database.setCustomHeaderFixedData(fixedKeys, value === 'fixedLeft' ? 'left' : value === 'fixedRight' ? 'right' : '');
this.hide();
}
else if (value === 'hide') {
const hideKeys = this.ctx.header.leafCellHeaders
.filter((item) => item.colIndex >= minX && item.colIndex <= maxX)
.filter((item) => !item.children.length)
.filter((item) => !item.column.hideDisabled)
.map((item) => item.key);
if (hideKeys.length > 0) {
this.ctx.database.setCustomHeaderHideData(hideKeys, true);
}
this.hide();
}
else if (value === 'visible') {
// 这个分支不应该被触发,因为 visible 是父菜单项
}
else if (value.startsWith('visible_')) {
if (!_itemData.key)
return;
if (_itemData.children) {
const childKeys = this.getLeafKeys(_itemData.children);
this.ctx.database.setCustomHeaderHideData(childKeys, false);
}
else {
this.ctx.database.setCustomHeaderHideData([_itemData.key], false);
}
// 直接删除对应的子菜单项
if (this.currentDOMTreeMenu) {
this.currentDOMTreeMenu.removeSubMenuItem(value);
}
// 检查是否还有隐藏的列
const columns = this.ctx.database.getColumns();
const leafColumns = toLeaf(columns);
const remainingHideColumns = leafColumns.filter((item) => item.hide);
if (remainingHideColumns.length === 0) {
// 如果没有隐藏的列了,隐藏菜单
this.hide();
}
}
else if (value === 'resetHeader') {
this.ctx.database.resetCustomHeader();
this.hide();
}
},
});
this.positionMenu(e);
});
this.ctx.on('click', () => {
this.hide();
});
this.ctx.on('onScroll', this.hide.bind(this));
this.ctx.on('resize', this.hide.bind(this));
}
positionMenu(e) {
const virtualReference = {
getBoundingClientRect: () => ({
width: 0,
height: 0,
top: e.clientY,
left: e.clientX,
right: e.clientX,
bottom: e.clientY,
x: e.clientX,
y: e.clientY,
}),
contextElement: document.body,
};
autoUpdate(virtualReference, this.contextMenuEl, () => {
computePosition(virtualReference, this.contextMenuEl, {
placement: 'right-start',
middleware: [offset(), shift(), flip()],
}).then(({ x, y }) => {
if (this.contextMenuEl) {
Object.assign(this.contextMenuEl.style, {
left: `${x}px`,
top: `${y}px`,
});
}
});
});
}
filterColumns(columns) {
const result = [];
for (const col of columns) {
// 有 children,递归过滤
if (col.children && col.children.length > 0) {
const filteredChildren = this.filterColumns(col.children);
if (filteredChildren.length > 0) {
result.push({
label: col.title,
key: col.key,
value: `visible_${col.key}`,
// disabled: true, // 非末级,且有子项保留 → disabled
children: filteredChildren,
});
}
}
// 末级节点,保留 hide===true 的
else if (col.hide) {
result.push({ label: col.title, value: `visible_${col.key}`, key: col.key });
}
}
return result;
}
// 递归获取所有子项key
getLeafKeys(tree) {
return tree.reduce((acc, node) => {
if (!node.children || node.children.length === 0) {
// 叶子节点,收集 value
acc.push(String(node.key));
}
else {
// 有子节点,递归收集
acc.push(...this.getLeafKeys(node.children));
}
return acc;
}, []);
}
hide() {
if (this.currentDOMTreeMenu) {
this.currentDOMTreeMenu.destroy();
this.currentDOMTreeMenu = undefined;
}
this.contextMenuEl.style.display = 'none';
this.ctx.contextMenuIng = false;
}
destroy() {
this.hide();
this.contextMenuEl?.remove();
}
}
//# sourceMappingURL=ContextMenu.js.map