本文详解一个轻量级JavaScript浮动提示组件的实现。该组件采用单例模式管理多个触发元素,支持点击与悬停两种触发方式及隐藏逻辑。核心功能包括延时显示、25种预设定位、自定义坐标及窗口自适应定位,通过精细的状态机处理鼠标交互与定时器,确保多场景下的稳定表现。
程序特点与概述
该功能实现难度适中,但在程序架构与扩展性方面进行了优化,旨在提升使用便捷性并扩大适用范围。
程序特点
1. 当同一提示框需关联多个触发元素时,仅需创建一个实例即可;
2. 支持配置显示与隐藏的触发方式,可选点击或鼠标悬停;
3. 支持配置显示与隐藏的延时时间;
4. 内置25种预设的定位位置供选择;
5. 支持在预设定位基础上进行自定义坐标设置;
6. 支持根据窗口大小自动调整定位位置。
Tip对象初始化与触发机制
程序说明
【Tip对象】
Tip对象是用于展示提示信息的容器,由程序中的Tip属性引用。初始化时需对其进行基础配置。
首先执行以下初始化设置:
代码如下:
var css = this._cssTip;
css.margin = 0;
css.position = "absolute"; css.visibility = "hidden";
css.display = "block"; css.zIndex = 99;
css.left = this._cssTip.top = "-9999px"; 设置margin为0以规避部分定位兼容性问题;使用visibility隐藏而非display,是因为程序后续需获取Tip的offsetWidth和offsetHeight;将left和top设为-9999px是为了防止Tip占据文档流位置导致页面出现滚动条。
由于Tip可能嵌套在其他定位元素内部,还需设置两个偏移修正参数:
代码如下:
var iLeft = 0, iTop = 0, p = this.Tip;
while (p.offsetParent) {
p = p.offsetParent; iLeft += p.offsetLeft; iTop += p.offsetTop;
};
this._offsetleft = iLeft;
this._offsettop = iTop; 最后,为Tip的mouseover事件绑定处理函数,具体逻辑后续详述。
【触发对象】
鉴于常见场景是一个Tip对应多个触发点,程序参考Table排序机制,增加了Add方法。
Tip实例化后,通过调用Add方法可为多个触发元素分别注册触发对象,当前触发的元素由_trigger属性记录。
Add方法的必要参数是触发元素,即触发Tip显示的目标元素。
可选通过options参数自定义触发对象的属性,包括:
属性: 默认值//说明
代码如下:
ShowType: "both",//显示方式
HideType: "both",//隐藏方式
ShowDelayType: "touch",//显示延迟方式
HideDelayType: "touch",//隐藏延迟方式
ShowDelay: 300,//显示延时时间
HideDelay: 300,//隐藏延时时间
Fixed: {},//定位对象
onShow: function(){},//显示时执行
onHide: function(){}//隐藏时执行 具体作用将在后文说明,这些默认值可在程序初始化时修改。
一个典型应用是在onShow回调中,根据具体触发对象动态修改Tip的内容。
此外,Elem属性用于保存触发元素本身。
显示隐藏逻辑:点击与触发方式
【显示和隐藏】
提示效果的核心在于Tip的显示与隐藏。程序通过切换Tip的visibility属性为hidden或非hidden来控制显隐状态。
具体的显示和隐藏逻辑分别在Show和Hide函数中处理,此外还有ReadyShow和ReadyHide函数,主要用于处理延时逻辑。
该提示效果的一个显著特点是:当鼠标移动到Tip上时,Tip会保持显示状态。
为实现此效果,在Tip的mouseover事件中写入以下逻辑:
this.Check(e.relatedTarget) && clearTimeout(this._timer);
其中Check函数用于判断relatedTarget是否为外部元素,即鼠标离开的元素是否属于外部。
若判断为外部元素,说明当前处于隐藏延时阶段,此时清除定时器即可取消隐藏操作。
此处“外部元素”指除触发元素、Tip对象本身及其内部子元素之外的所有元素。
这一概念可能略显抽象,通过查看Check函数的具体判断逻辑即可理解:
代码如下:
return !this._trigger ||
!(
this.Tip === elem || this._trigger.Elem === elem ||
Contains(this.Tip, elem) || Contains(this._trigger.Elem, elem)
); 首先判断_trigger是否存在,若不存在则视为初始触发状态,同样视为外部触发。
若存在,则进一步判断传入的元素是否为Tip或触发元素本身,最后使用Contains函数判断元素是否位于Tip或触发元素内部。
ps:关于Contains函数的实现,请参考相关的比较文档。
上述逻辑返回的是“是否内部元素”的判断结果,取反后即得到“是否外部元素”的结果。
【点击方式】
点击方式显示是指用户点击触发元素时显示Tip。
在Add方法中,会为触发元素的click事件绑定以下处理程序:
代码如下:
addEvent(elem, "click", BindAsEventListener(this, function(e){
if ( this.IsClick(trigger.ShowType) ) {
if ( this.CheckShow(trigger) ) {
this.ReadyShow(this.IsClick(trigger.ShowDelayType));
} else {
clearTimeout(this._timer);
};
};
})); 首先根据ClickShow判断是否执行点击显示,再通过CheckShow检测是否为同一个触发对象。
CheckShow函数的实现如下:
代码如下:
if (trigger !== this._trigger) {
this.Hide(); this._trigger = trigger; return true;
} else { return false }; 若触发对象不同,则先执行Hide清理前一个触发对象的状态以防冲突,再执行ReadyShow进行显示。
若为同一触发对象,说明当前处于延时隐藏阶段,只需清除定时器以保持显示状态即可。
对应地,点击方式隐藏是指点击外部元素时隐藏Tip。
在ReadyShow函数中,若使用点击方式隐藏,会将_fCH绑定到document的click事件:
this.IsTouch(trigger.HideType) && addEvent(this._trigger.Elem, "mouseout", this._fTH);
注意此处将隐藏绑定事件放在ReadyShow而非Show中,因为延时期间可能尚未显示就触发了隐藏事件。
其中_fCH是在初始化时定义的一个属性,用于添加和移除点击隐藏事件:
代码如下:
this._fCH = BindAsEventListener(this, function(e) {
if (this.Check(e.target) && this.CheckHide()) {
this.ReadyHide(this.IsClick(this._trigger.HideDelayType));
};
}); 与点击显示不同,由于绑定的是document,隐藏前需先确认e.target是否为外部元素。
其中CheckHide用于检查Tip当前是否处于隐藏状态:
代码如下:
if (this._cssTip.visibility === "hidden") {
clearTimeout(this._timer);
removeEvent(this._trigger.Elem, "mouseout", this._fTH);
this._trigger = null;
removeEvent(document, "click", this._fCH);
return false;
} else { return true }; 若Tip本就处于隐藏状态,只需清除定时器并移除事件,无需再执行Hide。
【触发方式】
触发方式针对的是mouseover和mouseout事件,其流程与点击方式类似。
触发方式显示是指鼠标从外部元素进入触发元素(触发mouseover)时显示Tip。
在Add方法中,会为触发元素的mouseover事件绑定以下处理程序:
代码如下:
addEvent(elem, "mouseover", BindAsEventListener(this, function(e){
if ( this.IsTouch(trigger.ShowType) ) {
if (this.CheckShow(trigger)) {
this.ReadyShow(this.IsTouch(trigger.ShowDelayType));
} else if (this.Check(e.relatedTarget)) {
clearTimeout(this._timer);
};
};
})); 与点击方式类似,需执行CheckShow,但不同之处在于,还会用Check判断e.relatedTarget是否为外部对象。
这是因为mouseover事件可能由触发元素内部元素(包括Tip)进入或内部元素冒泡触发,这些情况无需任何操作。
对应地,触发方式隐藏是指鼠标从触发元素或Tip离开时隐藏Tip。
当TouchHide为true时,在ReadyShow阶段会将_fTH绑定到触发元素的mouseout事件:
this.IsTouch(trigger.HideType) && addEvent(this._trigger.Elem, "mouseout", this._fTH);
在Show阶段,再绑定到Tip的mouseout:
this.IsClick(trigger.HideType) && addEvent(document, "click", this._fCH);
在ReadyShow绑定的原因同上,而Tip只需在显示时绑定。
其中_fTH与_fCH类似,也是在初始化时定义的一个属性,用于添加和移除触发隐藏事件:
代码如下:
this._fTH = BindAsEventListener(this, function(e) {
if (this.Check(e.relatedTarget) && this.CheckHide()) {
this.ReadyHide(this.IsTouch(this._trigger.HideDelayType));
};
}); 不同之处在于,mouseout在Check时使用的是e.relatedTarget。
触发流程状态机分析
【触发原理】
上述内容从程序角度说明了触发显示和隐藏的过程,但要深入理解,还需进行细致分析。
以下以触发方式的显示隐藏为例绘制流程图:

以下是文字流程说明:
1. 等待触发显示;
2. 进入触发元素,若设置延时则跳转至3,未设置延时则跳转至4;
3. 延时时间内,若离开至外部元素则清除定时器并返回1,超过延时时间则跳转至4;
4. 执行显示程序;
5. 显示Tip状态;
6. 离开触发元素,若进入Tip则跳转至7,若离开至外部元素则跳转至9;
7. 保持显示状态;
8. 离开Tip,若进入触发元素则返回5,若离开至外部元素则跳转至9;
9. 若设置延时则跳转至10,未设置延时则跳转至11;
10. 延时时间内,若进入Tip则清除定时器并返回7,若进入触发元素则清除定时器并返回5,超过延时时间则跳转至11;
11. 执行隐藏程序,返回1;
对照程序代码,应能理解整个流程,尽管可能仍有一定理解难度。
此流程仅针对单例情况,多例时需增加额外判断。
该流程看似简单,但若追求最优流程,需考虑的细枝末节可能令人头疼。
点击方式与触发方式的流程基本一致,且更为简单,此处不再赘述。
元素定位算法与配置
【元素定位】
完成显示隐藏逻辑后,进入本程序的另一个重点:元素定位。
程序使用GetRelative函数,通过定位元素、参考元素和参数对象,获取形如{ Left: 100, Top: 200 }的定位参数结果。
计算结果综合了以下定位方式:预设定位、自定义定位、自适应定位。
触发对象的Fixed属性用于保存定位参数对象,包括以下属性:
属性: 默认值//说明
Align: "clientleft",//水平方向定位
vAlign: "clienttop",//垂直方向定位
CustomLeft: 0,//自定义left定位
CustomTop: 0,//自定义top定位
PercentLeft: 0,//自定义left百分比定位
PercentTop: 0,//自定义top百分比定位
Adaptive: false,//是否自适应定位
Reset: false//自适应定位时是否重新定位
接下来将介绍如何通过这些属性设置定位。
【预设定位和自定义定位】
预设定位指使用程序内置的25个预设位置进行定位。
这25个位置是如何生成的?请看下面的具体演示:

2下一页阅读全文