@whitesev/pops
Version:
弹窗库,包含了alert、confirm、prompt、drawer、folder、loading、iframe、panel、tooltip、searchSuggestion、rightClickMenu组件
631 lines (610 loc) • 19 kB
text/typescript
import { GlobalConfig } from "../../config/GlobalConfig";
import { EventEmiter } from "../../event/EventEmiter";
import { PopsElementHandler } from "../../handler/PopsElementHandler";
import { PopsHandler } from "../../handler/PopsHandler";
import { PopsCSS } from "../../PopsCSS";
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 { PopsTooltipDefaultConfig } from "./defaultConfig";
import type { PopsToolTipConfig } from "./types/index";
type ToolTipEventTypeName = "MouseEvent" | "TouchEvent";
export class ToolTip {
$el = {
$shadowContainer: null as unknown as HTMLDivElement,
$shadowRoot: null as unknown as ShadowRoot | HTMLElement,
$toolTip: null as unknown as HTMLElement,
$content: null as unknown as HTMLElement,
$arrow: null as unknown as HTMLElement,
};
emitter: EventEmiter<EventMap>;
$data = {
config: null as any as Required<PopsToolTipConfig>,
guid: null as any as string,
timeId_close_TouchEvent: <number[]>[],
timeId_close_MouseEvent: <number[]>[],
};
constructor(
config: Required<PopsToolTipConfig>,
guid: string,
ShadowInfo: {
$shadowContainer: HTMLDivElement;
$shadowRoot: ShadowRoot | HTMLElement;
},
emitter: EventEmiter<EventMap>
) {
this.emitter = emitter;
this.$data.config = config;
this.$data.guid = guid;
this.$el.$shadowContainer = ShadowInfo.$shadowContainer;
this.$el.$shadowRoot = ShadowInfo.$shadowRoot;
this.show = this.show.bind(this);
this.close = this.close.bind(this);
this.toolTipAnimationFinishEvent = this.toolTipAnimationFinishEvent.bind(this);
this.toolTipMouseEnterEvent = this.toolTipMouseEnterEvent.bind(this);
this.toolTipMouseLeaveEvent = this.toolTipMouseLeaveEvent.bind(this);
this.init();
}
init() {
const toolTipInfo = this.createToolTip();
this.$el.$toolTip = toolTipInfo.$toolTipContainer;
this.$el.$content = toolTipInfo.$toolTipContent;
this.$el.$arrow = toolTipInfo.$toolTipArrow;
this.changeContent();
this.changeZIndex();
this.changePosition();
if (!this.$data.config.alwaysShow) {
this.offEvent();
this.onEvent();
}
}
/**
* 创建提示元素
*/
createToolTip() {
const $toolTipContainer = popsDOMUtils.createElement(
"div",
{
className: "pops-tip",
innerHTML: /*html*/ `
<div class="pops-tip-content" style="text-align: center;"></div>
<div class="pops-tip-arrow"></div>
`,
},
{
"data-position": this.$data.config.isFixed ? "fixed" : "absolute",
"data-guid": this.$data.guid,
}
);
/** 内容 */
const $toolTipContent = $toolTipContainer.querySelector<HTMLElement>(".pops-tip-content")!;
/** 箭头 */
const $toolTipArrow = $toolTipContainer.querySelector<HTMLElement>(".pops-tip-arrow")!;
// 处理className
popsDOMUtils.addClassName($toolTipContainer, this.$data.config.className);
// 添加z-index
$toolTipContainer.style.zIndex = PopsHandler.getTargerOrFunctionValue(this.$data.config.zIndex).toString();
// 添加自定义style
PopsElementHandler.addStyle($toolTipContainer, this.$data.config.style);
// 添加自定义浅色style
PopsElementHandler.addLightStyle($toolTipContainer, this.$data.config.lightStyle);
// 添加自定义深色style
PopsElementHandler.addDarkStyle($toolTipContainer, this.$data.config.darkStyle);
// 处理是否显示箭头元素
if (!this.$data.config.showArrow) {
popsDOMUtils.remove($toolTipArrow);
}
return {
$toolTipContainer: $toolTipContainer,
$toolTipArrow: $toolTipArrow,
$toolTipContent: $toolTipContent,
};
}
/**
* 获取提示的内容
*/
getContent() {
return typeof this.$data.config.content === "function" ? this.$data.config.content() : this.$data.config.content;
}
/**
* 修改提示的内容
* @param text
*/
changeContent(text?: string) {
if (text == null) {
text = this.getContent();
}
if (this.$data.config.isDiffContent) {
const contentPropKey = "data-content";
const originContentText: string = Reflect.get(this.$el.$content, contentPropKey);
if (typeof originContentText === "string") {
if (originContentText === text) {
// 内容未改变,不修改避免渲染
return;
}
}
Reflect.set(this.$el.$content, contentPropKey, text);
}
PopsSafeUtils.setSafeHTML(this.$el.$content, text);
}
/**
* 获取z-index
*/
getZIndex() {
const zIndex = PopsHandler.getTargerOrFunctionValue(this.$data.config.zIndex);
return zIndex;
}
/**
* 动态修改z-index
*/
changeZIndex() {
const zIndex = this.getZIndex();
this.$el.$toolTip.style.setProperty("z-index", zIndex.toString());
}
/**
* 计算 提示框的位置
* @param event 触发的事件
* @param targetElement 目标元素
* @param arrowDistance 箭头和目标元素的距离
* @param otherDistance 其它位置的偏移
*/
calcToolTipPosition(
targetElement: HTMLElement,
arrowDistance: number,
otherDistance: number,
event?: MouseEvent | TouchEvent | PointerEvent
) {
const offsetInfo = popsDOMUtils.offset(targetElement, !this.$data.config.isFixed);
// 目标 宽
const targetElement_width = offsetInfo.width;
// 目标 高
const targetElement_height = offsetInfo.height;
// 目标 顶部距离
const targetElement_top = offsetInfo.top;
// let targetElement_bottom = offsetInfo.bottom;
// 目标 左边距离
const targetElement_left = offsetInfo.left;
// let targetElement_right = offsetInfo.right;
const toolTipElement_width = popsDOMUtils.outerWidth(this.$el.$toolTip);
const toolTipElement_height = popsDOMUtils.outerHeight(this.$el.$toolTip);
// 目标元素的x轴的中间位置
const targetElement_X_center_pos = targetElement_left + targetElement_width / 2 - toolTipElement_width / 2;
// 目标元素的Y轴的中间位置
const targetElement_Y_center_pos = targetElement_top + targetElement_height / 2 - toolTipElement_height / 2;
let mouseX = 0;
let mouseY = 0;
if (event != null) {
if (event instanceof MouseEvent || event instanceof PointerEvent) {
mouseX = event.pageX;
mouseY = event.y;
} else if (event instanceof TouchEvent) {
const touchEvent = event.touches[0];
mouseX = touchEvent.pageX;
mouseY = touchEvent.pageY;
} else {
if (typeof (<MouseEvent>event).clientX === "number") {
mouseX = (<MouseEvent>event).clientX;
}
if (typeof (<MouseEvent>event).clientY === "number") {
mouseY = (<MouseEvent>event).clientY;
}
}
}
return {
TOP: {
left: targetElement_X_center_pos - otherDistance,
top: targetElement_top - toolTipElement_height - arrowDistance,
arrow: "bottom",
motion: "fadeInTop",
},
RIGHT: {
left: targetElement_left + targetElement_width + arrowDistance,
top: targetElement_Y_center_pos + otherDistance,
arrow: "left",
motion: "fadeInRight",
},
BOTTOM: {
left: targetElement_X_center_pos - otherDistance,
top: targetElement_top + targetElement_height + arrowDistance,
arrow: "top",
motion: "fadeInBottom",
},
LEFT: {
left: targetElement_left - toolTipElement_width - arrowDistance,
top: targetElement_Y_center_pos + otherDistance,
arrow: "right",
motion: "fadeInLeft",
},
FOLLOW: {
left: mouseX + otherDistance,
top: mouseY + otherDistance,
arrow: "follow",
motion: "",
},
};
}
/**
* 动态修改tooltip的位置
*/
changePosition(event?: MouseEvent | TouchEvent | PointerEvent) {
const positionInfo = this.calcToolTipPosition(
this.$data.config.$target,
this.$data.config.arrowDistance,
this.$data.config.otherDistance,
event
);
const positionKey = this.$data.config.position.toUpperCase() as keyof typeof positionInfo;
const position = positionInfo[positionKey];
if (position) {
this.$el.$toolTip.style.left = position.left + "px";
this.$el.$toolTip.style.top = position.top + "px";
this.$el.$toolTip.setAttribute("data-motion", position.motion);
this.$el.$arrow.setAttribute("data-position", position.arrow);
} else {
console.error("不存在该位置", this.$data.config.position);
}
}
/**
* 事件绑定
*/
onEvent() {
// 监听动画结束事件
this.onToolTipAnimationFinishEvent();
this.onShowEvent();
this.onCloseEvent();
this.onToolTipMouseEnterEvent();
this.onToolTipMouseLeaveEvent();
}
/**
* 取消事件绑定
*/
offEvent() {
this.offToolTipAnimationFinishEvent();
this.offShowEvent();
this.offCloseEvent();
this.offToolTipMouseEnterEvent();
this.offToolTipMouseLeaveEvent();
}
/**
* 添加关闭的timeId
* @param type
* @param timeId
*/
addCloseTimeoutId(type: ToolTipEventTypeName, timeId: number) {
if (type === "MouseEvent") {
this.$data.timeId_close_MouseEvent.push(timeId);
} else {
this.$data.timeId_close_TouchEvent.push(timeId);
}
}
/**
* 清除延迟的timeId
* @param type 事件类型
*/
clearCloseTimeoutId(type: ToolTipEventTypeName, timeId?: number) {
const timeIdList = type === "MouseEvent" ? this.$data.timeId_close_MouseEvent : this.$data.timeId_close_TouchEvent;
for (let index = 0; index < timeIdList.length; index++) {
const currentTimeId = timeIdList[index];
if (typeof timeId === "number") {
// 只清除一个
if (timeId == currentTimeId) {
popsUtils.clearTimeout(timeId);
timeIdList.splice(index, 1);
break;
}
} else {
popsUtils.clearTimeout(currentTimeId);
timeIdList.splice(index, 1);
index--;
}
}
}
/**
* 显示提示框
*/
show(...args: any[]) {
const event = args[0] as MouseEvent | TouchEvent;
const eventType: ToolTipEventTypeName = event instanceof MouseEvent ? "MouseEvent" : "TouchEvent";
this.clearCloseTimeoutId(eventType);
if (typeof this.$data.config.showBeforeCallBack === "function") {
const result = this.$data.config.showBeforeCallBack(this.$el.$toolTip);
if (typeof result === "boolean" && !result) {
return;
}
}
if (!popsUtils.contains(this.$el.$shadowRoot, this.$el.$toolTip)) {
// 不在容器中,添加
this.init();
popsDOMUtils.append(this.$el.$shadowRoot, this.$el.$toolTip);
}
if (!popsUtils.contains(this.$el.$shadowContainer)) {
// 页面不存在Shadow,添加
this.emitter.emit("pops:before-append-to-page", this.$el.$shadowRoot, this.$el.$shadowContainer);
popsDOMUtils.append(document.body, this.$el.$shadowContainer);
}
// 更新内容
this.changeContent();
// 更新tip的位置
this.changePosition(event);
if (typeof this.$data.config.showAfterCallBack === "function") {
this.$data.config.showAfterCallBack(this.$el.$toolTip);
}
}
/**
* 绑定 显示事件
*/
onShowEvent() {
popsDOMUtils.on(
this.$data.config.$target,
this.$data.config.onShowEventName,
this.show,
this.$data.config.eventOption
);
}
/**
* 取消绑定 显示事件
*/
offShowEvent() {
popsDOMUtils.off(
this.$data.config.$target,
this.$data.config.onShowEventName,
this.show,
this.$data.config.eventOption
);
}
/**
* 关闭提示框
*/
close(...args: any[]) {
const event = args[0] as MouseEvent | TouchEvent;
const eventType: ToolTipEventTypeName = event instanceof MouseEvent ? "MouseEvent" : "TouchEvent";
// 只判断鼠标事件
// 其它的如Touch事件不做处理
if (event && event instanceof MouseEvent) {
const $target = event.composedPath()[0];
// 如果是目标元素的子元素/tooltip元素的子元素触发的话,那就不管
if ($target != this.$data.config.$target && $target != this.$el.$toolTip) {
return;
}
}
if (typeof this.$data.config.closeBeforeCallBack === "function") {
const result = this.$data.config.closeBeforeCallBack(this.$el.$toolTip);
if (typeof result === "boolean" && !result) {
return;
}
}
if (
this.$data.config.delayCloseTime == null ||
(typeof this.$data.config.delayCloseTime === "number" && this.$data.config.delayCloseTime <= 0)
) {
this.$data.config.delayCloseTime = 100;
}
const timeId = popsUtils.setTimeout(() => {
// 设置属性触发关闭动画
this.clearCloseTimeoutId(eventType, timeId);
if (this.$el.$toolTip == null) {
// 已清除了
return;
}
const motion = this.$el.$toolTip.getAttribute("data-motion");
if (motion == null || motion.trim() === "") {
// 没有动画
this.toolTipAnimationFinishEvent();
} else {
// 修改data-motion触发动画关闭
this.$el.$toolTip.setAttribute(
"data-motion",
this.$el.$toolTip.getAttribute("data-motion")!.replace("fadeIn", "fadeOut")
);
}
}, this.$data.config.delayCloseTime);
this.addCloseTimeoutId(eventType, timeId);
if (typeof this.$data.config.closeAfterCallBack === "function") {
this.$data.config.closeAfterCallBack(this.$el.$toolTip);
}
}
/**
* 绑定 关闭事件
*/
onCloseEvent() {
popsDOMUtils.on(
this.$data.config.$target,
this.$data.config.onCloseEventName,
this.close,
this.$data.config.eventOption
);
}
/**
* 取消绑定 关闭事件
*/
offCloseEvent() {
popsDOMUtils.off(
this.$data.config.$target,
this.$data.config.onCloseEventName,
this.close,
this.$data.config.eventOption
);
}
/**
* 销毁元素
*/
destory() {
if (this.$el.$toolTip) {
popsDOMUtils.remove(this.$el.$toolTip);
}
// @ts-expect-error
this.$el.$toolTip = null;
// @ts-expect-error
this.$el.$arrow = null;
// @ts-expect-error
this.$el.$content = null;
}
/**
* 动画结束事件
*/
toolTipAnimationFinishEvent() {
if (!this.$el.$toolTip) {
return;
}
if (this.$el.$toolTip.getAttribute("data-motion")!.includes("In")) {
return;
}
this.destory();
}
/**
* 监听tooltip的动画结束
*/
onToolTipAnimationFinishEvent() {
popsDOMUtils.on(this.$el.$toolTip, popsDOMUtils.getAnimationEndNameList(), this.toolTipAnimationFinishEvent);
}
/**
* 取消tooltip监听动画结束
*/
offToolTipAnimationFinishEvent() {
popsDOMUtils.off(this.$el.$toolTip, popsDOMUtils.getAnimationEndNameList(), this.toolTipAnimationFinishEvent);
}
/**
* 鼠标|触摸进入事件
*/
toolTipMouseEnterEvent() {
this.clearCloseTimeoutId("MouseEvent");
this.clearCloseTimeoutId("TouchEvent");
// 重置动画状态
// this.$el.$toolTip.style.animationPlayState = "paused";
// if (parseInt(getComputedStyle(toolTipElement)) > 0.5) {
// toolTipElement.style.animationPlayState = "paused";
// }
}
/**
* 监听鼠标|触摸事件
*/
onToolTipMouseEnterEvent() {
this.clearCloseTimeoutId("MouseEvent");
this.clearCloseTimeoutId("TouchEvent");
popsDOMUtils.on(
this.$el.$toolTip,
"mouseenter touchstart",
this.toolTipMouseEnterEvent,
this.$data.config.eventOption
);
}
/**
* 取消监听事件 - 鼠标|触摸
*/
offToolTipMouseEnterEvent() {
popsDOMUtils.off(
this.$el.$toolTip,
"mouseenter touchstart",
this.toolTipMouseEnterEvent,
this.$data.config.eventOption
);
}
/**
* 离开事件 - 鼠标|触摸
*/
toolTipMouseLeaveEvent(event: MouseEvent | PointerEvent) {
this.close(event);
// this.$el.$toolTip.style.animationPlayState = "running";
}
/**
* 监听离开事件 - 鼠标|触摸
*/
onToolTipMouseLeaveEvent() {
popsDOMUtils.on(
this.$el.$toolTip,
"mouseleave touchend touchcancel",
this.toolTipMouseLeaveEvent,
this.$data.config.eventOption
);
}
/**
* 取消监听离开事件 - 鼠标|触摸
*/
offToolTipMouseLeaveEvent() {
popsDOMUtils.off(
this.$el.$toolTip,
"mouseleave touchend touchcancel",
this.toolTipMouseLeaveEvent,
this.$data.config.eventOption
);
}
}
export type PopsTooltipResult<T extends PopsToolTipConfig> = {
guid: string;
config: T;
$shadowContainer: HTMLDivElement;
$shadowRoot: ShadowRoot;
toolTip: typeof ToolTip.prototype;
};
export const PopsTooltip = {
init(__config__: PopsToolTipConfig) {
const guid = popsUtils.getRandomGUID();
// 设置当前类型
const popsType: PopsType = "tooltip";
let config = PopsTooltipDefaultConfig();
config = popsUtils.assign(config, GlobalConfig.getGlobalConfig());
config = popsUtils.assign(config, __config__);
if (!(config.$target instanceof HTMLElement)) {
throw new TypeError("config.target 必须是HTMLElement类型");
}
config = PopsHandler.handleOnly(popsType, config);
if (config.position === "follow") {
config.onShowEventName = config.onShowEventName.trim();
const showEventNameSplit = config.onShowEventName.split(" ");
["mousemove", "touchmove"].forEach((it) => {
if (showEventNameSplit.includes(it)) return;
config.onShowEventName += ` ${it}`;
});
}
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: "tooltipCSS",
css: PopsCSS.tooltipCSS,
},
]);
const toolTip = new ToolTip(
config,
guid,
{
$shadowContainer,
$shadowRoot,
},
emitter
);
if (config.alwaysShow) {
// 总是显示
// 直接显示
toolTip.show();
} else {
// 事件触发才显示
}
return {
guid,
config,
$shadowContainer,
$shadowRoot,
toolTip,
emitter,
};
},
};