@whitesev/pops
Version:
弹窗库,包含了alert、confirm、prompt、drawer、folder、loading、iframe、panel、tooltip、searchSuggestion、rightClickMenu组件
577 lines (568 loc) • 20.7 kB
text/typescript
import type { PopsAlertConfig } from "../components/alert/types";
import type { PopsConfirmConfig } from "../components/confirm/types";
import type { PopsDrawerConfig } from "../components/drawer/types";
import type { PopsFolderConfig } from "../components/folder/types";
import type { PopsIframeConfig } from "../components/iframe/types";
import type { PopsLoadingConfig } from "../components/loading/types";
import type { PopsPanelConfig } from "../components/panel/types";
import type { PopsPromptConfig } from "../components/prompt/types";
import type { PopsRightClickMenuConfig } from "../components/rightClickMenu/types";
import type { PopsToolTipConfig } from "../components/tooltip/types";
import { PopsAnimation } from "../PopsAnimation";
import { PopsCore } from "../PopsCore";
import { PopsInstData } from "../PopsInst";
import type { PopsInstGeneralConfig } from "../types/inst";
import type { PopsInstStoreType } from "../types/main";
import { popsDOMUtils } from "../utils/PopsDOMUtils";
import { PopsInstanceUtils } from "../utils/PopsInstanceUtils";
import { popsUtils } from "../utils/PopsUtils";
export const PopsInstHandler = {
/**
* 删除配置中对应的对象
* @param totalInstConfigList 配置实例列表
* @param guid 唯一标识
* @param isAll 是否全部删除
*/
async removeInstance(totalInstConfigList: PopsInstGeneralConfig[][], guid?: string, isAll = false) {
/**
* 移除元素实例
* @param instCommonConfig
*/
const removeInst = async (instCommonConfig: PopsInstGeneralConfig) => {
await instCommonConfig.emitter.emit("pops:before-destory", instCommonConfig);
popsDOMUtils.offAll(instCommonConfig.$shadowRoot);
popsDOMUtils.remove(instCommonConfig?.$anim);
popsDOMUtils.remove(instCommonConfig?.$pops);
popsDOMUtils.remove(instCommonConfig?.$mask);
popsDOMUtils.remove(instCommonConfig?.$shadowContainer);
await instCommonConfig.emitter.emit("pops:destory");
// 再清空全部监听的事件
await instCommonConfig.emitter.offAll();
};
const asyncInstTask: Promise<void>[] = [];
// [ inst[], inst[],...]
for (const instConfigList of totalInstConfigList) {
for (let index = 0; index < instConfigList.length; index++) {
const instConfigItem = instConfigList[index];
// 移除全部或者guid相同
if (isAll || (typeof guid === "string" && instConfigItem.guid === guid)) {
// 判断是否有动画
const animName = instConfigItem.$anim.getAttribute("anim")!;
if (PopsAnimation.hasAnim(animName)) {
const reverseAnimName = animName + "-reverse";
popsDOMUtils.css(instConfigItem.$anim, "width", "100%");
popsDOMUtils.css(instConfigItem.$anim, "height", "100%");
popsDOMUtils.css(instConfigItem.$anim, "animation-name", reverseAnimName);
if (PopsAnimation.hasAnim(popsDOMUtils.css(instConfigItem.$anim, "animation-name"))) {
asyncInstTask.push(
new Promise<void>((resolve) => {
popsDOMUtils.on(
instConfigItem.$anim,
popsDOMUtils.getAnimationEndNameList(),
async () => {
await removeInst(instConfigItem);
resolve();
},
{
capture: true,
}
);
})
);
} else {
asyncInstTask.push(removeInst(instConfigItem));
}
} else {
asyncInstTask.push(removeInst(instConfigItem));
}
instConfigList.splice(index, 1);
index--;
}
}
}
await Promise.all(asyncInstTask);
return totalInstConfigList;
},
/**
* 显示
* @param popsType
* @param instConfigList
* @param guid
* @param config
* @param $anim
* @param $mask
*/
show(
config:
| PopsAlertConfig
| PopsDrawerConfig
| PopsPromptConfig
| PopsConfirmConfig
| PopsIframeConfig
| PopsLoadingConfig
| PopsPanelConfig
| PopsFolderConfig,
popsType: PopsInstStoreType,
instConfigList: PopsInstGeneralConfig[],
guid: string,
$anim: HTMLElement,
$mask?: HTMLElement
) {
// oxlint-disable-next-line no-async-promise-executor
return new Promise<void>(async (resolve) => {
const $pops = $anim.querySelector<HTMLDivElement>(".pops[type-value]")!;
const fintInst = instConfigList.find((instConfigItem) => instConfigItem.guid === guid);
if (fintInst) {
// 由于是隐蔽状态
// 先设置好动画状态
// 再显示,会自执行动画
const checkVisible = () => {
if (!PopsInstanceUtils.isHide($anim)) {
return true;
} else {
return false;
}
};
const startAnim = async () => {
if (popsType === "drawer") {
// drawer是抽屉
// 单独处理动画
const drawerConfig = config as Required<PopsDrawerConfig>;
await popsUtils.sleep(drawerConfig.openDelay ?? 0);
if ($mask) {
popsDOMUtils.css($mask, "display", "");
}
const direction = drawerConfig.direction!;
const size = drawerConfig.size!.toString();
if (["top", "bottom"].includes(direction)) {
$pops.style.setProperty("height", size);
} else if (["left", "right"].includes(direction)) {
$pops.style.setProperty("width", size);
} else {
console.error("未知direction:", direction);
}
} else {
instConfigItem.$anim.style.width = "";
instConfigItem.$anim.style.height = "";
Reflect.set(instConfigItem.$anim.style, "animation-name", animName);
}
instConfigItem.$anim.style.display = "";
if (instConfigItem.$mask) {
instConfigItem.$mask.style.display = "";
}
};
const endCallback = () => {
fintInst.emitter.emit("pops:show", instConfigItem);
};
const instConfigItem = fintInst;
const animName = instConfigItem.$anim!.getAttribute("anim")!.replace("-reverse", "");
fintInst.emitter.emit("pops:before-show", instConfigItem);
if (checkVisible() && PopsAnimation.hasAnim(animName)) {
/**
* 动画结束的回调
*/
const animationendCallBack = () => {
listener.off();
endCallback();
resolve();
};
const listener = popsDOMUtils.on(
instConfigItem.$anim,
popsDOMUtils.getAnimationEndNameList(),
animationendCallBack,
{
capture: true,
}
);
await startAnim();
} else {
await startAnim();
endCallback();
resolve();
}
} else {
console.error("show-error: 该实例未存储");
resolve();
}
});
},
/**
* 隐藏
* @param popsType
* @param instConfigList
* @param guid
* @param config
* @param $anim
* @param $mask
*/
hide(
config:
| PopsAlertConfig
| PopsDrawerConfig
| PopsPromptConfig
| PopsConfirmConfig
| PopsIframeConfig
| PopsLoadingConfig
| PopsPanelConfig
| PopsFolderConfig,
popsType: PopsInstStoreType,
instConfigList: PopsInstGeneralConfig[],
guid: string,
$anim: HTMLElement,
$mask?: HTMLElement
) {
// oxlint-disable-next-line no-async-promise-executor
return new Promise<void>(async (resolve) => {
const $pops = $anim.querySelector<HTMLDivElement>(".pops[type-value]")!;
const fintInst = instConfigList.find((instConfigItem) => instConfigItem.guid === guid);
if (fintInst) {
// 由于是已显示状态
// 先执行动画
// 再隐藏
// 存在实例
const checkVisible = () => {
if (!PopsInstanceUtils.isHide($anim)) {
return true;
} else {
return false;
}
};
const startAnim = async () => {
if (popsType === "drawer") {
// drawer是抽屉
// 单独处理动画
const drawerConfig = config as Required<PopsDrawerConfig>;
await popsUtils.sleep(drawerConfig.closeDelay ?? 0);
if ($mask) {
popsDOMUtils.css($mask, "display", "none");
}
const direction = drawerConfig.direction!;
const size = "0";
if (["top", "bottom"].includes(direction)) {
$pops.style.setProperty("height", size);
} else if (["left", "right"].includes(direction)) {
$pops.style.setProperty("width", size);
} else {
console.error("未知direction: ", direction);
}
} else {
instConfigItem.$anim.style.width = "100%";
instConfigItem.$anim.style.height = "100%";
Reflect.set(instConfigItem.$anim.style, "animation-name", reverseAnimName);
}
};
const endCallback = () => {
instConfigItem.$anim.style.display = "none";
if (instConfigItem.$mask) {
instConfigItem.$mask.style.display = "none";
}
fintInst.emitter.emit("pops:hide", instConfigItem);
};
const instConfigItem = fintInst;
const animName = instConfigItem.$anim!.getAttribute("anim")!.replace("-reverse", "");
const reverseAnimName = animName + "-reverse";
fintInst.emitter.emit("pops:before-hide", instConfigItem);
if (!checkVisible() && PopsAnimation.hasAnim(reverseAnimName)) {
/**
* 动画结束的回调
*/
const animationendCallBack = () => {
listener.off();
endCallback();
resolve();
};
const listener = popsDOMUtils.on(
instConfigItem.$anim,
popsDOMUtils.getAnimationEndNameList(),
animationendCallBack,
{
capture: true,
once: true,
}
);
await startAnim();
} else {
await startAnim();
endCallback();
resolve();
}
} else {
console.error("hide-error: 该实例未存储");
resolve();
}
});
},
/**
* 关闭
* @param popsType
* @param instConfigList
* @param guid
* @param config
* @param $anim
*/
async close(
config:
| PopsAlertConfig
| PopsDrawerConfig
| PopsPromptConfig
| PopsConfirmConfig
| PopsIframeConfig
| PopsLoadingConfig
| PopsPanelConfig
| PopsFolderConfig,
popsType: string,
instConfigList: PopsInstGeneralConfig[],
guid: string,
$anim: HTMLElement
) {
// 判断组件内部是否有rightClickMenu、tooltip、searchSuggestion组件
// 有的话也需要关闭
PopsInstData.rightClickMenu.forEach((itemConfig) => {
const config = itemConfig.config as PopsRightClickMenuConfig;
if (config.$target instanceof HTMLElement) {
const $root = config.$target.getRootNode();
if ($root instanceof HTMLElement && $root.parentElement == null) {
// 触发销毁元素
itemConfig.emitter.emit("pops:before-destory", itemConfig);
}
}
});
PopsInstData.tooltip.forEach((itemConfig) => {
const config = itemConfig.config as PopsToolTipConfig;
if (config.$target instanceof HTMLElement) {
const $root = config.$target.getRootNode();
if ($root instanceof HTMLElement && $root.parentElement == null) {
// 触发销毁元素
itemConfig.emitter.emit("pops:before-destory", itemConfig);
}
}
});
// eslint-disable-next-line no-async-promise-executor
await new Promise<void>(async (resolve) => {
const $pops = $anim.querySelector<HTMLDivElement>(".pops[type-value]")!;
const drawerConfig = config as Required<PopsDrawerConfig>;
const startAnim = () => {
/**
* 弹窗已关闭的回调
*/
const transtionEndCallback = async (event: Event) => {
if ((event as TransitionEvent).propertyName !== "transform") {
return;
}
listener.off();
await PopsInstHandler.removeInstance([instConfigList], guid);
resolve();
};
// 监听过渡结束
const listener = popsDOMUtils.on($pops, popsDOMUtils.getTransitionEndNameList(), transtionEndCallback, {
once: true,
});
const popsTransForm = globalThis.getComputedStyle($pops).transform;
if (popsTransForm !== "none") {
// 不存在动画
// 直接移除实例
listener.emit({ propertyName: "transform" });
return;
}
if (["top"].includes(drawerConfig.direction)) {
$pops.style.setProperty("transform", "translateY(-100%)");
} else if (["bottom"].includes(drawerConfig.direction)) {
$pops.style.setProperty("transform", "translateY(100%)");
} else if (["left"].includes(drawerConfig.direction)) {
$pops.style.setProperty("transform", "translateX(-100%)");
} else if (["right"].includes(drawerConfig.direction)) {
$pops.style.setProperty("transform", "translateX(100%)");
} else {
console.error("未知direction: ", drawerConfig.direction);
}
};
if (popsType === "drawer") {
await popsUtils.sleep(drawerConfig.closeDelay ?? 0);
startAnim();
} else {
await PopsInstHandler.removeInstance([instConfigList], guid);
resolve();
}
});
},
/**
* 拖拽元素
* 说明:
* + 元素的position为absolute或者fixed
* + 会为元素的
* @param $move 需要拖拽的元素
* @param options 配置
*/
drag(
$move: HTMLElement,
options: {
dragElement: HTMLElement;
limit: boolean;
emitClick?: boolean;
extraDistance: number;
container?: Window | typeof globalThis | HTMLElement;
startCallBack?: (moveElement: HTMLElement, left: number, top: number) => void;
moveCallBack?: (moveElement: HTMLElement, left: number, top: number) => void;
endCallBack?: (moveElement: HTMLElement, left: number, top: number) => void;
preventEvent?: (event: TouchEvent | PointerEvent) => boolean;
}
) {
options = Object.assign(
{
limit: true,
extraDistance: 3,
container: PopsCore.globalThis,
emitClick: true,
},
options
);
let isMove = false;
// 点击元素,left偏移
let clickElementLeftOffset = 0;
// 点击元素,top偏移
let clickElementTopOffset = 0;
const AnyTouch = popsUtils.AnyTouch();
const anyTouchElement = new AnyTouch(options.dragElement, {
preventDefault(event: Event) {
if (typeof options.preventEvent === "function") {
return options.preventEvent(event as any);
} else {
// 返回true阻止滑动
return true;
}
},
});
popsDOMUtils.css(options.dragElement, {
cursor: "move",
});
/**
* 修改移动的元素的style
*/
function changeMoveElementStyle(element: HTMLElement) {
const old_transitionDuration = element.style.transitionDuration;
if (globalThis.getComputedStyle(element).transitionDuration !== "0s") {
element.style.transitionDuration = "0s";
}
return () => {
element.style.transitionDuration = old_transitionDuration;
};
}
/**
* 获取容器的高度、宽度,一般是window为容器
*/
function getContainerWidthOrHeight(container: HTMLElement | Window | typeof globalThis) {
container = container ?? globalThis;
return {
width: popsDOMUtils.width(container),
height: popsDOMUtils.height(container),
};
}
/**
* 获取容器的最小left、top偏移
*/
function getContainerTopOrLeft(container: HTMLElement | Window | typeof globalThis) {
container = container ?? globalThis;
if (popsUtils.isWin(container)) {
return {
left: 0,
top: 0,
};
} else {
const rect = (container as HTMLElement).getBoundingClientRect();
return {
left: rect.left,
top: rect.top,
};
}
}
// 获取transform偏移
let transformInfo = popsDOMUtils.getTransform($move);
let resumeMoveElementStyle: ((...args: any[]) => any) | null = null;
anyTouchElement.on("pan", function (event) {
if (!isMove) {
isMove = true;
const rect = options.dragElement.getBoundingClientRect();
clickElementLeftOffset = event.x - rect.left;
clickElementTopOffset = event.y - rect.top;
transformInfo = popsDOMUtils.getTransform($move);
resumeMoveElementStyle = changeMoveElementStyle($move);
if (typeof options.startCallBack === "function") {
options.startCallBack($move, clickElementLeftOffset, clickElementTopOffset);
}
}
/** 当前移动的left偏移 */
let currentMoveLeftOffset = event.x - clickElementLeftOffset + transformInfo.transformLeft;
/** 当前移动的top偏移 */
let currentMoveTopOffset = event.y - clickElementTopOffset + transformInfo.transformTop;
// 拖拽移动
if (event.phase === "move") {
if (options.limit) {
// 限制在容器内移动
// left偏移最大值
const maxLeftOffset =
getContainerWidthOrHeight(options.container!).width -
popsDOMUtils.width($move) +
transformInfo.transformLeft;
const { left: minLeftOffset, top: minTopOffset } = getContainerTopOrLeft(options.container!);
// top偏移的最大值
const maxTopOffset =
getContainerWidthOrHeight(options.container!).height -
popsDOMUtils.height($move) +
transformInfo.transformTop;
if (currentMoveLeftOffset > maxLeftOffset) {
// 不允许超过容器的最大宽度
currentMoveLeftOffset = maxLeftOffset;
}
if (currentMoveTopOffset > maxTopOffset) {
// 不允许超过容器的最大高度
currentMoveTopOffset = maxTopOffset;
}
if (currentMoveLeftOffset - options.extraDistance * 2 < minLeftOffset + transformInfo.transformLeft) {
// 不允许left偏移小于容器最小值
currentMoveLeftOffset = minLeftOffset + transformInfo.transformLeft;
// 最左边 +额外距离
currentMoveLeftOffset += options.extraDistance;
} else {
// 最右边 -额外距离
currentMoveLeftOffset -= options.extraDistance;
}
if (currentMoveTopOffset - options.extraDistance * 2 < minTopOffset + transformInfo.transformTop) {
// 不允许top偏移小于容器最小值
currentMoveTopOffset = minTopOffset + transformInfo.transformTop;
// 最上面 +额外距离
currentMoveTopOffset += options.extraDistance;
} else {
// 最下面 -额外距离
currentMoveTopOffset -= options.extraDistance;
}
}
if (typeof options.moveCallBack === "function") {
options.moveCallBack($move, currentMoveLeftOffset, currentMoveTopOffset);
}
popsDOMUtils.css($move, {
left: currentMoveLeftOffset + "px",
top: currentMoveTopOffset + "px",
});
}
// 停止拖拽
if (event.phase === "end") {
isMove = false;
if (typeof resumeMoveElementStyle === "function") {
resumeMoveElementStyle();
resumeMoveElementStyle = null;
}
if (typeof options.endCallBack === "function") {
options.endCallBack($move, currentMoveLeftOffset, currentMoveTopOffset);
}
}
});
if (options.emitClick) {
// 因为会覆盖上面的点击事件,主动触发一下
anyTouchElement.on(["tap"], function (event) {
event.changedPoints.forEach((item) => {
popsDOMUtils.emit(item.target! as HTMLElement, "click", void 0, true);
});
});
}
},
};