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; $data = { config: null as any as Required, guid: null as any as string, timeId_close_TouchEvent: [], timeId_close_MouseEvent: [], }; constructor( config: Required, guid: string, ShadowInfo: { $shadowContainer: HTMLDivElement; $shadowRoot: ShadowRoot | HTMLElement; }, emitter: EventEmiter ) { 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*/ `
`, }, { "data-position": this.$data.config.isFixed ? "fixed" : "absolute", "data-guid": this.$data.guid, } ); /** 内容 */ const $toolTipContent = $toolTipContainer.querySelector(".pops-tip-content")!; /** 箭头 */ const $toolTipArrow = $toolTipContainer.querySelector(".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 (event).clientX === "number") { mouseX = (event).clientX; } if (typeof (event).clientY === "number") { mouseY = (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 = { 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(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, }; }, };