@whitesev/pops
Version:
弹窗库,包含了alert、confirm、prompt、drawer、folder、loading、iframe、panel、tooltip、searchSuggestion、rightClickMenu组件
665 lines (655 loc) • 25.3 kB
text/typescript
import { PopsCSS } from "../../PopsCSS";
import { OriginPrototype } from "../../PopsCore";
import { PopsIcon } from "../../PopsIcon";
import { GlobalConfig } from "../../config/GlobalConfig";
import { EventEmiter } from "../../event/EventEmiter";
import { PopsElementHandler } from "../../handler/PopsElementHandler";
import { PopsHandler } from "../../handler/PopsHandler";
import type { EventMap } from "../../types/EventEmitter";
import type { PopsType } from "../../types/main";
import { popsDOMUtils } from "../../utils/PopsDOMUtils";
import { PopsSafeUtils } from "../../utils/PopsSafeUtils";
import { popsUtils } from "../../utils/PopsUtils";
import { PopsRightClickMenuDefaultConfig } from "./defaultConfig";
import type {
PopsRightClickMenuChildRootStoreNodeValue,
PopsRightClickMenuConfig,
PopsRightClickMenuDataConfig,
PopsRightClickMenuItemStoreNodeValue,
PopsRightClickMenuRootStoreNodeValue,
} from "./types";
export const PopsRightClickMenu = {
init(__config__: PopsRightClickMenuConfig) {
const guid = popsUtils.getRandomGUID();
// 设置当前类型
const popsType: PopsType = "rightClickMenu";
let config = PopsRightClickMenuDefaultConfig();
config = popsUtils.assign(config, GlobalConfig.getGlobalConfig());
config = popsUtils.assign(config, __config__);
config = PopsHandler.handleOnly(popsType, config);
const emitter = config.emitter ?? new EventEmiter<EventMap>(popsType);
const { $shadowContainer, $shadowRoot } = PopsHandler.handlerShadow(config);
PopsHandler.handleInit($shadowRoot, [
{
name: "index",
css: PopsCSS.index,
},
{
name: "anim",
css: PopsCSS.anim,
},
{
name: "common",
css: PopsCSS.common,
},
{
name: "skeleton",
css: PopsCSS.skeletonCSS,
},
{
name: "rightClickMenu",
css: PopsCSS.rightClickMenu,
},
]);
// 添加自定义style
PopsElementHandler.addStyle($shadowRoot, config.style);
// 添加自定义浅色style
PopsElementHandler.addLightStyle($shadowRoot, config.lightStyle);
// 添加自定义深色style
PopsElementHandler.addDarkStyle($shadowRoot, config.darkStyle);
const PopsContextMenu = {
$data: {
menuDataKey: "data-menu",
},
$el: {
$root: null as any as HTMLElement,
},
/**
* 全局点击检测
* @param event
*/
windowCheckClickEvent(event: MouseEvent | PointerEvent) {
if (!PopsContextMenu.$el.$root) {
return;
}
const $click = event.target as HTMLElement;
if ($click.closest(`.pops-${popsType}`)) {
return;
}
if ($click.className && $click.className === "pops-shadow-container" && $click.shadowRoot != null) {
// pops的shadow-container
PopsContextMenu.shadowRootCheckClickEvent(event);
return;
}
PopsContextMenu.closeAllMenu(PopsContextMenu.$el.$root);
},
/**
* target为shadowRoot或shadowRoot内的全局点击检测
* @param event
*/
shadowRootCheckClickEvent(event: MouseEvent | PointerEvent) {
if (!PopsContextMenu.$el.$root) {
return;
}
const $click = event.composedPath()[0] as HTMLElement;
if ($click.closest(`.pops-${popsType}`)) {
return;
}
PopsContextMenu.closeAllMenu(PopsContextMenu.$el.$root);
},
/**
* 添加全局点击检测事件
*/
addWindowCheckClickListener() {
popsDOMUtils.on(globalThis, "click touchstart", PopsContextMenu.windowCheckClickEvent, {
capture: true,
});
if (config.$target instanceof Node) {
const $shadowRoot = config.$target.getRootNode();
if ($shadowRoot instanceof ShadowRoot) {
popsDOMUtils.on($shadowRoot, "click touchstart", PopsContextMenu.shadowRootCheckClickEvent, {
capture: true,
});
}
}
},
/**
* 移除全局点击检测事件
*/
removeWindowCheckClickListener() {
popsDOMUtils.off(globalThis, "click touchstart", PopsContextMenu.windowCheckClickEvent, {
capture: true,
});
if (config.$target instanceof Node) {
const $shadowRoot = config.$target.getRootNode();
if ($shadowRoot instanceof ShadowRoot) {
popsDOMUtils.off($shadowRoot, "click touchstart", PopsContextMenu.windowCheckClickEvent, {
capture: true,
});
}
}
},
/**
* contextmenu事件
* @param event
* @param selectorTarget
*/
async contextMenuEvent(event: PointerEvent, selectorTarget: NonNullable<PopsRightClickMenuConfig["$target"]>) {
if (config.preventDefault) {
popsDOMUtils.preventEvent(event);
}
PopsHandler.handleOnly(popsType, config);
if (PopsContextMenu.$el.$root) {
PopsContextMenu.closeAllMenu(PopsContextMenu.$el.$root);
}
selectorTarget = selectorTarget ?? config.$target;
const beforeShowCallBackResult = await config?.beforeShowCallBack(event);
if (typeof beforeShowCallBackResult === "boolean" && !beforeShowCallBackResult) {
return;
}
const rootElement = PopsContextMenu.showMenu(event, config.data, selectorTarget);
PopsContextMenu.$el.$root = rootElement;
if (config.only) {
PopsHandler.handlePush(popsType, {
$shadowRoot: $shadowRoot,
$shadowContainer: $shadowContainer,
guid: guid,
$anim: rootElement,
$pops: rootElement,
config: config,
emitter,
});
emitter.on("pops:before-destory", () => {
PopsContextMenu.closeAllMenu(rootElement);
});
emitter.on("pops:destory", () => {
PopsContextMenu.closeAllMenu(rootElement);
});
}
},
/**
* 添加contextmenu事件
* @param target 目标
* @param selector 子元素选择器
*/
addContextMenuEvent(target: PopsRightClickMenuConfig["$target"], selector?: string) {
popsDOMUtils.on(target!, "contextmenu", selector, PopsContextMenu.contextMenuEvent);
},
/**
* 移除contextmenu事件
* @param target 目标
* @param selector 子元素选择器
*/
removeContextMenuEvent(target: HTMLElement | typeof globalThis | Window, selector?: string) {
popsDOMUtils.off(target, "contextmenu", selector, PopsContextMenu.contextMenuEvent);
},
/**
* 自动判断是否存在动画,存在动画就执行关闭动画并删除
* @param $menu
*/
animationCloseMenu($menu: HTMLElement) {
/**
* 动画结束触发的事件
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const transitionEndEvent = (event: TransitionEvent) => {
popsDOMUtils.off($menu, popsDOMUtils.getTransitionEndNameList(), transitionEndEvent, {
capture: true,
});
popsDOMUtils.remove($menu);
};
if (popsDOMUtils.containsClassName($menu, `pops-${popsType}-anim-show`)) {
// 有动画
popsDOMUtils.on($menu, popsDOMUtils.getTransitionEndNameList(), transitionEndEvent, {
capture: true,
});
popsDOMUtils.removeClassName($menu, `pops-${popsType}-anim-show`);
} else if (
popsDOMUtils.containsClassName($menu, `pops-${popsType}-anim-scale`) &&
popsDOMUtils.containsClassName($menu, `pops-${popsType}-anim-scale-open`)
) {
// 有动画
popsDOMUtils.on($menu, popsDOMUtils.getTransitionEndNameList(), transitionEndEvent, {
capture: true,
});
popsDOMUtils.removeClassName($menu, `pops-${popsType}-anim-scale-open`);
popsDOMUtils.addClassName($menu, `pops-${popsType}-anim-scale-not-open`);
} else {
// 无动画
popsDOMUtils.remove($menu);
}
},
/**
* 关闭所有菜单
* @param $root
*/
closeAllMenu($root: HTMLElement) {
if ($root == null) {
return;
}
const rootElementMenuData: PopsRightClickMenuRootStoreNodeValue | PopsRightClickMenuChildRootStoreNodeValue =
Reflect.get($root, PopsContextMenu.$data.menuDataKey);
if ((<PopsRightClickMenuChildRootStoreNodeValue>rootElementMenuData)?.root) {
$root = (<PopsRightClickMenuChildRootStoreNodeValue>rootElementMenuData).root;
}
const childMenuList = (<PopsRightClickMenuRootStoreNodeValue>rootElementMenuData).child as HTMLElement[];
childMenuList.forEach((childMenuElement) => {
this.animationCloseMenu(childMenuElement);
});
this.animationCloseMenu($root);
PopsContextMenu.$el.$root = null as any;
},
/**
* 获取菜单容器
* @param isChildren 是否是rightClickMenu的某一项的子菜单
*/
createMenuContainerElement(isChildren: boolean) {
const $menu = popsDOMUtils.createElement(
"div",
{
className: `pops-${popsType}`,
innerHTML: /*html*/ `<ul class="pops-${popsType}-wrapper"></ul>`,
},
{
"data-position": config.position,
}
);
const zIndex = this.getMenuZIndex();
if (zIndex > 10000) {
// 大于默认值
$menu.style.zIndex = zIndex.toString();
}
if (isChildren) {
$menu.setAttribute("is-children", "true");
}
// 添加动画
if (config.isAnimation) {
popsDOMUtils.addClassName($menu, `pops-${popsType}-anim-grid`);
}
// 添加放大动画
if (config.useScaleAnimation) {
popsDOMUtils.addClassName($menu, `pops-${popsType}-anim-scale`);
popsDOMUtils.addClassName($menu, `pops-${popsType}-anim-scale-not-open`);
}
return $menu;
},
/**
* 动态获取配的z-index
*/
getMenuZIndex() {
return PopsHandler.getTargerOrFunctionValue(config.zIndex);
},
/**
* 获取left、top偏移
* @param $menu 当前生成的菜单元素
* @param mousePosition 鼠标位置信息
* @param isMainMenu 是否是主菜单
*/
getOffset(
$menu: HTMLElement,
mousePosition: { x: number; y: number },
parentInfo?: {
$menu: HTMLElement;
$parentItem: HTMLElement;
}
) {
const result = {
top: 0,
right: 0,
bottom: 0,
left: 0,
};
const menuElementWidth = popsDOMUtils.width($menu);
const menuElementHeight = popsDOMUtils.height($menu);
/**
* 限制的间隙距离
*/
const limitDistance = 1;
let maxPageLeftOffset = popsDOMUtils.width(globalThis) - limitDistance;
let maxPageTopOffset = popsDOMUtils.height(globalThis) - limitDistance;
if (config.position === "absolute") {
// 添加滚动距离
maxPageLeftOffset += globalThis.scrollX;
maxPageTopOffset += globalThis.scrollY;
}
// left最大偏移
const maxLeftOffset = maxPageLeftOffset - menuElementWidth;
// top最大偏移
const maxTopOffset = maxPageTopOffset - menuElementHeight;
const chileMenuLeftOrRightDistance = config.chileMenuLeftOrRightDistance;
const childMenuTopOrBottomDistance = config.childMenuTopOrBottomDistance;
let currentLeftOffset = mousePosition.x;
let currentTopOffset = mousePosition.y;
currentLeftOffset = currentLeftOffset < 0 ? 0 : currentLeftOffset;
currentTopOffset = currentTopOffset < 0 ? 0 : currentTopOffset;
// 不允许超出left最大值
if (config.limitPositionXInView && currentLeftOffset + chileMenuLeftOrRightDistance >= maxLeftOffset) {
// 超过,那么子菜单将会在放在左边
// 偏移计算方式就是父菜单的右偏移+父菜单的宽度
if (parentInfo) {
// 子菜单
const mainMenuOffset = popsDOMUtils.offset(parentInfo.$menu);
currentLeftOffset = maxPageLeftOffset - mainMenuOffset.left - chileMenuLeftOrRightDistance + limitDistance;
} else {
// 主菜单 默认的
currentLeftOffset = limitDistance + chileMenuLeftOrRightDistance;
}
if (currentLeftOffset < 0) {
currentLeftOffset = 0;
} else if (currentLeftOffset > maxLeftOffset) {
currentLeftOffset = maxLeftOffset;
}
// 去除左偏移,变为右偏移
result.right = currentLeftOffset;
Reflect.deleteProperty(result, "left");
} else {
// 右边
currentLeftOffset = currentLeftOffset + chileMenuLeftOrRightDistance;
result.left = currentLeftOffset;
Reflect.deleteProperty(result, "right");
}
// 不允许超出top最大值
if (config.limitPositionYInView && currentTopOffset + childMenuTopOrBottomDistance >= maxTopOffset) {
// 超过,那么子菜单将会在放在上面
if (parentInfo) {
// 以项的top偏移为基准
const parentItemOffset = popsDOMUtils.offset(parentInfo.$parentItem, false);
currentTopOffset =
maxPageTopOffset - parentItemOffset.bottom - childMenuTopOrBottomDistance + limitDistance;
} else {
currentTopOffset = limitDistance + childMenuTopOrBottomDistance;
}
if (currentTopOffset < 0) {
currentTopOffset = limitDistance;
} else if (currentTopOffset > maxTopOffset) {
currentTopOffset = maxTopOffset;
}
// 去除上偏移,变为下偏移
result.bottom = currentTopOffset;
Reflect.deleteProperty(result, "top");
} else {
currentTopOffset = currentTopOffset + childMenuTopOrBottomDistance;
// 底部偏移
result.top = currentTopOffset;
Reflect.deleteProperty(result, "bottom");
}
return result;
},
/**
* 显示菜单
* @param menuEvent 触发的事件
* @param dataConfig
* @param $listenerRootNode 右键菜单监听的元素
*/
showMenu(
menuEvent: PointerEvent,
dataConfig: PopsRightClickMenuDataConfig[],
$listenerRootNode: NonNullable<PopsRightClickMenuConfig["$target"]>
) {
const menuElement = this.createMenuContainerElement(false);
Reflect.set(menuElement, PopsContextMenu.$data.menuDataKey, {
child: [],
} as PopsRightClickMenuRootStoreNodeValue);
// 添加子元素
PopsContextMenu.addMenuLiELement(menuEvent, menuElement, menuElement, dataConfig, $listenerRootNode);
// 添加到页面
popsDOMUtils.append($shadowRoot, menuElement);
// 判断容器是否存在
if (!document.contains($shadowContainer)) {
emitter.emit("pops:before-append-to-page", $shadowRoot, $shadowContainer);
popsDOMUtils.appendBody($shadowContainer);
}
this.handlerShowMenuCSS(menuElement, menuEvent);
return menuElement;
},
/**
* 显示子菜单
* @param menuEvent 事件
* @param posInfo 位置信息
* @param dataConfig
* @param $root 根菜单元素
* @param $targetLi 父li项元素
* @param $listenerRootNode 右键菜单监听的元素
*/
showClildMenu(
menuEvent: PointerEvent,
posInfo: {
clientX: number;
clientY: number;
},
dataConfig: PopsRightClickMenuDataConfig[],
$root: HTMLDivElement,
$targetLi: HTMLLIElement,
$listenerRootNode: NonNullable<PopsRightClickMenuConfig["$target"]>
) {
const menuElement = this.createMenuContainerElement(true);
Reflect.set(menuElement, PopsContextMenu.$data.menuDataKey, {
parent: $targetLi,
root: $root,
} as PopsRightClickMenuItemStoreNodeValue);
// 根菜单数据
const rootElementMenuData: PopsRightClickMenuRootStoreNodeValue = Reflect.get(
$root,
PopsContextMenu.$data.menuDataKey
);
rootElementMenuData.child.push(menuElement);
// 添加子元素
PopsContextMenu.addMenuLiELement(menuEvent, $root, menuElement, dataConfig, $listenerRootNode);
// 添加到页面
popsDOMUtils.append($shadowRoot, menuElement);
const $parentMenu = $targetLi.closest<HTMLElement>(".pops-rightClickMenu")!;
this.handlerShowMenuCSS(menuElement, posInfo, {
$menu: $parentMenu,
$parentItem: $targetLi,
});
return menuElement;
},
/**
* 处理菜单显示的css样式(添加到页面后)
* @param $menu 菜单元素
* @param posInfo 菜单位置信息
* @param parentInfo 配置子菜单的父级信息
*/
handlerShowMenuCSS(
$menu: HTMLElement,
posInfo: {
clientX: number;
clientY: number;
},
parentInfo?: {
$menu: HTMLElement;
$parentItem: HTMLElement;
}
) {
const offset = this.getOffset(
$menu,
{
x: posInfo.clientX,
y: posInfo.clientY,
},
parentInfo
);
// 显示
popsDOMUtils.css($menu, {
...offset,
});
// 过渡动画
if (config.isAnimation) {
popsDOMUtils.addClassName($menu, `pops-${popsType}-anim-show`);
}
if (config.useScaleAnimation) {
popsDOMUtils.removeClassName($menu, `pops-${popsType}-anim-scale-not-open`);
popsDOMUtils.addClassName($menu, `pops-${popsType}-anim-scale-open`);
}
},
/**
* 获取菜单项的元素
* @param menuEvent 事件
* @param $root 根元素
* @param $menu 菜单元素
* @param dataConfig 配置
* @param $listenerRootNode 右键菜单监听的元素
*/
addMenuLiELement(
menuEvent: PointerEvent,
$root: HTMLDivElement,
$menu: HTMLDivElement,
dataConfig: PopsRightClickMenuDataConfig[],
$listenerRootNode: NonNullable<PopsRightClickMenuConfig["$target"]>
) {
const menuEventTarget = menuEvent.target;
const menuULElement = $menu.querySelector<HTMLUListElement>("ul")!;
dataConfig.forEach((item) => {
const menuLiElement = popsDOMUtils.parseTextToDOM<HTMLLIElement>(`<li></li>`);
// 判断有无图标,有就添加进去
if (typeof item.icon === "string" && item.icon.trim() !== "") {
const iconSVGHTML = PopsIcon.getIcon(item.icon) ?? item.icon;
const iconElement = popsDOMUtils.parseTextToDOM(
/*html*/ `<i class="pops-${popsType}-icon" is-loading="${item.iconIsLoading ?? false}">${iconSVGHTML}</i>`
);
menuLiElement.appendChild(iconElement);
}
// 插入文字
const text = typeof item.text === "function" ? item.text() : item.text;
menuLiElement.insertAdjacentHTML("beforeend", PopsSafeUtils.getSafeHTML(`<span>${text}</span>`));
// 如果存在子数据,显示
if (item.item && Array.isArray(item.item)) {
popsDOMUtils.addClassName(menuLiElement, `pops-${popsType}-item`);
}
// 鼠标|触摸 移入事件
// 在移动端会先触发touchstart再然后mouseenter
let isEmitTouchEvent = false;
/**
* 鼠标|触摸 移入事件
*/
function liElementHoverEvent(event: MouseEvent | TouchEvent) {
if (event.type === "touchstart") {
isEmitTouchEvent = true;
}
if (isEmitTouchEvent && event.type === "mouseenter") {
return;
}
Array.from(menuULElement.children as any as HTMLLIElement[]).forEach((liElement) => {
popsDOMUtils.removeClassName(liElement, `pops-${popsType}-is-visited`);
const li_menuData: PopsRightClickMenuItemStoreNodeValue = Reflect.get(
liElement,
PopsContextMenu.$data.menuDataKey
);
if (!li_menuData) {
return;
}
function removeElement($el: HTMLElement | undefined | null) {
if (!$el) return;
$el.querySelectorAll<HTMLLIElement>("ul li").forEach(($ele) => {
const menuData: PopsRightClickMenuItemStoreNodeValue = Reflect.get(
$ele,
PopsContextMenu.$data.menuDataKey
);
if (menuData?.child) {
removeElement(menuData.child);
}
});
popsDOMUtils.remove($el);
}
// 遍历根元素的上的__menuData__.child,判断
removeElement(li_menuData.child);
});
// 清理根元素上的children不存在于页面中的元素
const root_menuData: PopsRightClickMenuRootStoreNodeValue = Reflect.get(
$root,
PopsContextMenu.$data.menuDataKey
);
for (let index = 0; index < root_menuData.child.length; index++) {
const element = root_menuData.child[index];
if (!$shadowRoot.contains(element)) {
root_menuData.child.splice(index, 1);
index--;
}
}
popsDOMUtils.addClassName(menuLiElement, `pops-${popsType}-is-visited`);
if (!item.item) {
return;
}
const rect = menuLiElement.getBoundingClientRect();
const childMenu = PopsContextMenu.showClildMenu(
menuEvent,
{
clientX: rect.left + popsDOMUtils.outerWidth(menuLiElement),
clientY: rect.top,
},
item.item,
$root,
menuLiElement,
$listenerRootNode
);
Reflect.set(menuLiElement, PopsContextMenu.$data.menuDataKey, {
child: childMenu,
} as PopsRightClickMenuItemStoreNodeValue);
}
/**
* 点击事件
* @param clickEvent
*/
async function liElementClickEvent(clickEvent: MouseEvent | PointerEvent) {
if (typeof item.callback === "function") {
try {
OriginPrototype.Object.defineProperty(menuEvent, "target", {
get() {
return menuEventTarget;
},
});
} catch {
// 忽略
}
const callbackResult = await item.callback(
clickEvent as PointerEvent,
menuEvent,
menuLiElement,
$listenerRootNode
);
if (typeof callbackResult === "boolean" && callbackResult == false) {
return;
}
}
// 取消绑定的鼠标/触摸事件,防止关闭的时候再次触发
Array.from(menuULElement.children as any as HTMLLIElement[]).forEach((liEle) => {
popsDOMUtils.off(liEle, "mouseenter touchstart");
});
PopsContextMenu.closeAllMenu($root);
}
popsDOMUtils.on(menuLiElement, "mouseenter touchstart", liElementHoverEvent);
// 项-点击事件
popsDOMUtils.on(menuLiElement, "click", liElementClickEvent);
menuULElement.appendChild(menuLiElement);
});
},
};
// 添加右键菜单事件
PopsContextMenu.addContextMenuEvent(config.$target, config.targetSelector!);
// 添加全局点击检测
PopsContextMenu.addWindowCheckClickListener();
return {
guid: guid,
config: config as DeepRequired<PopsRightClickMenuConfig>,
addWindowCheckClickListener: PopsContextMenu.addWindowCheckClickListener,
removeWindowCheckClickListener: PopsContextMenu.removeWindowCheckClickListener,
addContextMenuEvent: PopsContextMenu.addContextMenuEvent,
removeContextMenuEvent: PopsContextMenu.removeContextMenuEvent,
/**
* 移除初始化时的添加的监听事件
*/
removeInitEventListener: {
contextMenu() {
PopsContextMenu.removeContextMenuEvent(config.$target as Window, config.targetSelector!);
},
windowClick() {
PopsContextMenu.removeWindowCheckClickListener();
},
},
/**
* 操作弹出菜单的对象
*/
PopsContextMenu: PopsContextMenu,
};
},
};