popper.js——tooltip文档及源码分析

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-绑定

选项类型说明
placementString弹出位置默认:bottom;top(-start, -end), right(-start, -end), bottom(-start, -end), left(-start, -end)
arrowSelectorString设置三角的class默认:.tooltip-arrow, .tooltip__arrow
innerSelectorString设置内容容器的class默认:.tooltip-inner, .tooltip__inner
containerHTMLElement、String、false将tooltip元素插入到container元素默认:false (参照元素的父节点)
delayNumber、Object延迟显示或隐藏 ,不适用手动触发类型;默认:0;如果是一个值(Number),同时被应用到显示和隐藏;如果是对象{ show: 500, hide: 100 },则分别被应用到显示和隐藏
htmlBoolean把title中的内容以text(false)或者html(true)格式插入到tooltip-inner里默认:false
templateString插入的模板内容默认:<div class="tooltip" role="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>
titleString、HTMLElement、TitleFunction弹出的具体内容title
triggerString触发方式:多种触发方式,中间用空格隔开;manual 手动,不能与其他方式组合使用;默认:hover focus;可选hover、focus、click、manual
closeOnClickOutsideBoolean是否在点击弹出元素和参照元素以外,关闭弹出元素;只适用于click方式默认:false
boundariesElementString、HTMLElement设置弹出元素的边界
offsetString、Number相对参照元素的偏移量设置默认:0
popperOptionsObject直接设置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)
    })
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

了 义

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值