I originally wanted to create a tooltips effect that combines floating positioning and mouse following, but I found that positioning and mouse following are still different in some key places, so we should do it separately.
This effect itself is not very difficult. We mainly put some effort into the program structure and expansion to make it more convenient to use and be used in more places.
Program features
1. When the same prompt box is used for multiple trigger elements, only one instance is needed;
2. There are click and trigger methods to choose between display and hiding respectively;
3. Can Set delayed display and hiding;
4, there are 25 preset positioning positions;
5, you can customize the positioning based on the preset positioning;
6, you can set adaptive window positioning;
Program Description
[Tip Object]
Tip object is a container used to display prompt information, and the program is represented by the Tip attribute. There are no requirements for this, some settings will be made to it when the program is initialized.
First make the following settings:
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";
The margin is set to 0 to avoid some positioning problems. Use visibility to hide instead of display because the program needs to obtain the offsetWidth and offsetHeight of the Tip and also need to set them. left and top are scroll bars that avoid appearing due to Tip occupying space.
Because Tip may be inside other positioning elements, two offset correction parameters must be set:
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;
Finally, add an event to Tip's mouseover, which will be explained later.
[Trigger Object]
Since in many cases one Tip corresponds to tips in multiple places, the program refers to the Table sorting method and adds an Add method.
After a Tip is instantiated, you can use the Add method to add trigger objects to multiple trigger elements. The _trigger attribute is used in the program to represent the current trigger object.
A necessary parameter of the Add method is the trigger element, which is the element that triggers the display of Tip.
If necessary, you can also use the options parameter to customize the attributes of the trigger object, including:
Attribute: Default value//Explanation
ShowType: "both",//显示方式 HideType: "both",//隐藏方式 ShowDelayType: "touch",//显示延迟方式 HideDelayType: "touch",//隐藏延迟方式 ShowDelay: 300,//显示延时时间 HideDelay: 300,//隐藏延时时间 Fixed: {},//定位对象 onShow: function(){},//显示时执行 onHide: function(){}//隐藏时执行
The specific function will be explained later, and can be used during program initialization Modify these default values.
A classic application is to modify the Tip in onShow to the content corresponding to each trigger object.
In addition, there is the Elem attribute to save the triggering element.
【Show and Hide】
One of the key points of the prompt effect is to show and hide the prompt information. The program displays and hides the Tip by setting whether the Tip's visibility is hidden.
The specific display and hiding programs are in the Show and Hide programs respectively, as well as the ReadyShow and ReadyHide programs, which are mainly used to deal with delays.
One feature of this tip effect is that when the mouse moves over the Tip, it will remain displayed.
In order to achieve this effect, write a program for Tip's mouseover:
this.Check(e.relatedTarget) && clearTimeout(this._timer);
The Check program is used to determine whether relatedTarget is an external element , that is, whether the element the mouse leaves is an external element.
If it is an external element, it means that it is currently in the hiding delay stage, so just clear the timer to unhide it.
The external elements here refer to elements other than the trigger element and the Tip object itself and its internal elements.
This is a bit confusing, then you will understand if you look at how the Check program determines:
return !this._trigger || !( this.Tip === elem || this._trigger.Elem === elem || Contains(this.Tip, elem) || Contains(this._trigger.Elem, elem) );
First determine whether _trigger exists. If it does not exist, it means that it has just started to be triggered, which is also regarded as External trigger.
If it exists, then determine whether the passed element is a Tip or trigger element itself, and finally use Contains to determine whether it is inside a Tip or trigger element.
ps: Regarding Contains, please refer to the comparison document location here.
What you get in this way is to determine whether it is an internal element, and the final negation is to determine whether it is an external element.
[Click method]
Click method display means that the Tip is displayed when the trigger element is clicked.
In the Add program, the following program will be bound to the click event of the triggering element:
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); }; }; }));
First, use ClickShow to determine whether to click and display, and then use CheckShow to detect whether it is the same trigger object.
The CheckShow program is like this:
if (trigger !== this._trigger) { this.Hide(); this._trigger = trigger; return true; } else { return false; };
If it is not the same trigger object, first execute Hide to clean up the previous trigger object to prevent conflicts, and then execute ReadyShow to display it.
If it is the same trigger object, it means that it is currently in the delayed hidden stage. Just clear the timer and keep it displayed.
Correspondingly, hiding by clicking means hiding the Tip when clicking on an external element.
In ReadyShow, when using the click method to hide, _fCH will be bound to the click event of the document:
this.IsTouch(trigger.HideType) && addEvent(this._trigger.Elem, "mouseout ", this._fTH);
Note that the hidden binding event should be placed in ReadyShow instead of Show, because during the delay, the hidden event may be triggered before it is displayed.
where _fCH is an attribute defined during initialization, used to add and remove click-to-hide events:
this._fCH = BindAsEventListener(this, function(e) { if (this.Check(e.target) && this.CheckHide()) { this.ReadyHide(this.IsClick(this._trigger.HideDelayType)); }; });
Note that it is different from click-to-display. Since it is bound to a document, it must be hidden before First determine whether e.target is an external element.
CheckHide is used to check whether the Tip is currently hidden:
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; };
如果本来就是隐藏状态,清除定时器移除事件就行,不需要再执行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个位置是怎么来的呢?看下面的具体演示:
其中黑色框代表触发元素,红色框代表Tip。
一眼望去,要实现这么多的位置好像很复杂,这时要想找到最好的方法就要细心分析找出规律。
这25个位置其实都是由5个水平坐标和5个垂直坐标组合而来的,只要计算好这10个坐标,就能组合出这25个位置来了。
其中1,2,3,4,5代表的水平坐标,程序分别用left,clientleft,center,clientright,right来标识。
而1,6,11,16,21代表的垂直坐标,程序分别用top,clienttop,center,clientbottom,bottom来标识。
ps:词穷,只好加个client来充数。
下面说说如何获取这些坐标的值,首先通过getBoundingClientRect要获取触发元素的坐标对象。
ps:关于getBoundingClientRect的介绍请看这里的元素位置。
再利用这个坐标对像,通过GetRelative.Left和GetRelative.Top来获取水平和垂直坐标。
GetRelative.Left和GetRelative.Top里面都是些简单的获取坐标算法,具体请参考代码。
使用时,把水平坐标和垂直坐标的标识值(字符)分别赋给触发对象的Align和vAlign属性,系统就会自动设置对应的位置。
例如要设置位置14,那么Align设为"clientright",vAlign设为"center"就可以了。
至于自定义定位就是在预设定位得到的坐标基础上,根据CustomLeft和CustomTop的值再进行left和top的修正。
自定义百分比定位是以触发元素的宽和高为基准,根据PercentLeft和PercentTop取百分比:
if (options.PercentLeft) { iLeft += .01 * options.PercentLeft * relElem.offsetWidth; };
if (options.PercentTop) { iTop += .01 * options.PercentTop * relElem.offsetHeight; };
注意数值单位是0.01。
【自适应定位】
自适应定位的作用是当Tip显示的范围超过浏览器可视范围的时候,自动修正到可视范围里面。
因为上面通过getBoundingClientRect获取的定位是以视窗为准的,所以可以直接通过clientWidth/clientHeight来判断是否超过视窗范围。
首先获取最大left和top值:
var maxLeft = document.documentElement.clientWidth - fixedElem.offsetWidth,
maxTop = document.documentElement.clientHeight - fixedElem.offsetHeight;
最小值是0就不用计算了。
如果Reset属性是true会使用重新定位的方法。
理想的效果是能自动从25个预设定位中找到适合的定位位置。
但这个需求实在变化太多,要全部实现估计要长长的代码,程序仅仅做了简单的修正:
if (iLeft > maxLeft || iLeft < 0) {
iLeft = GetRelative.Left(2 * iLeft > maxLeft ? "left" : "right") + options.CustomLeft;
};
if (iTop > maxTop || iTop < 0) {
iTop = GetRelative.Top(2 * iTop > maxTop ? "top" : "bottom") + options.CustomTop;
};
实际应用的话估计要按需求重写这部分才行。
如果不是用Reset重新定位,只需要根据这几个值获取适合的值就行了:
iLeft = Math.max(Math.min(iLeft, maxLeft), 0);
iTop = Math.max(Math.min(iTop, maxTop), 0);
【参数设计】
程序中用ShowType、HideType、ShowDelayType和HideDelayType这几个属性来设置执行方式的。
以ShowType显示方式属性为例,原来的方式是分两个bool属性ClickShowType和TouchShowType表示的。
这样的好处是程序判断方便,效率高,问题是使用不方便,感觉混乱。
为了减少参数数量,后来把属性值改成字符形式,可以是以下4个值:
"click":只用点击方式
"touch":只用触发方式
"both":两个都使用
"none":都不使用(其他字符值也当成是"none")
这样就可以把ClickShowType和TouchShowType合并成一个ShowType来表示了。
参数数量是减少了,但程序中就必须每次都要根据字符值判断一下属于哪个类型。
为了方便程序判断,添加了IsClick和IsTouch方法,参数是上面的执行方式属性,用来判断是否使用点击和触发方式。
例如IsClick是这样的:
type = type.toLowerCase();
return type === "both" || type === "click";
这样就间接把字符判断变成bool判断,只是代码比直接bool判断长了点。
【隐藏select】
又是ie6的隐藏select问题,这里用的是iframe遮盖法。
首先初始化时插入iframe:
var iframe = document.createElement("<iframe style='position:absolute;filter:alpha(opacity=0);display:none;'>"); document.body.insertBefore(iframe, document.body.childNodes[0]); this._cssiframe = iframe.style;
在Show的时候,参照Tip设置好样式,再显示:
var css = this._cssiframe; css.width = this.Tip.offsetWidth + "px"; css.height = this.Tip.offsetHeight + "px"; css.left = iLeft + "px"; css.top = iTop + "px"; css.display = "";
其实就是要垫在Tip的下面。
在Hidde时隐藏就可以了。
使用说明
实例化时,第一个必要参数是Tip对象:
var ft = new FixedTips("idTip");
第二个可选参数用来设置触发对象属性的统一默认值。
然后用Add方法添加触发对象:
var trigger1 = ft.Add("idTrigger1");
第二个可选参数用来设置该触发对象属性。
要添加多个触发对象时只需继续用Add添加就行了。
程序源码
var FixedTips = function(tip, options){ this.Tip = $$(tip);//提示框 this._trigger = null;//触发对象 this._timer = null;//定时器 this._cssTip = this.Tip.style;//简化代码 this._onshow = false;//记录当前显示状态 this.SetOptions(options); //处理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";//避免占位出现滚动条 //offset修正参数 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对象时保持显示状态 addEvent(this.Tip, "mouseover", BindAsEventListener(this, function(e){ //如果是外部元素进入,说明当前是隐藏延时阶段,那么清除定时器取消隐藏 this.Check(e.relatedTarget) && clearTimeout(this._timer); })); //ie6处理select if (isIE6) { var iframe = document.createElement("<iframe style='position:absolute;filter:alpha(opacity=0);display:none;'>"); document.body.insertBefore(iframe, document.body.childNodes[0]); this._cssiframe = iframe.style; }; //用于点击方式隐藏 this._fCH = BindAsEventListener(this, function(e) { if (this.Check(e.target) && this.CheckHide()) { this.ReadyHide(this.IsClick(this._trigger.HideDelayType)); }; }); //用于触发方式隐藏 this._fTH = BindAsEventListener(this, function(e) { if (this.Check(e.relatedTarget) && this.CheckHide()) { this.ReadyHide(this.IsTouch(this._trigger.HideDelayType)); }; }); }; FixedTips.prototype = { //设置默认属性 SetOptions: function(options) { this.options = {//默认值 ShowType: "both",//显示方式 HideType: "both",//隐藏方式 ShowDelayType: "touch",//显示延迟方式 HideDelayType: "touch",//隐藏延迟方式 //"click":只用点击方式,"touch":只用触发方式,"both":两个都使用,"none":都不使用 ShowDelay: 300,//显示延时时间 HideDelay: 300,//隐藏延时时间 Fixed: {},//定位对象 onShow: function(){},//显示时执行 onHide: function(){}//隐藏时执行 }; Extend(this.options, options || {}); }, //检查触发元素 Check: function(elem) { //返回是否外部元素(即触发元素和Tip对象本身及其内部元素以外的元素对象) return !this._trigger || !( this.Tip === elem || this._trigger.Elem === elem || Contains(this.Tip, elem) || Contains(this._trigger.Elem, elem) ); }, //准备显示 ReadyShow: function(delay) { clearTimeout(this._timer); var trigger = this._trigger; //触发方式隐藏 this.IsTouch(trigger.HideType) && addEvent(this._trigger.Elem, "mouseout", this._fTH); //点击方式隐藏 this.IsClick(trigger.HideType) && addEvent(document, "click", this._fCH); //显示 if (delay) { this._timer = setTimeout(Bind(this, this.Show), trigger.ShowDelay); } else { this.Show(); }; }, //显示 Show: function() { clearTimeout(this._timer); this._trigger.onShow();//放在前面方便修改属性 //根据预设定位和自定义定位计算left和top var trigger = this._trigger, pos = GetRelative(trigger.Elem, this.Tip, trigger.Fixed), iLeft = pos.Left, iTop = pos.Top; //设置位置并显示 this._cssTip.left = iLeft - this._offsetleft + "px"; this._cssTip.top = iTop - this._offsettop + "px"; this._cssTip.visibility = "visible"; //ie6处理select if (isIE6) { var css = this._cssiframe; css.width = this.Tip.offsetWidth + "px"; css.height = this.Tip.offsetHeight + "px"; css.left = iLeft + "px"; css.top = iTop + "px"; css.display = ""; }; //触发方式隐藏 this.IsTouch(trigger.HideType) && addEvent(this.Tip, "mouseout", this._fTH); }, //准备隐藏 ReadyHide: function(delay) { clearTimeout(this._timer); if (delay) { this._timer = setTimeout(Bind(this, this.Hide), this._trigger.HideDelay); } else { this.Hide(); }; }, //隐藏 Hide: function() { clearTimeout(this._timer); //设置隐藏 this._cssTip.visibility = "hidden"; this._cssTip.left = this._cssTip.top = "-9999px"; //ie6处理select if (isIE6) { this._cssiframe.display = "none"; }; //处理触发对象 if (!!this._trigger) { this._trigger.onHide(); removeEvent(this._trigger.Elem, "mouseout", this._fTH); } this._trigger = null; //移除事件 removeEvent(this.Tip, "mouseout", this._fTH); removeEvent(document, "click", this._fCH); }, //添加触发对象 Add: function(elem, options) { //创建一个触发对象 var elem = $$(elem), trigger = Extend( Extend( { Elem: elem }, this.options ), options || {} ); //点击方式显示 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); }; }; })); //触发方式显示 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); }; }; })); //返回触发对象 return trigger; }, //显示检查 CheckShow: function(trigger) { if ( trigger !== this._trigger ) { //不是同一个触发对象就先执行Hide防止冲突 this.Hide(); this._trigger = trigger; return true; } else { return false; }; }, //隐藏检查 CheckHide: function() { if ( this._cssTip.visibility === "hidden" ) { //本来就是隐藏状态,不需要再执行Hide clearTimeout(this._timer); removeEvent(this._trigger.Elem, "mouseout", this._fTH); this._trigger = null; removeEvent(document, "click", this._fCH); return false; } else { return true; }; }, //是否点击方式 IsClick: function(type) { type = type.toLowerCase(); return type === "both" || type === "click"; }, //是否触发方式 IsTouch: function(type) { type = type.toLowerCase(); return type === "both" || type === "touch"; } };