popper.js之tooltip文档及源码分析
popper说明
由于popper.js这个名字比较大众化,有必要说明一下具体指的是哪一个;
没错就是你想的这个:
https://popper.js.org/tooltip-examples.html
https://github.com/FezVrasta/popper.js
popper 是一个定位引擎,就是说他只做一件事使指定节点定位到指定位置。至于DOM的显示、隐藏、内容、外观、丑美胖瘦……他都不管
那tooltip.js和popper是什么关系呢?
可以说是理论与实践的关系,tooltip是对popper的具体应用。想想是不是很兴奋,什么下拉菜单、select表单模拟、提示工具……总之,一切弹出定位的UI组件都可以通过这个引擎实现。
tooltip文档简单说明
调用方式
new Tooltip(element,options);
参数说明
element
- DOM节点 :如 document.getElementById(el)
- jquery 对象 : 如 $(el) , 默认 取第0个元素
options
选项既可以在js初始化时传入,也可以在元素上通过data-绑定
选项 | 类型 | 说明 | 值 |
---|---|---|---|
placement | String | 弹出位置 | 默认:bottom;top(-start, -end), right(-start, -end), bottom(-start, -end), left(-start, -end) |
arrowSelector | String | 设置三角的class | 默认:.tooltip-arrow, .tooltip__arrow |
innerSelector | String | 设置内容容器的class | 默认:.tooltip-inner, .tooltip__inner |
container | HTMLElement、String、false | 将tooltip元素插入到container元素 | 默认:false (参照元素的父节点) |
delay | Number、Object | 延迟显示或隐藏 ,不适用手动触发类型; | 默认:0;如果是一个值(Number),同时被应用到显示和隐藏;如果是对象{ show: 500, hide: 100 },则分别被应用到显示和隐藏 |
html | Boolean | 把title中的内容以text(false)或者html(true)格式插入到tooltip-inner里 | 默认:false |
template | String | 插入的模板内容 | 默认:<div class="tooltip" role="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div> |
title | String、HTMLElement、TitleFunction | 弹出的具体内容title | |
trigger | String | 触发方式:多种触发方式,中间用空格隔开;manual 手动,不能与其他方式组合使用; | 默认:hover focus;可选hover、focus、click、manual |
closeOnClickOutside | Boolean | 是否在点击弹出元素和参照元素以外,关闭弹出元素;只适用于click方式 | 默认:false |
boundariesElement | String、HTMLElement | 设置弹出元素的边界 | |
offset | String、Number | 相对参照元素的偏移量设置 | 默认:0 |
popperOptions | Object | 直接设置popper实例的选项 |
tooltip源码注释
import Popper from 'popper.js';
import isFunction from '../../popper/src/utils/isFunction';
const DEFAULT_OPTIONS = {
container: false,
delay: 0,
html: false,
placement: 'top',
title: '',
template:
'<div class="tooltip" role="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>',
trigger: 'hover focus',
offset: 0,
arrowSelector: '.tooltip-arrow, .tooltip__arrow',
innerSelector: '.tooltip-inner, .tooltip__inner',
};
export default class Tooltip {
constructor(reference, options) {
// apply user options over default ones
// 用户选项覆盖默认选项
options = { ...DEFAULT_OPTIONS, ...options };
// 如果是jq对象,取第一个————转成js节点
reference.jquery && (reference = reference[0]);
// cache reference and options
// 缓存参照元素和选项
this.reference = reference;
this.options = options;
// get events list
// 获取触发方式的数组列表
// events 是数组,触发事件的数组
const events =
typeof options.trigger === 'string'
? options.trigger
.split(' ')
.filter(
trigger => ['click', 'hover', 'focus'].indexOf(trigger) !== -1
)
: [];
// set initial state
// 设置出事状态
// 显示或隐藏。默认隐藏
this._isOpen = false;
// Popper实例选项
this._popperOptions = {};
// set event listeners
// 设置事件监听
this._setEventListeners(reference, events, options);
}
//
// Public methods
//
/**
* Reveals an element's tooltip. This is considered a "manual" triggering of the tooltip.
* Tooltips with zero-length titles are never displayed.
* @method Tooltip#show
* @memberof Tooltip
*/
// manual 手动触发
show = () => this._show(this.reference, this.options);
/**
* Hides an element’s tooltip. This is considered a “manual” triggering of the tooltip.
* @method Tooltip#hide
* @memberof Tooltip
*/
// manual 手动触发
hide = () => this._hide();
/**
* Hides and destroys an element’s tooltip.
* @method Tooltip#dispose
* @memberof Tooltip
*/
// manual 手动触发
dispose = () => this._dispose();
/**
* Toggles an element’s tooltip. This is considered a “manual” triggering of the tooltip.
* @method Tooltip#toggle
* @memberof Tooltip
*/
// manual 手动触发
toggle = () => {
if (this._isOpen) {
return this.hide();
} else {
return this.show();
}
};
/**
* Updates the tooltip's title content
* @method Tooltip#updateTitleContent
* @memberof Tooltip
* @param {String|HTMLElement} title - The new content to use for the title
*/
// manual 手动触发
updateTitleContent = (title) => this._updateTitleContent(title);
//
// Private methods
//
// 私有方法列表
_events = [];
/**
* Creates a new tooltip node
* @memberof Tooltip
* @private
* @param {HTMLElement} reference
* @param {String} template
* @param {String|HTMLElement|TitleFunction} title
* @param {Boolean} allowHtml
* @return {HTMLElement} tooltipNode
*/
_create(reference, template, title, allowHtml) {
// create tooltip element
// 创建一个div元素,里面插入template内容
const tooltipGenerator = window.document.createElement('div');
tooltipGenerator.innerHTML = template.trim();
// 获取到模板中的第一个子节点————class=“tooltip”
const tooltipNode = tooltipGenerator.childNodes[0];
// add unique ID to our tooltip (needed for accessibility reasons)
// 给class=“tooltip”添加一个随机ID
tooltipNode.id = `tooltip_${Math.random()
.toString(36)
.substr(2, 10)}`;
// set initial `aria-hidden` state to `false` (it's visible!)
// 设置属性为可见的
tooltipNode.setAttribute('aria-hidden', 'false');
// add title to tooltip
// 获取title节点,————tooltip-inner(现在里面是空的)
const titleNode = tooltipGenerator.querySelector(this.options.innerSelector);
// 给title节点添加内容
this._addTitleContent(reference, title, allowHtml, titleNode);
// return the generated tooltip node
// 返回新创建div里的内容,也就是class tooltip 节点
return tooltipNode;
}
_addTitleContent(reference, title, allowHtml, titleNode) {
if (title.nodeType === 1 || title.nodeType === 11) {
// if title is a element node or document fragment, append it only if allowHtml is true
// 如果title内容是元素节点或者 #document 片段
// 如果option.html=true,允许插入html类型
// 把title内容插入title节点(tooltip-inner)
allowHtml && titleNode.appendChild(title);
} else if (isFunction(title)) {
// if title is a function, call it and set textContent or innerHtml depending by `allowHtml` value
// 如果title是一个函数;
// titleText 调用函数,并且把this指向参照元素,获取title函数的返回值,
const titleText = title.call(reference);
// 如果option.html=true,允许插入html类型--> 则插入title的所有内容;
// 否则插入title中的所有文本,包括<style>和display:none中的文本
allowHtml
? (titleNode.innerHTML = titleText)
: (titleNode.textContent = titleText);
} else {
// if it's just a simple text, set textContent or innerHtml depending by `allowHtml` value
// 如果不是元素也不是函数,根据option.html类型插入
allowHtml ? (titleNode.innerHTML = title) : (titleNode.textContent = title);
}
}
_show(reference, options) {
// don't show if it's already visible
// or if it's not being showed
// 如果实例是显示状态 且 不是正在显示中,则return
if (this._isOpen && !this._isOpening) {
return this;
}
// 设置实例为显示状态
this._isOpen = true;
// if the tooltipNode already exists, just show it
// 如果tooltip节点存在,则显示他
if (this._tooltipNode) {
this._tooltipNode.style.visibility = 'visible';
this._tooltipNode.setAttribute('aria-hidden', 'false');
this.popperInstance.update();
return this;
}
// tooltip节点不存在时
// get title 获取title从参照元素的属性中或者选项中
const title = reference.getAttribute('title') || options.title;
// don't show tooltip if no title is defined
// 如果title为空,则return,不显示,不创建节点
if (!title) {
return this;
}
// create tooltip node
// 创建tooltip节点
const tooltipNode = this._create(
reference,
options.template,
title,
options.html
);
// Add `aria-describedby` to our reference element for accessibility reasons
// 给参照元素设置tooltip的对应ID
reference.setAttribute('aria-describedby', tooltipNode.id);
// append tooltip to container
// 把创建的tooltip元素,插入到指定容器
// 根据options.container 获取插入的指定容器
// ————如果option.container 是一个字符串,则获取这个元素,(可能为空)
// ————如果是默认值,false,则是参照元素的父节点
const container = this._findContainer(options.container, reference);
// tooltipNode 插入 container
this._append(tooltipNode, container);
// 设置popper实例的选项
// 选项中配置的popper实例选项和配置的方位选项
this._popperOptions = {
...options.popperOptions,
placement: options.placement,
};
// modifiers 调节器
this._popperOptions.modifiers = {
...this._popperOptions.modifiers,
arrow: {
element: this.options.arrowSelector,
},
offset: {
offset: options.offset,
},
};
// 通过 modifiers 调节器设置,元素边界
if (options.boundariesElement) {
this._popperOptions.modifiers.preventOverflow = {
boundariesElement: options.boundariesElement,
};
}
// popper 实例
this.popperInstance = new Popper(
reference,
tooltipNode,
this._popperOptions
);
// 把弹出tooltip节点,添加到tooltip实例
this._tooltipNode = tooltipNode;
return this;
}
_hide(/*reference, options*/) {
// don't hide if it's already hidden
// 如果是隐藏状态,则return
if (!this._isOpen) {
return this;
}
// 设置状态,为隐藏
this._isOpen = false;
// hide tooltipNode
// 隐藏节点
this._tooltipNode.style.visibility = 'hidden';
this._tooltipNode.setAttribute('aria-hidden', 'true');
return this;
}
_dispose() {
// remove event listeners first to prevent any unexpected behaviour
this._events.forEach(({ func, event }) => {
this.reference.removeEventListener(event, func);
});
this._events = [];
if (this._tooltipNode) {
this._hide();
// destroy instance
this.popperInstance.destroy();
// destroy tooltipNode if removeOnDestroy is not set, as popperInstance.destroy() already removes the element
if (!this.popperInstance.options.removeOnDestroy) {
this._tooltipNode.parentNode.removeChild(this._tooltipNode);
this._tooltipNode = null;
}
}
return this;
}
_findContainer(container, reference) {
// if container is a query, get the relative element
// 如果option.container 是一个字符串,则获取这个元素,(可能为空)
if (typeof container === 'string') {
container = window.document.querySelector(container);
} else if (container === false) {
// if container is `false`, set it to reference parent
// 如果是默认值,false,则是参照元素的父节点
container = reference.parentNode;
}
// 其他情况未做过滤处理,直接返回
// container 值只能是惟一的,不可能同时找到多个元素节点
return container;
}
/**
* Append tooltip to container
把节点插入到容器
* @memberof Tooltip
* @private
* @param {HTMLElement} tooltipNode
* @param {HTMLElement|String|false} container
*/
_append(tooltipNode, container) {
container.appendChild(tooltipNode);
}
_setEventListeners(reference, events, options) {
// 储存触发方式事件列表
// 显示事件列表
const directEvents = [];
// 隐藏事件列表
const oppositeEvents = [];
events.forEach(event => {
switch (event) {
case 'hover':
directEvents.push('mouseenter');
oppositeEvents.push('mouseleave');
break;
case 'focus':
directEvents.push('focus');
oppositeEvents.push('blur');
break;
case 'click':
directEvents.push('click');
oppositeEvents.push('click');
break;
}
});
// schedule show tooltip
// 根据触发事件,设置tooltip显示
directEvents.forEach(event => {
// 事件监听,执行函数
const func = evt => {
// evt 进入时,事件
// 如果参照元素是 正在 显示过程 状态,则return
if (this._isOpening === true) {
return;
}
// 如果显示与隐藏是同一个事件,表示当前事件已被占用(click)
evt.usedByTooltip = true;
// 触发tooltip显示,设置延时显示
this._scheduleShow(reference, options.delay, options, evt);
};
// 将触发事件和对应方法,保存到_events数组;
this._events.push({ event, func });
// 对参照元素设置,触发事件监听
reference.addEventListener(event, func);
});
// schedule hide tooltip
// 根据触发事件,设置tooltip隐藏
oppositeEvents.forEach(event => {
// 事件监听,执行函数
const func = evt => {
// evt 离开时,事件
// 如果事件被显示占用,则return
if (evt.usedByTooltip === true) {
return;
}
// 触发tooltip隐藏,设置延时隐藏
this._scheduleHide(reference, options.delay, options, evt);
};
// 将触发事件和对应方法,保存到_events数组;
this._events.push({ event, func });
// 对参照元素设置,触发事件监听
reference.addEventListener(event, func);
// 在触发事件为click的情况下,判断是否可以,通过点击tooltip和参照元素以外的地方,关闭tooltip
if (event === 'click' && options.closeOnClickOutside) {
// 在document上监听鼠标按下事件
document.addEventListener('mousedown', e => {
// 如果是关闭状态,则return
if (!this._isOpening) {
return;
}
// popper弹出元素
const popper = this.popperInstance.popper;
// 如果 点击在参照元素上或者在popper上,则return
if (reference.contains(e.target) ||
popper.contains(e.target)) {
return;
}
// 打开状态,且click在元素外,去隐藏
func(e);
}, true);
}
});
}
_scheduleShow(reference, delay, options /*, evt */) {
// 设置实例状态为true---打开(开始显示,或正在显示的过程中)
this._isOpening = true;
// defaults to 0
// 延迟时间,默认0
const computedDelay = (delay && delay.show) || delay || 0;
// this._showTimeout 显示的setTimeout延迟对象
this._showTimeout = window.setTimeout(
() => this._show(reference, options),
computedDelay
);
}
_scheduleHide(reference, delay, options, evt) {
// 设置实例状态为false---隐藏(开始隐藏,或正在隐藏的过程中)
this._isOpening = false;
// defaults to 0
// 获取延时时间,默认为0
const computedDelay = (delay && delay.hide) || delay || 0;
// 延时隐藏
window.setTimeout(() => {
// 把正在显示的tooltip节点(还未显示),清除关闭
window.clearTimeout(this._showTimeout);
// 如果是关闭状态则return
if (this._isOpen === false) {
return;
}
// 如果tooltip节点不存在,则return
if (!document.body.contains(this._tooltipNode)) {
return;
}
// if we are hiding because of a mouseleave, we must check that the new
// reference isn't the tooltip, because in this case we don't want to hide it
// 如果根据mouseleave去隐藏
// 就必须判断,从参照元素移入的元素是不是tooltip节点;
if (evt.type === 'mouseleave') {
const isSet = this._setTooltipNodeEvent(evt, reference, delay, options);
// if we set the new event, don't hide the tooltip yet
// the new event will take care to hide it if necessary
// 如果移入的是tooltip节点,则不隐藏
if (isSet) {
return;
}
}
this._hide(reference, options);
}, computedDelay);
}
_setTooltipNodeEvent = (evt, reference, delay, options) => {
// 移入的元素,可能是tooltip元素,也可能是其他元素,总之不应该是参照元素
const relatedreference =
evt.relatedreference || evt.toElement || evt.relatedTarget;
const callback = evt2 => {
// 从tooltip节点上移出,进入的元素
const relatedreference2 =
evt2.relatedreference || evt2.toElement || evt2.relatedTarget;
// Remove event listener after call
// 移出取消对tooltip节点的监听
this._tooltipNode.removeEventListener(evt.type, callback);
// If the new reference is not the reference element
// 如果移到的这样元素不是参照元素,则去隐藏
if (!reference.contains(relatedreference2)) {
// Schedule to hide tooltip
this._scheduleHide(reference, options.delay, options, evt2);
}
};
// 如果tooltip节点包含这个移入的元素,也就是说从参照元素出到tooltip上了
if (this._tooltipNode.contains(relatedreference)) {
// listen to mouseleave on the tooltip element to be able to hide the tooltip
// 在tooltip节点上添加mouseleave事件监听
this._tooltipNode.addEventListener(evt.type, callback);
return true;
}
return false;
};
_updateTitleContent(title) {
// 如果tooltip节点,不存在(实例已生成,但是还没生成节点)
// 如果选项中存在title属性,就把title保存到实例中
if(typeof this._tooltipNode === 'undefined') {
if(typeof this.options.title !== 'undefined') {
this.options.title = title;
}
return;
}
// 如果节点存在
// 清空节点中内容,给节点重新赋值,并保存到实例选项中
// popper 更新
const titleNode = this._tooltipNode.parentNode.querySelector(this.options.innerSelector);
this._clearTitleContent(titleNode, this.options.html, this.reference.getAttribute('title') || this.options.title)
this._addTitleContent(this.reference, title, this.options.html, titleNode);
this.options.title = title;
this.popperInstance.update();
}
// 清空节点中内容
_clearTitleContent(titleNode, allowHtml, lastTitle) {
if(lastTitle.nodeType === 1 || lastTitle.nodeType === 11) {
allowHtml && titleNode.removeChild(lastTitle);
} else {
allowHtml ? titleNode.innerHTML = '' : titleNode.textContent = '';
}
}
}
jquery 简单调用
function tips(selector,option){
$(selector).each(function(){
new Tooltip(this,option)
})
}