tooltip是bootstrap的一个封装插件,用于快速简便生成提示。bootstrap基于angularJS开发了angular-bootstrap包,其中的tooltip实现原理与原来基于jquery的tooltip实现原理相同,但是由于angular的特性,tooltip在此基础上又有了不同的改进。
由于angular数据绑定的特性,同样angular-bootstrap的tooltip也带有这种能力。tooltip的文本能够随着用户操作变化,大幅降低了编写动态提示(tooltip)的难度。
生成tooltip完整流程:触发显示事件 -> 创建tooltip -> 调整tooltip位置 -> 添加动画 -> 显示tooltip -> 触发隐藏事件 -> 隐藏tooltip -> 删除tooltip
具体来讲,angular-bootstrap的tooltip实现分4个主要步骤,初始化配置、数据注入、生成tooltip、编译模板。
初始化配置并不难,首先我们需要一个provider(在angular中provider在配置文件中预先调用,即provider作为模块配置的API)生成初始化配置需要的接口。
provider:
//TODO: 初始化tooltip的显示样式与动画效果
var defaultOptions = {
placement: 'top',
placementClassPrefix: '',
animation: true,
popupDelay: 0,
popupCloseDelay: 0,
useContentExp: false
};
//TODO: 设定tooltip默认的触发条件,可以选择以下五种之一,
//也可以由自己编写一个自定义的触发方式
var triggerMap = {
'mouseenter': 'mouseleave',
'click': 'click',
'outsideClick': 'outsideClick',
'focus': 'blur',
'none': ''
};
var globalOptions = {};
//TODO: 通过this返回API
this.options = function (value) {
angular.extend(globalOptions, value);
};
this.setTriggers = function setTriggers(triggers) {
angular.extend(triggerMap, triggers);
};
这样就可以在config文件中通过options和setTriggers两个接口设置tooltip的默认样式和默认触发条件了。
接下来是添加模板,tooltip使用了指令的方式去编译模板,首先定义一个模板
var template = ''
然后在模板中加入自定义的内容
var template =
'
(options.useContentExp ?
'content-exp="contentExp()" ' :
'content="' + startSym + 'content' + endSym + '" ') +
'origin-scope="origScope" ' +
'>' +
'
这个过程是怎么实现的呢,事实上比较巧妙,这里使用了二次绑定的手法,tooltip分为两层作用域(scope),首层的作用域用于传递数据,即绑定数据的来源,tooltip的模板地址tooltip-templlate、模板内容tooltip-html、文本内容tooltip等等。
ttScope.origScope = scope;
通过指令中的属性绑定,即
function uibATooltipTemplatePopup() {
return {
scope: {
originScope: '=',
content: '=',
}
……
}
}
将首层作用域“originScope”与tooltip的中的html内容“content”传递到下一层(我将它称为模板处理层),这一层的作用是使用cache的形式获取模板数据(新建一个空白的tooltip可以在控制台查看模板的结构,也可以在源代码中查找“html”的部分查看缓存的模板),然后利用html内容与数据将tooltip初始化。
初始化的过程如下:
首先申请模板:
$templateRequest(templateSrc, true)
然后传入数据:
var newScope = origScope.$new();
编译模板:
var clone = $compile(template)(newScope, function(clone) {
cleanupLastIncludeContent();
$animate.enter(clone, elem);
});
绑定tooltip与其作用域
currentElement = clone;
currentScope = newScope;
绑定tooltip与其作用域的作用是在多个tooltip存在的情况下,清理上个tooltip的数据并加载当前tooltip的数据。
var cleanupLastIncludeContent = function() {
if (previousElement) {
previousElement.remove();
previousElement = null;
}
if (currentScope) {
currentScope.$destroy();
currentScope = null;
}
if (currentElement) {
$animate.leave(currentElement).then(function() {
previousElement = null;
});
previousElement = currentElement;
currentElement = null;
}
};
一个简单的tooltip已经完成了,接下来是给它添加上控制逻辑。
一是tooltip的位置控制,根据绑定的元素、设定好的显示方向、是否添加到body元素三者来决定tooltip的位置。
var ttPosition = $position.positionElements(element, tooltip, ttScope.placement, appendToBody);
二是触发控制,根据配置的触发条件绑定事件。
triggers = {
show: showTriggers,
hide: hideTriggers
};
triggers.show.forEach(function (trigger, idx) {
if (trigger === 'outsideClick') {
element.on('click', toggleTooltipBind);
$document.on('click', bodyHideTooltipBind);
} else if (trigger === triggers.hide[idx]) {
element.on(trigger, toggleTooltipBind);
} else if (trigger) {
element.on(trigger, showTooltipBind);
element.on(triggers.hide[idx], hideTooltipBind);
}});
最后是编译模板并生产tooltip:
var tooltipLinker = $compile(template);
tooltip = tooltipLinker(tooltipLinkedScope, function (tooltip) {
if (appendToBody) {
$document.find('body').append(tooltip);
} else {
element.after(tooltip);
}
});
做到这一步,tooltip已经可以正常生成和显示了,但是还没完,这只能实现一个简单的tooltip。要实现一个完整的控件,还要对此加上控制与销毁tooltip的逻辑。
管理tooltip实际上是通过管理它的父元素作用域来完成的,那么我们需要建立一个容器来盛放每个tooltip父元素的作用域。
创建一个map:
function stackMap() {
return {
//TODO: 通过createNew来初始化一个新的map
createNew: function() {
var stack = [];
}
}
接下来给它加上增删查等功能:
function stackMap() {
return {
//TODO: 通过createNew来初始化一个新的map
createNew: function() {
var stack = [];
return {
add: function (key, value) {
stack.push({
key: key,
value: value
});
},
get: function (key) {},
getKeys: function() {},
top: function () {
return stack[stack.length - 1];
},
remove: function (key) {},
removeTop: function (key) {},
length: function () {}
}
}
}
在建立了这么一个tooltip专有的容器后用它来存放每次新建的tooltip的作用域:
var openedTooltips = $$stackedMap.createNew();
//TODO: 通过tooltip provider的作用域生成它的子作用域,即对应tooltip的scope
var ttScope = scope.$new(true);
openedTooltips.add(ttScope, {
close: hide
});
通过属性值监控tooltip:
if (!ttScope.isOpen) {
showTooltipBind();
} else {
hideTooltipBind();
}
在路由变化之后删除掉多余的父元素作用域,避免造成冗余的$digest回调:
openedTooltips.remove(ttScope);
总结:主要考虑的地方在于数据传递、模板构造、加载新tooltip,清理冗余。