小莫碎碎念
小莫第一次写技术博客无甚经验,望诸位大神和小白同僚莫要见怪,鉴于希望小莫日后能不忘初衷,每篇博客开头都有雷打不动的常设模块“小莫碎碎念”,关注技术的同僚可以绕过,这个模块基本没什么有用的,咳咳。
小莫最近在做的项目中用了ng2-bootstrap,经常会用到tooltip,但bootstrap的tooltip有一个缺憾,就是在鼠标悬浮在tip上的时候tip就消失了了,bootstrap的解释是移动端不需要鼠标悬浮的功能,小莫从网上查了很多解决方案,但都是基于js的,没有基于ts的,小莫才疏学浅又不愿意动源码,当然也是为了方便小莫学习原生js,小莫当下决定自己用js封装一个tooltip组件。
其实小莫在查阅资料的时候大多不会看实现思路和心路历程之类的前提,都会直接看结果,相信很多同僚和小莫一样只关注结果,但在封装组件的过程中小莫的确遇到了一些不大不小的问题,为了方便记忆也为了整理出一套思考问题的方法,小莫决定从头到尾详细记录,只关注结果的同僚请绕过“实现思路”。
好!碎碎念完毕,进入主题!
实现思路
tooltip是一个相对比较简单的功能,主要需要实现以下三个功能点:
1.tip的弹出和隐藏;
2.tip弹出位置和内容自定义;
3.相关css实现。
第一条和第二条细分又可分为以下几点:
1.鼠标悬浮,弹出tip;
2.鼠标移开,隐藏tip;
3.鼠标移到tip上,tip不隐藏;
4.鼠标移出tip,tip隐藏;
5.从目标元素的上下左右弹出tip。
首先,要实现鼠标over和leave目标元素后,tip的显示和隐藏,分别用onmouseenter和onmouseleave;然后,判断鼠标是否over在tip区域,如果在tip上,tip不隐藏,否则反之,这里要注意添加鼠标leave tip后,tip隐藏的事件;最后,确定tip的弹出位置和内容。
最终代码
css不做赘述直接上代码:
.tooltip{position:absolute;max-width:300px;z-index:999;}
.tip-content{border:1px solid gray;border-radius: 6px;box-shadow: 0 5px 10px rgba(0,0,0,.2);padding:5px;background-color: #ffffff;}
.tip-arrow{
position:absolute;
border:7px solid transparent;
}
.tip-arrow:after{
position:absolute;
content:'';
width:0;height:0;
box-sizing:border-box;
border:7px solid transparent;
}
.tip-content.top{
margin-bottom:7px;
}
.tip-arrow.top{
border-bottom-width:0;bottom: 0;left:50%;margin-left: -7px;
border-top-color:gray;
}
.tip-arrow.top:after{
margin-left:-7px;
margin-top:-8px;
border-bottom-width:0;
border-top-color:#fff;
}
.tip-content.bottom{
margin-top:7px;
}
.tip-arrow.bottom{
border-top-width:0;top: 0;left:50%;margin-left: -7px;
border-bottom-color:gray;
}
.tip-arrow.bottom:after{
margin-left:-7px;
margin-top:1px;
border-top-width:0;
border-bottom-color:#fff;
}
.tip-content.left{
margin-right:7px;
}
.tip-arrow.left{
border-right-width:0;right: 0;top:50%;margin-top: -7px;
border-left-color:gray;
}
.tip-arrow.left:after{
margin-left:-8px;
margin-top:-7px;
border-right-width:0;
border-left-color:#fff;
}
.tip-content.right{
margin-left:7px;
}
.tip-arrow.right{
border-left-width:0;left: 0;top:50%;margin-top: -7px;
border-right-color:gray;
}
.tip-arrow.right:after{
margin-left:1px;
margin-top:-7px;
border-left-width:0;
border-right-color:#fff;
}
js也直接上代码:
function getElementPos(el) {
let _x = 0, _y = 0;
do {
_x += el.offsetLeft;
_y += el.offsetTop;
} while (el = el.offsetParent);
return { x: _x, y: _y };
}
var ToolTip = function(){
var obj = {};
obj.tip = null;
obj.showTip = function(el,html){
if(obj.tip){
return;
}
var elPos = getElementPos(el);
var posi = el.getAttribute('posi')?el.getAttribute('posi'):"top";
var tip = document.createElement("div");
tip.className = 'tooltip';
var arrow = document.createElement("div");
arrow.className = 'tip-arrow '+posi;
var content = document.createElement("div");
content.className = 'tip-content '+posi;
tip.appendChild(content);
tip.appendChild(arrow);
content.innerHTML = html;
document.body.appendChild(tip);
switch (posi){
case "top":{
tip.style.top = (elPos.y - content.offsetHeight - 7) + 'px';
tip.style.left = (elPos.x + el.offsetWidth/2 - content.offsetWidth/2) + 'px';
}
break;
case "bottom":{
tip.style.top = (elPos.y + el.offsetHeight) + 'px';
tip.style.left = (elPos.x + el.offsetWidth/2 - content.offsetWidth/2) + 'px';
}
break;
case "left":{
tip.style.top = (elPos.y + el.offsetHeight/2 - content.offsetHeight/2) + 'px';
tip.style.left = (elPos.x - content.offsetWidth - 7) + 'px';
}
break;
case "right":{
tip.style.top = (elPos.y + el.offsetHeight/2 - content.offsetHeight/2) + 'px';
tip.style.left = (elPos.x + el.offsetWidth) + 'px';
}
break;
}
tip.addEventListener("mouseenter",function(){
tip.setAttribute("in",true);
});
tip.addEventListener("mouseleave",function(){
tip.setAttribute("in",false);
obj.hideTip();
});
obj.tip = tip;
};
obj.hideTip = function () {
if(this.tip && this.tip.getAttribute("in") != "true"){
document.body.removeChild(this.tip);
obj.tip = null;
}
};
obj.init = function(el){
el.onmouseenter = function(){obj.showTip(this, this.getAttribute('tipContent'))};
el.onmouseleave = function(){
setTimeout(function(){
obj.hideTip();
},0);
};
};
return obj;
}
html代码(有的时候小莫这样的小白面对封装好的组件却不会用,那心情相当沉重):
<div class="container" posi = "bottom" name="tip" tipContent="this <br> <button>diandian</button> is a long long long long long <br> long long long long long long tool tip.">hover me</div>
<div class="container" posi = "left" name="tip" tipContent="this is a tool tip2.">hover me2</div>
<div class="container" posi = "right" name="tip" tipContent="this <br> is a long long long long long <br> long long long long long long tool tip3.">hover me3</div>
<div class="container" posi = "top" name="tip" tipContent="this is a tool tip4.">hover me4</div>
<script>
var conArr = document.getElementsByName("tip");
if(conArr) {
for (var i = 0; i < conArr.length; i++) {
var tip = new ToolTip();
tip.init(conArr[i]);
}
}
</script>
顺便附上ts版tooltip:
import {Directive, ElementRef, Input, HostListener} from "@angular/core";
@Directive({
selector: '[tooltip]'
})
export class ToolTip{
@Input('tooltip')
tooltipCon: string;
private tip;
constructor(private elRef: ElementRef) {}
@HostListener('mouseenter') onMouseEnter() {
this.show();
}
@HostListener('mouseleave') onMouseLeave() {
let self = this;
setTimeout(function(){
self.hide();
},0);
}
show(){
let el = this.elRef.nativeElement as HTMLElement;
if(this.tip){
return;
}
let posi = el.getAttribute('posi')?el.getAttribute('posi'):"top";
let elPos = this.getElementPos(el);
let self = this;
let tip = document.createElement("div");
tip.className = 'tip-container';
let arrow = document.createElement("div");
arrow.className = 'tip-arrow '+posi;
let content = document.createElement("div");
content.className = 'tip-content '+posi;
tip.appendChild(content);
tip.appendChild(arrow);
content.innerHTML = this.tooltipCon;
document.body.appendChild(tip);
tip.style.top = (elPos.y - content.offsetHeight - 10) + 'px';
tip.style.left = (elPos.x + el.offsetWidth/2 - content.offsetWidth/2) + 'px';
switch (posi){
case "top":{
tip.style.top = (elPos.y - content.offsetHeight - 7) + 'px';
tip.style.left = (elPos.x + el.offsetWidth/2 - content.offsetWidth/2) + 'px';
}
break;
case "bottom":{
tip.style.top = (elPos.y + el.offsetHeight) + 'px';
tip.style.left = (elPos.x + el.offsetWidth/2 - content.offsetWidth/2) + 'px';
}
break;
case "left":{
tip.style.top = (elPos.y + el.offsetHeight/2 - content.offsetHeight/2) + 'px';
tip.style.left = (elPos.x - content.offsetWidth - 7) + 'px';
}
break;
case "right":{
tip.style.top = (elPos.y + el.offsetHeight/2 - content.offsetHeight/2) + 'px';
tip.style.left = (elPos.x + el.offsetWidth) + 'px';
}
break;
}
tip.addEventListener("mouseenter",function(){
tip.setAttribute("in","true");
});
tip.addEventListener("mouseleave",function(){
tip.setAttribute("in","false");
self.hide();
});
this.tip = tip;
}
hide(){
if(this.tip && this.tip.getAttribute("in") != "true"){
document.body.removeChild(this.tip);
this.tip = null;
}
}
getElementPos(el) {
let _x = 0, _y = 0;
do {
_x += el.offsetLeft;
_y += el.offsetTop;
} while (el = el.offsetParent);
return { x: _x, y: _y };
}
}
按照一般指令使用即可,使用方法小莫就不在赘述了:
1 <div tooltip="this is a tool tip." posi = "bottom"> 2 hover me 3 </div> 4 <div tooltip="this is a tool tip2." posi = "left"> 5 hover me2 6 </div> 7 <div tooltip="this is a tool tip3." posi = "right"> 8 hover me3 9 </div> 10 <div tooltip="this is a tool tip4." posi = "top"> 11 hover me4 12 </div>
只关注结果的童鞋可以直接右拐直行了,下面都是代码分析啦!
代码分析
css没什么可说的,只是一些小技巧,给箭头元素添加一个背景色为白的:after元素,再控制一下位置,就能将箭头的边框画出来了,除此之外大家想必都很熟悉了,我们还是着重说js。
1 function getElementPos(el) { 2 let _x = 0, _y = 0; 3 do { 4 _x += el.offsetLeft; 5 _y += el.offsetTop; 6 } while (el = el.offsetParent); 7 return { x: _x, y: _y }; 8 }
这个方法用于获取目标元素左上角相对于窗体的坐标,由于offsetLeft和offsetTop只能用于获取元素相对于父元素的位置,所以需要循环获取父元素的坐标直至根元素。
我们先从初始化说起,给目标元素添加over和leave事件:
1 obj.init = function(el){ 2 el.onmouseenter = function(){obj.showTip(this, this.getAttribute('tipContent'))}; 3 el.onmouseleave = function(){ 4 setTimeout(function(){ 5 obj.hideTip(); 6 },0); 7 8 }; 9 };
这里提一句,动态给元素添加事件的方法基本有三个,感兴趣的小盆友请猛戳这里:点我点我点我。添加setTimeout是为了将这段代码放到后面执行,也就是说需要在给tip元素的in属性赋值后执行,否则会先执行hide方法再给tip元素的in属性赋值(那么这个赋值就没有任何意义了,因为我们在隐藏tip的时候要判断tip元素的in属性是否为true)。
然后是重头戏tip的显示。
1 obj.showTip = function(el,html){ 2 //如果当前目标元素的tip已经显示,返回,避免重复生成tip元素。 3 if(obj.tip){ 4 return; 5 } 6 //获取目标元素坐标 7 var elPos = getElementPos(el); 8 //获取tip弹出位置 9 var posi = el.getAttribute('posi')?el.getAttribute('posi'):"top"; 10 11 //创建tip元素 12 var tip = document.createElement("div"); 13 tip.className = 'tooltip'; 14 //创建箭头元素 15 var arrow = document.createElement("div"); 16 arrow.className = 'tip-arrow '+posi; 17 //创建内容元素 18 var content = document.createElement("div"); 19 content.className = 'tip-content '+posi; 20 //给tip元素添加箭头元素和内容元素 21 tip.appendChild(content); 22 tip.appendChild(arrow); 23 content.innerHTML = html; 24 //将tip元素添加到body,必须先将元素添加到body,后面的代码才会生效 25 document.body.appendChild(tip); 26 //根据不同弹出位置确定tip的坐标 27 switch (posi){ 28 case "top":{ 29 tip.style.top = (elPos.y - content.offsetHeight - 7) + 'px'; 30 tip.style.left = (elPos.x + el.offsetWidth/2 - content.offsetWidth/2) + 'px'; 31 } 32 break; 33 case "bottom":{ 34 tip.style.top = (elPos.y + el.offsetHeight) + 'px'; 35 tip.style.left = (elPos.x + el.offsetWidth/2 - content.offsetWidth/2) + 'px'; 36 } 37 break; 38 case "left":{ 39 tip.style.top = (elPos.y + el.offsetHeight/2 - content.offsetHeight/2) + 'px'; 40 tip.style.left = (elPos.x - content.offsetWidth - 7) + 'px'; 41 } 42 break; 43 case "right":{ 44 tip.style.top = (elPos.y + el.offsetHeight/2 - content.offsetHeight/2) + 'px'; 45 tip.style.left = (elPos.x + el.offsetWidth) + 'px'; 46 } 47 break; 48 } 49 50 //当鼠标进入tip区域,将属性in设置为true 51 tip.addEventListener("mouseenter",function(){ 52 tip.setAttribute("in",true); 53 }); 54 //当鼠标离开tip区域,将属性in设置为false,同时隐藏tip 55 tip.addEventListener("mouseleave",function(){ 56 tip.setAttribute("in",false); 57 obj.hideTip(); 58 }); 59 60 //将tip元素赋值给obj,以判断当前目标元素是否已经拥有tip元素,同时在hide的时候判断当前需要移出的是哪个tip元素 61 obj.tip = tip; 62 };
隐藏tip就相对简单多啦:
1 obj.hideTip = function () { 2 //判断当前目标元素的tip元素的in属性,也就是判断鼠标是否在tip区域内 3 if(this.tip && this.tip.getAttribute("in") != "true"){ 4 document.body.removeChild(this.tip); 5 //将当前目标元素的tip元素置为null 6 obj.tip = null; 7 } 8 };
组件写完啦,下面看看怎么使用它:
1 //遍历所有tooltip的目标元素,然后初始化 2 var conArr = document.getElementsByName("tip"); 3 if(conArr) { 4 for (var i = 0; i < conArr.length; i++) { 5 var tip = new ToolTip(); 6 tip.init(conArr[i]); 7 } 8 }
本来小莫并没有每一个目标元素对应一个tip元素,但后来发现当目标元素都聚集在一起的时候(比方说表格)会出现tip闪烁或者不显示的bug,究其原因是多个目标元素共用一个tip,而hide方法放到了setTimeout中,导致下一个tip元素刚生成就被hide了,所以要求每一个目标元素对应一个tip元素。
大功告成!
总结
虽然是小莫原生js封装组件的处女座,但仿佛没有什么可总结的,但这个环节必须要有!ok,多谢能坚持到最后的各位同僚!小莫拜谢!