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.
418 lines • 16.8 kB
JavaScript
import { computePosition, autoUpdate, flip, shift, offset } from '@floating-ui/dom';
import { expandSvg } from './Icons';
export class DOMTreeMenu {
constructor(container, menuData = [], options = {}) {
Object.defineProperty(this, "container", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "menuData", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "onClick", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "activeSubmenus", {
enumerable: true,
configurable: true,
writable: true,
value: new Set()
});
Object.defineProperty(this, "boundMouseEnterHandler", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "boundMouseLeaveHandler", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "boundClickHandler", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
this.container = container;
this.menuData = menuData;
this.onClick = options.onClick;
// 绑定事件处理器
this.boundMouseEnterHandler = (e) => this.handleMouseEvent(e, 'enter');
this.boundMouseLeaveHandler = (e) => this.handleMouseEvent(e, 'leave');
this.boundClickHandler = (e) => this.handleClick(e);
this.createMenu();
this.bindEvents();
}
createMenu() {
this.container.className = 'e-virt-table-main-menu';
// 确保菜单可见
this.container.style.display = 'block';
const mainMenu = document.createDocumentFragment();
this.menuData.forEach((item) => {
mainMenu.appendChild(this.createMenuItem(item));
});
this.container.appendChild(mainMenu);
}
createMenuItem(item, isSubmenu = false) {
const menuItem = this.createElement('div', isSubmenu ? 'e-virt-table-submenu-item' : 'e-virt-table-menu-item');
menuItem.setAttribute(isSubmenu ? 'data-submenu' : 'data-menu', item.value);
if (item.disabled)
menuItem.classList.add('disabled');
const contentContainer = this.createElement('div', 'e-virt-table-menu-item-content');
if (item.icon) {
const iconContainer = this.createElement('span', 'e-virt-table-menu-item-icon');
iconContainer.innerHTML = item.icon;
contentContainer.appendChild(iconContainer);
}
else {
contentContainer.classList.add('menu-item-no-icon');
}
const textSpan = this.createElement('span', 'e-virt-table-menu-item-text');
textSpan.textContent = item.label;
contentContainer.appendChild(textSpan);
menuItem.appendChild(contentContainer);
if (item.children?.length) {
const arrowContainer = this.createElement('span', 'e-virt-table-menu-arrow');
arrowContainer.innerHTML = expandSvg;
menuItem.appendChild(arrowContainer);
const submenu = this.createSubmenu(item.children);
menuItem._submenu = submenu;
this.container.appendChild(submenu);
}
return menuItem;
}
createSubmenu(submenuData) {
const submenu = this.createElement('div', 'e-virt-table-submenu');
submenuData.forEach((item) => {
submenu.appendChild(this.createMenuItem(item, true));
});
return submenu;
}
createElement(tagName, className = '') {
const element = document.createElement(tagName);
if (className)
element.className = className;
return element;
}
bindEvents() {
this.container.addEventListener('mouseenter', this.boundMouseEnterHandler, true);
this.container.addEventListener('mouseleave', this.boundMouseLeaveHandler, true);
this.container.addEventListener('click', this.boundClickHandler);
}
handleMouseEvent(e, type) {
// 阻止事件冒泡,防止触发外部的鼠标事件
e.stopPropagation();
const target = e.target;
const menuItem = target.closest('.e-virt-table-menu-item, .e-virt-table-submenu-item');
if (menuItem &&
(this.container.contains(menuItem) || menuItem.classList.contains('e-virt-table-submenu-item'))) {
if (type === 'enter') {
this.handleHover(menuItem);
}
else {
this.handleLeave(menuItem);
}
}
}
handleHover(menuItem) {
if (menuItem.classList.contains('e-virt-table-menu-item')) {
this.container
.querySelectorAll('.e-virt-table-menu-item')
.forEach((item) => item.classList.remove('active'));
if (!menuItem.classList.contains('disabled')) {
menuItem.classList.add('active');
}
}
const submenu = menuItem._submenu || menuItem.querySelector('.e-virt-table-submenu');
if (submenu) {
this.hideSiblingSubmenus(menuItem);
this.showSubmenu(menuItem, submenu);
}
}
handleLeave(menuItem) {
const submenu = menuItem._submenu || menuItem.querySelector('.e-virt-table-submenu');
setTimeout(() => {
const submenuEl = submenu;
if (submenuEl && !submenuEl.matches(':hover') && !menuItem.matches(':hover')) {
this.hideSubmenu(submenuEl);
if (menuItem.classList.contains('e-virt-table-menu-item')) {
menuItem.classList.remove('active');
}
}
}, 150);
}
hideSiblingSubmenus(menuItem) {
// 找到同级的所有菜单项
let siblings;
if (menuItem.classList.contains('e-virt-table-menu-item')) {
// 主菜单项:隐藏其他主菜单项的子菜单
siblings = this.container.querySelectorAll('.e-virt-table-menu-item');
}
else {
// 子菜单项:隐藏同一个子菜单容器中其他子菜单项的子菜单
const parentSubmenu = menuItem.closest('.e-virt-table-submenu');
if (parentSubmenu) {
siblings = parentSubmenu.querySelectorAll('.e-virt-table-submenu-item');
}
else {
return;
}
}
// 隐藏所有同级菜单项的子菜单(除了当前项)
siblings.forEach((sibling) => {
if (sibling !== menuItem && sibling._submenu) {
this.hideSubmenu(sibling._submenu);
}
});
}
async showSubmenu(trigger, submenu) {
if (this.activeSubmenus.has(submenu))
return;
this.activeSubmenus.add(submenu);
submenu.classList.add('show');
const cleanup = autoUpdate(trigger, submenu, async () => {
// 根据主菜单容器的整体位置判断子菜单显示方向
const containerRect = this.container.getBoundingClientRect();
const viewportWidth = window.innerWidth;
const submenuWidth = submenu.offsetWidth || 200; // 预估宽度
// 计算主菜单右侧和左侧可用空间
const rightSpace = viewportWidth - containerRect.right;
const leftSpace = containerRect.left;
// 根据主菜单位置决定所有子菜单的统一方向
const placement = (rightSpace >= submenuWidth || rightSpace >= leftSpace) ? 'right-start' : 'left-start';
const { x, y } = await computePosition(trigger, submenu, {
placement,
middleware: [offset(8), shift({ padding: 8 })],
});
Object.assign(submenu.style, {
left: `${x}px`,
top: `${y}px`,
});
});
submenu._cleanup = cleanup;
}
hideSubmenu(submenu) {
if (!this.activeSubmenus.has(submenu))
return;
this.activeSubmenus.delete(submenu);
submenu.classList.remove('show');
this.hideAllChildSubmenus(submenu);
const menuElement = submenu;
if (menuElement._cleanup) {
menuElement._cleanup();
delete menuElement._cleanup;
}
}
hideAllChildSubmenus(parentSubmenu) {
const childMenuItems = parentSubmenu.querySelectorAll('.e-virt-table-submenu-item');
childMenuItems.forEach((menuItem) => {
if (menuItem._submenu) {
const childSubmenu = menuItem._submenu;
if (this.activeSubmenus.has(childSubmenu)) {
this.activeSubmenus.delete(childSubmenu);
childSubmenu.classList.remove('show');
const menuElement = childSubmenu;
if (menuElement._cleanup) {
menuElement._cleanup();
delete menuElement._cleanup;
}
this.hideAllChildSubmenus(childSubmenu);
}
}
});
}
handleClick(e) {
// 阻止事件冒泡,防止触发外部的点击事件
e.stopPropagation();
const menuItem = e.target.closest('.e-virt-table-menu-item, .e-virt-table-submenu-item');
if (!menuItem)
return;
if (menuItem.classList.contains('disabled'))
return;
// 如果点击的是有子菜单的菜单项,先显示子菜单
if (menuItem.classList.contains('e-virt-table-menu-item')) {
const submenu = menuItem._submenu || menuItem.querySelector('.e-virt-table-submenu');
if (submenu) {
this.showSubmenu(menuItem, submenu);
return; // 不执行点击回调,只是显示子菜单
}
}
const menuValue = menuItem.getAttribute('data-menu') || menuItem?.getAttribute('data-submenu');
const itemData = this.findMenuItem(menuValue || '');
if (itemData) {
if (itemData.event) {
itemData.event(e, this.hide.bind(this));
}
if (this.onClick) {
this.onClick(itemData, menuValue || '');
}
}
}
findMenuItem(value, items = this.menuData) {
for (const item of items) {
if (item.value === value)
return item;
if (item.children) {
const found = this.findMenuItem(value, item.children);
if (found)
return found;
}
}
return null;
}
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.container, () => {
computePosition(virtualReference, this.container, {
placement: 'right-start',
middleware: [offset(), shift(), flip()],
}).then(({ x, y }) => {
if (this.container) {
Object.assign(this.container.style, {
left: `${x}px`,
top: `${y}px`,
});
}
});
});
}
hide() {
// 隐藏菜单
this.cleanupAllSubmenus(this.container);
this.container.style.display = 'none';
}
destroy() {
// 移除事件监听器
this.container.removeEventListener('mouseenter', this.boundMouseEnterHandler, true);
this.container.removeEventListener('mouseleave', this.boundMouseLeaveHandler, true);
this.container.removeEventListener('click', this.boundClickHandler);
// 递归清理所有子菜单
this.cleanupAllSubmenus(this.container);
// 隐藏菜单
this.container.style.display = 'none';
// 清理容器
this.container.replaceChildren();
}
/**
* 删除指定值的菜单项
* @param value 要删除的菜单项的值
* @returns 是否成功删除
*/
removeMenuItem(value) {
// 查找要删除的菜单项
const menuItem = this.container.querySelector(`[data-menu="${value}"]`);
if (!menuItem) {
return false;
}
// 如果菜单项有子菜单,先清理子菜单
const submenu = menuItem.querySelector('.e-virt-table-submenu');
if (submenu) {
this.cleanupSubmenuRecursively(submenu);
}
// 从 DOM 中移除菜单项
menuItem.remove();
return true;
}
/**
* 删除指定值的子菜单项
* @param value 要删除的子菜单项的值
* @returns 是否成功删除
*/
removeSubMenuItem(value) {
// 查找要删除的子菜单项
const subMenuItem = this.container.querySelector(`[data-submenu="${value}"]`);
if (!subMenuItem) {
return false;
}
// 找到所属的父级子菜单容器
const parentSubmenu = subMenuItem.closest('.e-virt-table-submenu');
// 如果子菜单项有子菜单,先清理子菜单
const childSubmenu = subMenuItem._submenu;
if (childSubmenu) {
this.cleanupSubmenuRecursively(childSubmenu);
}
// 从 DOM 中移除子菜单项
subMenuItem.remove();
// 检查父级子菜单容器是否还有其他子菜单项
if (parentSubmenu) {
const remainingItems = parentSubmenu.querySelectorAll('.e-virt-table-submenu-item');
if (remainingItems.length === 0) {
// 没有子项了,找到拥有这个子菜单的父级菜单项并移除
const parentMenuItem = this.container.querySelector(`[data-menu]`);
if (parentMenuItem && parentMenuItem._submenu === parentSubmenu) {
this.removeMenuItem(parentMenuItem.getAttribute('data-menu') || '');
}
else {
// 可能是嵌套子菜单,需要查找所有可能的父级
const allMenuItems = this.container.querySelectorAll('[data-menu], [data-submenu]');
for (const item of allMenuItems) {
if (item._submenu === parentSubmenu) {
const parentValue = item.getAttribute('data-menu') || item.getAttribute('data-submenu');
if (parentValue) {
if (item.hasAttribute('data-menu')) {
this.removeMenuItem(parentValue);
}
else {
this.removeSubMenuItem(parentValue);
}
}
break;
}
}
}
}
}
return true;
}
cleanupAllSubmenus(container) {
// 清理所有菜单项的子菜单
const menuItems = container.querySelectorAll('.e-virt-table-menu-item');
menuItems.forEach((menuItem) => {
if (menuItem._submenu) {
this.cleanupSubmenuRecursively(menuItem._submenu);
menuItem._submenu = undefined;
}
});
}
cleanupSubmenuRecursively(submenu) {
// 清理当前子菜单的清理函数
const menuElement = submenu;
if (menuElement._cleanup) {
menuElement._cleanup();
delete menuElement._cleanup;
}
// 递归清理子菜单中的子菜单
const childMenuItems = submenu.querySelectorAll('.e-virt-table-submenu-item');
childMenuItems.forEach((menuItem) => {
if (menuItem._submenu) {
this.cleanupSubmenuRecursively(menuItem._submenu);
}
});
// 从 DOM 中移除子菜单
submenu.remove();
}
}
//# sourceMappingURL=DOMTreeMenu.js.map