From cc6e130fc1c6f794fcdb24737cb584ac2c937d33 Mon Sep 17 00:00:00 2001
From: Alessandro Chitolina <alekitto@gmail.com>
Date: Thu, 21 Sep 2017 18:04:47 +0200
Subject: [PATCH] tooltip without jquery

---
 js/src/dom/data.js                |   1 -
 js/src/dom/eventHandler.js        | 145 ++++++++++++---------
 js/src/dom/selectorEngine.js      |   2 -
 js/src/popover.js                 |  49 ++++----
 js/src/tooltip.js                 | 202 +++++++++++++++++-------------
 js/src/util.js                    |   4 +
 js/tests/unit/dom/eventHandler.js |  14 +++
 js/tests/unit/popover.js          |  18 ++-
 js/tests/unit/tooltip.js          | 101 +++++++--------
 js/tests/visual/popover.html      |   2 +
 js/tests/visual/tooltip.html      |   3 +
 11 files changed, 306 insertions(+), 235 deletions(-)

diff --git a/js/src/dom/data.js b/js/src/dom/data.js
index f3e4386fcc..82ff5eae83 100644
--- a/js/src/dom/data.js
+++ b/js/src/dom/data.js
@@ -6,7 +6,6 @@
  */
 
 const Data = (() => {
-
   /**
    * ------------------------------------------------------------------------
    * Constants
diff --git a/js/src/dom/eventHandler.js b/js/src/dom/eventHandler.js
index 7854401c37..465cbbeacc 100644
--- a/js/src/dom/eventHandler.js
+++ b/js/src/dom/eventHandler.js
@@ -8,7 +8,6 @@ import Util from '../util'
  */
 
 const EventHandler = (() => {
-
   /**
    * ------------------------------------------------------------------------
    * Polyfills
@@ -90,10 +89,6 @@ const EventHandler = (() => {
    * ------------------------------------------------------------------------
    */
 
-  const TransitionEndEvent = {
-    WebkitTransition : 'webkitTransitionEnd',
-    transition       : 'transitionend'
-  }
   const namespaceRegex = /[^.]*(?=\..*)\.|.*/
   const stripNameRegex = /\..*/
   const keyEventRegex  = /^key/
@@ -123,51 +118,125 @@ const EventHandler = (() => {
    * ------------------------------------------------------------------------
    */
 
-
   function getUidEvent(element, uid) {
-    return element.uidEvent = uid && `${uid}::${uidEvent++}` || element.uidEvent || uidEvent++
+    return uid && `${uid}::${uidEvent++}` || element.uidEvent || uidEvent++
   }
 
   function getEvent(element) {
     const uid = getUidEvent(element)
+    element.uidEvent = uid
+
     return eventRegistry[uid] = eventRegistry[uid] || {}
   }
 
-  function fixEvent(event) {
+  function fixEvent(event, element) {
     // Add which for key events
     if (event.which === null && keyEventRegex.test(event.type)) {
       event.which = event.charCode !== null ? event.charCode : event.keyCode
     }
-    return event
+
+    event.delegateTarget = element
   }
 
   function bootstrapHandler(element, fn) {
-    return function (event) {
-      event = fixEvent(event)
+    return function handler(event) {
+      fixEvent(event, element)
+      if (handler.oneOff) {
+        EventHandler.off(element, event.type, fn)
+      }
+
       return fn.apply(element, [event])
     }
   }
 
   function bootstrapDelegationHandler(element, selector, fn) {
-    return function (event) {
-      event = fixEvent(event)
+    return function handler(event) {
       const domElements = element.querySelectorAll(selector)
       for (let target = event.target; target && target !== this; target = target.parentNode) {
         for (let i = domElements.length; i--;) {
           if (domElements[i] === target) {
+            fixEvent(event, target)
+            if (handler.oneOff) {
+              EventHandler.off(element, event.type, fn)
+            }
+
             return fn.apply(target, [event])
           }
         }
       }
+
       // To please ESLint
       return null
     }
   }
 
+  function findHandler(events, handler) {
+    for (const uid in events) {
+      if (!Object.prototype.hasOwnProperty.call(events, uid)) {
+        continue
+      }
+
+      if (events[uid].originalHandler === handler) {
+        return events[uid]
+      }
+    }
+
+    return null
+  }
+
+  function addHandler(element, originalTypeEvent, handler, delegationFn, oneOff) {
+    if (typeof originalTypeEvent !== 'string' || (typeof element === 'undefined' || element === null)) {
+      return
+    }
+
+    if (!handler) {
+      handler = delegationFn
+      delegationFn = null
+    }
+
+    const delegation      = typeof handler === 'string'
+    const originalHandler = delegation ? delegationFn : handler
+
+    // allow to get the native events from namespaced events ('click.bs.button' --> 'click')
+    let typeEvent = originalTypeEvent.replace(stripNameRegex, '')
+
+    const custom = customEvents[typeEvent]
+    if (custom) {
+      typeEvent = custom
+    }
+
+    const isNative = nativeEvents.indexOf(typeEvent) > -1
+    if (!isNative) {
+      typeEvent = originalTypeEvent
+    }
+
+    const events     = getEvent(element)
+    const handlers   = events[typeEvent] || (events[typeEvent] = {})
+    const previousFn = findHandler(handlers, originalHandler)
+
+    if (previousFn) {
+      previousFn.oneOff = previousFn.oneOff && oneOff
+      return
+    }
+
+    const uid = getUidEvent(originalHandler, originalTypeEvent.replace(namespaceRegex, ''))
+    const fn  = !delegation ? bootstrapHandler(element, handler) : bootstrapDelegationHandler(element, handler, delegationFn)
+
+    fn.isDelegation = delegation
+    fn.originalHandler = originalHandler
+    fn.oneOff = oneOff
+    handlers[uid] = fn
+
+    element.addEventListener(typeEvent, fn, delegation)
+  }
+
   function removeHandler(element, events, typeEvent, handler) {
-    const uidEvent = handler.uidEvent
-    const fn = events[typeEvent][uidEvent]
-    element.removeEventListener(typeEvent, fn, fn.delegation)
+    const fn = findHandler(events[typeEvent], handler)
+    if (fn === null) {
+      return
+    }
+
+    element.removeEventListener(typeEvent, fn, fn.isDelegation)
     delete events[typeEvent][uidEvent]
   }
 
@@ -185,48 +254,12 @@ const EventHandler = (() => {
   }
 
   return {
-    on(element, originalTypeEvent, handler, delegationFn) {
-      if (typeof originalTypeEvent !== 'string' ||
-          (typeof element === 'undefined' || element === null)) {
-        return
-      }
-
-      const delegation      = typeof handler === 'string'
-      const originalHandler = delegation ? delegationFn : handler
-
-      // allow to get the native events from namespaced events ('click.bs.button' --> 'click')
-      let typeEvent = originalTypeEvent.replace(stripNameRegex, '')
-
-      const custom = customEvents[typeEvent]
-      if (custom) {
-        typeEvent = custom
-      }
-
-      const isNative = nativeEvents.indexOf(typeEvent) > -1
-      if (!isNative) {
-        typeEvent = originalTypeEvent
-      }
-      const events    = getEvent(element)
-      const handlers  = events[typeEvent] || (events[typeEvent] = {})
-      const uid       = getUidEvent(originalHandler, originalTypeEvent.replace(namespaceRegex, ''))
-      if (handlers[uid]) {
-        return
-      }
-
-      const fn = !delegation ? bootstrapHandler(element, handler) : bootstrapDelegationHandler(element, handler, delegationFn)
-      fn.isDelegation = delegation
-      handlers[uid] = fn
-      originalHandler.uidEvent = uid
-      fn.originalHandler = originalHandler
-      element.addEventListener(typeEvent, fn, delegation)
+    on(element, event, handler, delegationFn) {
+      addHandler(element, event, handler, delegationFn, false)
     },
 
-    one(element, event, handler) {
-      function complete(e) {
-        EventHandler.off(element, event, complete)
-        handler.apply(element, [e])
-      }
-      EventHandler.on(element, event, complete)
+    one(element, event, handler, delegationFn) {
+      addHandler(element, event, handler, delegationFn, true)
     },
 
     off(element, originalTypeEvent, handler) {
diff --git a/js/src/dom/selectorEngine.js b/js/src/dom/selectorEngine.js
index 99dc26b797..d132d7d245 100644
--- a/js/src/dom/selectorEngine.js
+++ b/js/src/dom/selectorEngine.js
@@ -6,8 +6,6 @@
  */
 
 const SelectorEngine = (() => {
-
-
   /**
    * ------------------------------------------------------------------------
    * Polyfills
diff --git a/js/src/popover.js b/js/src/popover.js
index 98f2f3fbef..e7c00a4cd2 100644
--- a/js/src/popover.js
+++ b/js/src/popover.js
@@ -5,8 +5,10 @@
  * --------------------------------------------------------------------------
  */
 
-import $ from 'jquery'
+import Data from './dom/data'
+import SelectorEngine from './dom/selectorEngine'
 import Tooltip from './tooltip'
+import Util from './util'
 
 /**
  * ------------------------------------------------------------------------
@@ -18,7 +20,6 @@ const NAME                = 'popover'
 const VERSION             = '4.3.1'
 const DATA_KEY            = 'bs.popover'
 const EVENT_KEY           = `.${DATA_KEY}`
-const JQUERY_NO_CONFLICT  = $.fn[NAME]
 const CLASS_PREFIX        = 'bs-popover'
 const BSCLS_PREFIX_REGEX  = new RegExp(`(^|\\s)${CLASS_PREFIX}\\S+`, 'g')
 
@@ -105,26 +106,22 @@ class Popover extends Tooltip {
   }
 
   addAttachmentClass(attachment) {
-    $(this.getTipElement()).addClass(`${CLASS_PREFIX}-${attachment}`)
-  }
-
-  getTipElement() {
-    this.tip = this.tip || $(this.config.template)[0]
-    return this.tip
+    this.getTipElement().classList.add(`${CLASS_PREFIX}-${attachment}`)
   }
 
   setContent() {
-    const $tip = $(this.getTipElement())
+    const tip = this.getTipElement()
 
-    // We use append for html objects to maintain js events
-    this.setElementContent($tip.find(Selector.TITLE), this.getTitle())
+    // we use append for html objects to maintain js events
+    this.setElementContent(SelectorEngine.findOne(Selector.TITLE, tip), this.getTitle())
     let content = this._getContent()
     if (typeof content === 'function') {
       content = content.call(this.element)
     }
-    this.setElementContent($tip.find(Selector.CONTENT), content)
+    this.setElementContent(SelectorEngine.findOne(Selector.CONTENT, tip), content)
 
-    $tip.removeClass(`${ClassName.FADE} ${ClassName.SHOW}`)
+    tip.classList.remove(ClassName.FADE)
+    tip.classList.remove(ClassName.SHOW)
   }
 
   // Private
@@ -135,10 +132,12 @@ class Popover extends Tooltip {
   }
 
   _cleanTipClass() {
-    const $tip = $(this.getTipElement())
-    const tabClass = $tip.attr('class').match(BSCLS_PREFIX_REGEX)
+    const tip = this.getTipElement()
+    const tabClass = tip.getAttribute('class').match(BSCLS_PREFIX_REGEX)
     if (tabClass !== null && tabClass.length > 0) {
-      $tip.removeClass(tabClass.join(''))
+      tabClass.map((token) => token.trim()).forEach((tClass) => {
+        tip.classList.remove(tClass)
+      })
     }
   }
 
@@ -146,7 +145,7 @@ class Popover extends Tooltip {
 
   static _jQueryInterface(config) {
     return this.each(function () {
-      let data = $(this).data(DATA_KEY)
+      let data      = Data.getData(this, DATA_KEY)
       const _config = typeof config === 'object' ? config : null
 
       if (!data && /dispose|hide/.test(config)) {
@@ -155,7 +154,7 @@ class Popover extends Tooltip {
 
       if (!data) {
         data = new Popover(this, _config)
-        $(this).data(DATA_KEY, data)
+        Data.setData(this, DATA_KEY, data)
       }
 
       if (typeof config === 'string') {
@@ -174,11 +173,15 @@ class Popover extends Tooltip {
  * ------------------------------------------------------------------------
  */
 
-$.fn[NAME] = Popover._jQueryInterface
-$.fn[NAME].Constructor = Popover
-$.fn[NAME].noConflict = () => {
-  $.fn[NAME] = JQUERY_NO_CONFLICT
-  return Popover._jQueryInterface
+const $ = Util.jQuery
+if (typeof $ !== 'undefined') {
+  const JQUERY_NO_CONFLICT = $.fn[NAME]
+  $.fn[NAME]               = Popover._jQueryInterface
+  $.fn[NAME].Constructor   = Popover
+  $.fn[NAME].noConflict    = () => {
+    $.fn[NAME] = JQUERY_NO_CONFLICT
+    return Popover._jQueryInterface
+  }
 }
 
 export default Popover
diff --git a/js/src/tooltip.js b/js/src/tooltip.js
index 64dcb08401..d7fe510b11 100644
--- a/js/src/tooltip.js
+++ b/js/src/tooltip.js
@@ -9,8 +9,10 @@ import {
   DefaultWhitelist,
   sanitizeHtml
 } from './tools/sanitizer'
-import $ from 'jquery'
+import Data from './dom/data'
+import EventHandler from './dom/eventHandler'
 import Popper from 'popper.js'
+import SelectorEngine from './dom/selectorEngine'
 import Util from './util'
 
 /**
@@ -23,11 +25,11 @@ const NAME                  = 'tooltip'
 const VERSION               = '4.3.1'
 const DATA_KEY              = 'bs.tooltip'
 const EVENT_KEY             = `.${DATA_KEY}`
-const JQUERY_NO_CONFLICT    = $.fn[NAME]
 const CLASS_PREFIX          = 'bs-tooltip'
 const BSCLS_PREFIX_REGEX    = new RegExp(`(^|\\s)${CLASS_PREFIX}\\S+`, 'g')
 const DISALLOWED_ATTRIBUTES = ['sanitize', 'whiteList', 'sanitizeFn']
 
+
 const DefaultType = {
   animation         : 'boolean',
   template          : 'string',
@@ -193,14 +195,14 @@ class Tooltip {
 
     if (event) {
       const dataKey = this.constructor.DATA_KEY
-      let context = $(event.currentTarget).data(dataKey)
+      let context = Data.getData(event.delegateTarget, dataKey)
 
       if (!context) {
         context = new this.constructor(
           event.currentTarget,
           this._getDelegateConfig()
         )
-        $(event.currentTarget).data(dataKey, context)
+        Data.setData(event.delegateTarget, dataKey, context)
       }
 
       context._activeTrigger.click = !context._activeTrigger.click
@@ -211,7 +213,7 @@ class Tooltip {
         context._leave(null, context)
       }
     } else {
-      if ($(this.getTipElement()).hasClass(ClassName.SHOW)) {
+      if (this.getTipElement().classList.contains(ClassName.SHOW)) {
         this._leave(null, this)
         return
       }
@@ -223,13 +225,13 @@ class Tooltip {
   dispose() {
     clearTimeout(this._timeout)
 
-    $.removeData(this.element, this.constructor.DATA_KEY)
+    Data.removeData(this.element, this.constructor.DATA_KEY)
 
-    $(this.element).off(this.constructor.EVENT_KEY)
-    $(this.element).closest('.modal').off('hide.bs.modal')
+    EventHandler.off(this.element, this.constructor.EVENT_KEY)
+    EventHandler.off(SelectorEngine.closest(this.element, '.modal'), 'hide.bs.modal')
 
     if (this.tip) {
-      $(this.tip).remove()
+      this.tip.parentNode.removeChild(this.tip)
     }
 
     this._isEnabled     = null
@@ -247,21 +249,18 @@ class Tooltip {
   }
 
   show() {
-    if ($(this.element).css('display') === 'none') {
+    if (this.element.style.display === 'none') {
       throw new Error('Please use show on visible elements')
     }
 
-    const showEvent = $.Event(this.constructor.Event.SHOW)
     if (this.isWithContent() && this._isEnabled) {
-      $(this.element).trigger(showEvent)
-
+      const showEvent = EventHandler.trigger(this.element, this.constructor.Event.SHOW)
       const shadowRoot = Util.findShadowRoot(this.element)
-      const isInTheDom = $.contains(
-        shadowRoot !== null ? shadowRoot : this.element.ownerDocument.documentElement,
-        this.element
-      )
+      const isInTheDom = shadowRoot !== null
+        ? shadowRoot.contains(this.element)
+        : this.element.ownerDocument.documentElement.contains(this.element)
 
-      if (showEvent.isDefaultPrevented() || !isInTheDom) {
+      if (showEvent.defaultPrevented || !isInTheDom) {
         return
       }
 
@@ -274,7 +273,7 @@ class Tooltip {
       this.setContent()
 
       if (this.config.animation) {
-        $(tip).addClass(ClassName.FADE)
+        tip.classList.add(ClassName.FADE)
       }
 
       const placement  = typeof this.config.placement === 'function'
@@ -285,13 +284,13 @@ class Tooltip {
       this.addAttachmentClass(attachment)
 
       const container = this._getContainer()
-      $(tip).data(this.constructor.DATA_KEY, this)
+      Data.setData(tip, this.constructor.DATA_KEY, this)
 
-      if (!$.contains(this.element.ownerDocument.documentElement, this.tip)) {
-        $(tip).appendTo(container)
+      if (!this.element.ownerDocument.documentElement.contains(this.tip)) {
+        container.appendChild(tip)
       }
 
-      $(this.element).trigger(this.constructor.Event.INSERTED)
+      EventHandler.trigger(this.element, this.constructor.Event.INSERTED)
 
       this._popper = new Popper(this.element, tip, {
         placement: attachment,
@@ -315,14 +314,16 @@ class Tooltip {
         onUpdate: (data) => this._handlePopperPlacementChange(data)
       })
 
-      $(tip).addClass(ClassName.SHOW)
+      tip.classList.add(ClassName.SHOW)
 
       // If this is a touch-enabled device we add extra
       // empty mouseover listeners to the body's immediate children;
       // only needed because of broken event delegation on iOS
       // https://www.quirksmode.org/blog/archives/2014/02/mouse_event_bub.html
       if ('ontouchstart' in document.documentElement) {
-        $(document.body).children().on('mouseover', null, $.noop)
+        Util.makeArray(document.body.children).forEach((element) => {
+          EventHandler.on(element, 'mouseover', Util.noop)
+        })
       }
 
       const complete = () => {
@@ -332,20 +333,18 @@ class Tooltip {
         const prevHoverState = this._hoverState
         this._hoverState     = null
 
-        $(this.element).trigger(this.constructor.Event.SHOWN)
+        EventHandler.trigger(this.element, this.constructor.Event.SHOWN)
 
         if (prevHoverState === HoverState.OUT) {
           this._leave(null, this)
         }
       }
 
-      if ($(this.tip).hasClass(ClassName.FADE)) {
+      if (this.tip.classList.contains(ClassName.FADE)) {
         const transitionDuration = Util.getTransitionDurationFromElement(this.tip)
 
-        $(this.tip)
-          .one(Util.TRANSITION_END, complete)
-
-        Util.emulateTransitionEnd(tip, transitionDuration)
+        EventHandler.one(this.tip, Util.TRANSITION_END, complete)
+        Util.emulateTransitionEnd(this.tip, transitionDuration)
       } else {
         complete()
       }
@@ -354,15 +353,14 @@ class Tooltip {
 
   hide(callback) {
     const tip       = this.getTipElement()
-    const hideEvent = $.Event(this.constructor.Event.HIDE)
-    const complete = () => {
+    const complete  = () => {
       if (this._hoverState !== HoverState.SHOW && tip.parentNode) {
         tip.parentNode.removeChild(tip)
       }
 
       this._cleanTipClass()
       this.element.removeAttribute('aria-describedby')
-      $(this.element).trigger(this.constructor.Event.HIDDEN)
+      EventHandler.trigger(this.element, this.constructor.Event.HIDDEN)
       if (this._popper !== null) {
         this._popper.destroy()
       }
@@ -372,30 +370,29 @@ class Tooltip {
       }
     }
 
-    $(this.element).trigger(hideEvent)
-
-    if (hideEvent.isDefaultPrevented()) {
+    const hideEvent = EventHandler.trigger(this.element, this.constructor.Event.HIDE)
+    if (hideEvent.defaultPrevented) {
       return
     }
 
-    $(tip).removeClass(ClassName.SHOW)
+    tip.classList.remove(ClassName.SHOW)
 
     // If this is a touch-enabled device we remove the extra
     // empty mouseover listeners we added for iOS support
     if ('ontouchstart' in document.documentElement) {
-      $(document.body).children().off('mouseover', null, $.noop)
+      Util.makeArray(document.body.children)
+        .forEach((element) => EventHandler.off(element, 'mouseover', Util.noop))
     }
 
     this._activeTrigger[Trigger.CLICK] = false
     this._activeTrigger[Trigger.FOCUS] = false
     this._activeTrigger[Trigger.HOVER] = false
 
-    if ($(this.tip).hasClass(ClassName.FADE)) {
+    if (this.tip.classList.contains(ClassName.FADE)) {
       const transitionDuration = Util.getTransitionDurationFromElement(tip)
 
-      $(tip)
-        .one(Util.TRANSITION_END, complete)
-        .emulateTransitionEnd(transitionDuration)
+      EventHandler.one(tip, Util.TRANSITION_END, complete)
+      Util.emulateTransitionEnd(tip, transitionDuration)
     } else {
       complete()
     }
@@ -416,29 +413,46 @@ class Tooltip {
   }
 
   addAttachmentClass(attachment) {
-    $(this.getTipElement()).addClass(`${CLASS_PREFIX}-${attachment}`)
+    this.getTipElement().classList.add(`${CLASS_PREFIX}-${attachment}`)
   }
 
   getTipElement() {
-    this.tip = this.tip || $(this.config.template)[0]
+    if (this.tip) {
+      return this.tip
+    }
+
+    const element = document.createElement('div')
+    element.innerHTML = this.config.template
+
+    this.tip = element.children[0]
     return this.tip
   }
 
   setContent() {
     const tip = this.getTipElement()
-    this.setElementContent($(tip.querySelectorAll(Selector.TOOLTIP_INNER)), this.getTitle())
-    $(tip).removeClass(`${ClassName.FADE} ${ClassName.SHOW}`)
+    this.setElementContent(SelectorEngine.findOne(Selector.TOOLTIP_INNER, tip), this.getTitle())
+    tip.classList.remove(ClassName.FADE)
+    tip.classList.remove(ClassName.SHOW)
   }
 
-  setElementContent($element, content) {
+  setElementContent(element, content) {
+    if (element === null) {
+      return
+    }
+
     if (typeof content === 'object' && (content.nodeType || content.jquery)) {
-      // Content is a DOM node or a jQuery
+      if (content.jquery) {
+        content = content[0]
+      }
+
+      // content is a DOM node or a jQuery
       if (this.config.html) {
-        if (!$(content).parent().is($element)) {
-          $element.empty().append(content)
+        if (content.parentNode !== element) {
+          element.innerHTML = ''
+          element.appendChild(content)
         }
       } else {
-        $element.text($(content).text())
+        element.innerText = content.textContent
       }
 
       return
@@ -449,9 +463,9 @@ class Tooltip {
         content = sanitizeHtml(content, this.config.whiteList, this.config.sanitizeFn)
       }
 
-      $element.html(content)
+      element.innerHTML = content
     } else {
-      $element.text(content)
+      element.innerText = content
     }
   }
 
@@ -494,10 +508,10 @@ class Tooltip {
     }
 
     if (Util.isElement(this.config.container)) {
-      return $(this.config.container)
+      return this.config.container
     }
 
-    return $(document).find(this.config.container)
+    return SelectorEngine.findOne(this.config.container)
   }
 
   _getAttachment(placement) {
@@ -509,7 +523,7 @@ class Tooltip {
 
     triggers.forEach((trigger) => {
       if (trigger === 'click') {
-        $(this.element).on(
+        EventHandler.on(this.element,
           this.constructor.Event.CLICK,
           this.config.selector,
           (event) => this.toggle(event)
@@ -522,21 +536,20 @@ class Tooltip {
           ? this.constructor.Event.MOUSELEAVE
           : this.constructor.Event.FOCUSOUT
 
-        $(this.element)
-          .on(
-            eventIn,
-            this.config.selector,
-            (event) => this._enter(event)
-          )
-          .on(
-            eventOut,
-            this.config.selector,
-            (event) => this._leave(event)
-          )
+        EventHandler.on(this.element,
+          eventIn,
+          this.config.selector,
+          (event) => this._enter(event)
+        )
+        EventHandler.on(this.element,
+          eventOut,
+          this.config.selector,
+          (event) => this._leave(event)
+        )
       }
     })
 
-    $(this.element).closest('.modal').on(
+    EventHandler.on(SelectorEngine.closest(this.element, '.modal'),
       'hide.bs.modal',
       () => {
         if (this.element) {
@@ -571,14 +584,14 @@ class Tooltip {
 
   _enter(event, context) {
     const dataKey = this.constructor.DATA_KEY
-    context = context || $(event.currentTarget).data(dataKey)
+    context = context || Data.getData(event.delegateTarget, dataKey)
 
     if (!context) {
       context = new this.constructor(
-        event.currentTarget,
+        event.delegateTarget,
         this._getDelegateConfig()
       )
-      $(event.currentTarget).data(dataKey, context)
+      Data.setData(event.delegateTarget, dataKey, context)
     }
 
     if (event) {
@@ -587,7 +600,8 @@ class Tooltip {
       ] = true
     }
 
-    if ($(context.getTipElement()).hasClass(ClassName.SHOW) || context._hoverState === HoverState.SHOW) {
+    if (context.getTipElement().classList.contains(ClassName.SHOW) ||
+        context._hoverState === HoverState.SHOW) {
       context._hoverState = HoverState.SHOW
       return
     }
@@ -610,14 +624,14 @@ class Tooltip {
 
   _leave(event, context) {
     const dataKey = this.constructor.DATA_KEY
-    context = context || $(event.currentTarget).data(dataKey)
+    context = context || Data.getData(event.delegateTarget, dataKey)
 
     if (!context) {
       context = new this.constructor(
-        event.currentTarget,
+        event.delegateTarget,
         this._getDelegateConfig()
       )
-      $(event.currentTarget).data(dataKey, context)
+      Data.setData(event.delegateTarget, dataKey, context)
     }
 
     if (event) {
@@ -657,7 +671,7 @@ class Tooltip {
   }
 
   _getConfig(config) {
-    const dataAttributes = $(this.element).data()
+    const dataAttributes = Util.getDataAttributes(this.element)
 
     Object.keys(dataAttributes)
       .forEach((dataAttr) => {
@@ -666,6 +680,11 @@ class Tooltip {
         }
       })
 
+    if (typeof config !== 'undefined' &&
+      typeof config.container === 'object' && config.container.jquery) {
+      config.container = config.container[0]
+    }
+
     config = {
       ...this.constructor.Default,
       ...dataAttributes,
@@ -715,10 +734,12 @@ class Tooltip {
   }
 
   _cleanTipClass() {
-    const $tip = $(this.getTipElement())
-    const tabClass = $tip.attr('class').match(BSCLS_PREFIX_REGEX)
+    const tip = this.getTipElement()
+    const tabClass = tip.getAttribute('class').match(BSCLS_PREFIX_REGEX)
     if (tabClass !== null && tabClass.length) {
-      $tip.removeClass(tabClass.join(''))
+      tabClass
+        .map((token) => token.trim())
+        .forEach((tClass) => tip.classList.remove(tClass))
     }
   }
 
@@ -737,7 +758,7 @@ class Tooltip {
       return
     }
 
-    $(tip).removeClass(ClassName.FADE)
+    tip.classList.remove(ClassName.FADE)
     this.config.animation = false
     this.hide()
     this.show()
@@ -748,7 +769,7 @@ class Tooltip {
 
   static _jQueryInterface(config) {
     return this.each(function () {
-      let data = $(this).data(DATA_KEY)
+      let data      = Data.getData(this, DATA_KEY)
       const _config = typeof config === 'object' && config
 
       if (!data && /dispose|hide/.test(config)) {
@@ -757,7 +778,7 @@ class Tooltip {
 
       if (!data) {
         data = new Tooltip(this, _config)
-        $(this).data(DATA_KEY, data)
+        Data.setData(this, DATA_KEY, data)
       }
 
       if (typeof config === 'string') {
@@ -775,12 +796,15 @@ class Tooltip {
  * jQuery
  * ------------------------------------------------------------------------
  */
-
-$.fn[NAME] = Tooltip._jQueryInterface
-$.fn[NAME].Constructor = Tooltip
-$.fn[NAME].noConflict = () => {
-  $.fn[NAME] = JQUERY_NO_CONFLICT
-  return Tooltip._jQueryInterface
+const $ = Util.jQuery
+if (typeof $ !== 'undefined') {
+  const JQUERY_NO_CONFLICT  = $.fn[NAME]
+  $.fn[NAME]                = Tooltip._jQueryInterface
+  $.fn[NAME].Constructor    = Tooltip
+  $.fn[NAME].noConflict     = () => {
+    $.fn[NAME] = JQUERY_NO_CONFLICT
+    return Tooltip._jQueryInterface
+  }
 }
 
 export default Tooltip
diff --git a/js/src/util.js b/js/src/util.js
index e0a81b9ec8..de4f061bbe 100644
--- a/js/src/util.js
+++ b/js/src/util.js
@@ -235,6 +235,10 @@ const Util = {
     return Util.findShadowRoot(element.parentNode)
   },
 
+  // eslint-disable-next-line no-empty-function
+  noop() {
+  },
+
   get jQuery() {
     return window.$ || window.jQuery
   }
diff --git a/js/tests/unit/dom/eventHandler.js b/js/tests/unit/dom/eventHandler.js
index 0730e38468..7404d6c32b 100644
--- a/js/tests/unit/dom/eventHandler.js
+++ b/js/tests/unit/dom/eventHandler.js
@@ -254,4 +254,18 @@ $(function () {
     EventHandler.trigger(element, 'click')
     document.body.removeChild(element)
   })
+
+  QUnit.test('off should remove a listener registered by .one', function (assert) {
+    assert.expect(0)
+
+    var element = document.createElement('div')
+    var handler = function () {
+      assert.notOk(true, 'listener called')
+    }
+
+    EventHandler.one(element, 'foobar', handler)
+    EventHandler.off(element, 'foobar', handler)
+
+    EventHandler.trigger(element, 'foobar')
+  })
 })
diff --git a/js/tests/unit/popover.js b/js/tests/unit/popover.js
index f4b04ded57..593391c547 100644
--- a/js/tests/unit/popover.js
+++ b/js/tests/unit/popover.js
@@ -65,7 +65,7 @@ $(function () {
     assert.expect(1)
     var $popover = $('<a href="#" title="mdo" data-content="https://twitter.com/mdo">@mdo</a>').bootstrapPopover()
 
-    assert.ok($popover.data('bs.popover'), 'popover instance exists')
+    assert.ok(Data.getData($popover[0], 'bs.popover'), 'popover instance exists')
   })
 
   QUnit.test('should store popover trigger in popover instance data object', function (assert) {
@@ -76,7 +76,7 @@ $(function () {
 
     $popover.bootstrapPopover('show')
 
-    assert.ok($('.popover').data('bs.popover'), 'popover trigger stored in instance data')
+    assert.ok(Data.getData($('.popover')[0], 'bs.popover'), 'popover trigger stored in instance data')
   })
 
   QUnit.test('should get title and content from options', function (assert) {
@@ -252,24 +252,20 @@ $(function () {
   })
 
   QUnit.test('should destroy popover', function (assert) {
-    assert.expect(7)
+    assert.expect(3)
     var $popover = $('<div/>')
       .bootstrapPopover({
         trigger: 'hover'
       })
       .on('click.foo', $.noop)
 
-    assert.ok($popover.data('bs.popover'), 'popover has data')
-    assert.ok($._data($popover[0], 'events').mouseover && $._data($popover[0], 'events').mouseout, 'popover has hover event')
-    assert.strictEqual($._data($popover[0], 'events').click[0].namespace, 'foo', 'popover has extra click.foo event')
+    assert.ok(Data.getData($popover[0], 'bs.popover'), 'popover has data')
 
     $popover.bootstrapPopover('show')
     $popover.bootstrapPopover('dispose')
 
     assert.ok(!$popover.hasClass('show'), 'popover is hidden')
     assert.ok(!$popover.data('popover'), 'popover does not have data')
-    assert.strictEqual($._data($popover[0], 'events').click[0].namespace, 'foo', 'popover still has click.foo')
-    assert.ok(!$._data($popover[0], 'events').mouseover && !$._data($popover[0], 'events').mouseout, 'popover does not have any events')
   })
 
   QUnit.test('should render popover element using delegated selector', function (assert) {
@@ -342,7 +338,7 @@ $(function () {
         assert.ok(false, 'should not fire any popover events')
       })
       .bootstrapPopover('hide')
-    assert.strictEqual(typeof $popover.data('bs.popover'), 'undefined', 'should not initialize the popover')
+    assert.ok(Data.getData($popover[0], 'bs.popover') === null, 'should not initialize the popover')
   })
 
   QUnit.test('should fire inserted event', function (assert) {
@@ -440,11 +436,11 @@ $(function () {
       })
 
     $popover.bootstrapPopover('disable')
-    $popover.trigger($.Event('click'))
+    EventHandler.trigger($popover[0], 'click')
     setTimeout(function () {
       assert.strictEqual($('.popover').length === 0, true)
       $popover.bootstrapPopover('enable')
-      $popover.trigger($.Event('click'))
+      EventHandler.trigger($popover[0], 'click')
     }, 200)
   })
 
diff --git a/js/tests/unit/tooltip.js b/js/tests/unit/tooltip.js
index e66450fb85..d2729fa64f 100644
--- a/js/tests/unit/tooltip.js
+++ b/js/tests/unit/tooltip.js
@@ -118,13 +118,11 @@ $(function () {
 
     $tooltip
       .one('shown.bs.tooltip', function () {
-        assert.ok($('.tooltip')
-          .is('.fade.bs-tooltip-bottom.show'), 'has correct classes applied')
-
+        assert.ok($('.tooltip').is('.fade.bs-tooltip-bottom.show'), 'has correct classes applied')
         $tooltip.bootstrapTooltip('hide')
       })
       .one('hidden.bs.tooltip', function () {
-        assert.strictEqual($tooltip.data('bs.tooltip').tip.parentNode, null, 'tooltip removed')
+        assert.strictEqual(Data.getData($tooltip[0], 'bs.tooltip').tip.parentNode, null, 'tooltip removed')
         done()
       })
       .bootstrapTooltip('show')
@@ -145,7 +143,7 @@ $(function () {
         $tooltip.bootstrapTooltip('hide')
       })
       .one('hidden.bs.tooltip', function () {
-        assert.strictEqual($tooltip.data('bs.tooltip').tip.parentNode, null, 'tooltip removed')
+        assert.strictEqual(Data.getData($tooltip[0], 'bs.tooltip').tip.parentNode, null, 'tooltip removed')
         done()
       })
       .bootstrapTooltip('show')
@@ -207,7 +205,7 @@ $(function () {
         $tooltip.bootstrapTooltip('hide')
       })
       .one('hidden.bs.tooltip', function () {
-        assert.strictEqual($tooltip.data('bs.tooltip').tip.parentNode, null, 'tooltip removed')
+        assert.strictEqual(Data.getData($tooltip[0], 'bs.tooltip').tip.parentNode, null, 'tooltip removed')
         done()
       })
       .bootstrapTooltip('show')
@@ -333,22 +331,18 @@ $(function () {
   })
 
   QUnit.test('should destroy tooltip', function (assert) {
-    assert.expect(7)
+    assert.expect(3)
     var $tooltip = $('<div/>')
       .bootstrapTooltip()
       .on('click.foo', function () {})  // eslint-disable-line no-empty-function
 
-    assert.ok($tooltip.data('bs.tooltip'), 'tooltip has data')
-    assert.ok($._data($tooltip[0], 'events').mouseover && $._data($tooltip[0], 'events').mouseout, 'tooltip has hover events')
-    assert.strictEqual($._data($tooltip[0], 'events').click[0].namespace, 'foo', 'tooltip has extra click.foo event')
+    assert.ok(Data.getData($tooltip[0], 'bs.tooltip'), 'tooltip has data')
 
     $tooltip.bootstrapTooltip('show')
     $tooltip.bootstrapTooltip('dispose')
 
     assert.ok(!$tooltip.hasClass('show'), 'tooltip is hidden')
-    assert.ok(!$._data($tooltip[0], 'bs.tooltip'), 'tooltip does not have data')
-    assert.strictEqual($._data($tooltip[0], 'events').click[0].namespace, 'foo', 'tooltip still has click.foo')
-    assert.ok(!$._data($tooltip[0], 'events').mouseover && !$._data($tooltip[0], 'events').mouseout, 'tooltip does not have hover events')
+    assert.ok(!Data.getData($tooltip[0], 'bs.tooltip'), 'tooltip does not have data')
   })
 
   // QUnit.test('should show tooltip with delegate selector on click', function (assert) {
@@ -477,7 +471,7 @@ $(function () {
         trigger: 'manual'
       })
       .on('inserted.bs.tooltip', function () {
-        var $tooltip = $($(this).data('bs.tooltip').tip)
+        var $tooltip = $(Data.getData(this, 'bs.tooltip').tip)
         assert.ok($tooltip.hasClass('bs-tooltip-right'))
         assert.ok(typeof $tooltip.attr('style') === 'undefined')
         $styles.remove()
@@ -587,7 +581,7 @@ $(function () {
       done()
     }, 200)
 
-    $tooltip.trigger('mouseenter')
+    EventHandler.trigger($tooltip[0], 'mouseover')
   })
 
   QUnit.test('should not show tooltip if leave event occurs before delay expires', function (assert) {
@@ -602,7 +596,7 @@ $(function () {
 
     setTimeout(function () {
       assert.ok(!$('.tooltip').is('.fade.show'), '100ms: tooltip not faded active')
-      $tooltip.trigger('mouseout')
+      EventHandler.trigger($tooltip[0], 'mouseout')
     }, 100)
 
     setTimeout(function () {
@@ -610,7 +604,7 @@ $(function () {
       done()
     }, 200)
 
-    $tooltip.trigger('mouseenter')
+    EventHandler.trigger($tooltip[0], 'mouseover')
   })
 
   QUnit.test('should not hide tooltip if leave event occurs and enter event occurs within the hide delay', function (assert) {
@@ -628,11 +622,11 @@ $(function () {
 
     setTimeout(function () {
       assert.ok($('.tooltip').is('.fade.show'), '1ms: tooltip faded active')
-      $tooltip.trigger('mouseout')
+      EventHandler.trigger($tooltip[0], 'mouseout')
 
       setTimeout(function () {
         assert.ok($('.tooltip').is('.fade.show'), '100ms: tooltip still faded active')
-        $tooltip.trigger('mouseenter')
+        EventHandler.trigger($tooltip[0], 'mouseover')
       }, 100)
 
       setTimeout(function () {
@@ -641,7 +635,7 @@ $(function () {
       }, 200)
     }, 0)
 
-    $tooltip.trigger('mouseenter')
+    EventHandler.trigger($tooltip[0], 'mouseover')
   })
 
   QUnit.test('should not show tooltip if leave event occurs before delay expires', function (assert) {
@@ -656,7 +650,7 @@ $(function () {
 
     setTimeout(function () {
       assert.ok(!$('.tooltip').is('.fade.show'), '100ms: tooltip not faded active')
-      $tooltip.trigger('mouseout')
+      EventHandler.trigger($tooltip[0], 'mouseout')
     }, 100)
 
     setTimeout(function () {
@@ -664,7 +658,7 @@ $(function () {
       done()
     }, 200)
 
-    $tooltip.trigger('mouseenter')
+    EventHandler.trigger($tooltip[0], 'mouseover')
   })
 
   QUnit.test('should not show tooltip if leave event occurs before delay expires, even if hide delay is 0', function (assert) {
@@ -682,7 +676,7 @@ $(function () {
 
     setTimeout(function () {
       assert.ok(!$('.tooltip').is('.fade.show'), '100ms: tooltip not faded active')
-      $tooltip.trigger('mouseout')
+      EventHandler.trigger($tooltip[0], 'mouseout')
     }, 100)
 
     setTimeout(function () {
@@ -690,7 +684,7 @@ $(function () {
       done()
     }, 250)
 
-    $tooltip.trigger('mouseenter')
+    EventHandler.trigger($tooltip[0], 'mouseover')
   })
 
   QUnit.test('should wait 200ms before hiding the tooltip', function (assert) {
@@ -707,21 +701,21 @@ $(function () {
       })
 
     setTimeout(function () {
-      assert.ok($($tooltip.data('bs.tooltip').tip).is('.fade.show'), '1ms: tooltip faded active')
+      assert.ok($(Data.getData($tooltip[0], 'bs.tooltip').tip).is('.fade.show'), '1ms: tooltip faded active')
 
-      $tooltip.trigger('mouseout')
+      EventHandler.trigger($tooltip[0], 'mouseout')
 
       setTimeout(function () {
-        assert.ok($($tooltip.data('bs.tooltip').tip).is('.fade.show'), '100ms: tooltip still faded active')
+        assert.ok($(Data.getData($tooltip[0], 'bs.tooltip').tip).is('.fade.show'), '100ms: tooltip still faded active')
       }, 100)
 
       setTimeout(function () {
-        assert.ok(!$($tooltip.data('bs.tooltip').tip).is('.show'), '200ms: tooltip removed')
+        assert.ok(!$(Data.getData($tooltip[0], 'bs.tooltip').tip).is('.show'), '200ms: tooltip removed')
         done()
       }, 200)
     }, 0)
 
-    $tooltip.trigger('mouseenter')
+    EventHandler.trigger($tooltip[0], 'mouseover')
   })
 
   QUnit.test('should not reload the tooltip on subsequent mouseenter events', function (assert) {
@@ -746,11 +740,11 @@ $(function () {
       title: titleHtml
     })
 
-    $('#tt-outer').trigger('mouseenter')
+    EventHandler.trigger($('#tt-outer')[0], 'mouseover')
 
     var currentUid = $('#tt-content').text()
 
-    $('#tt-content').trigger('mouseenter')
+    EventHandler.trigger($('#tt-outer')[0], 'mouseover')
     assert.strictEqual(currentUid, $('#tt-content').text())
   })
 
@@ -776,18 +770,18 @@ $(function () {
       title: titleHtml
     })
 
-    var obj = $tooltip.data('bs.tooltip')
+    var obj = Data.getData($tooltip[0], 'bs.tooltip')
 
-    $('#tt-outer').trigger('mouseenter')
+    EventHandler.trigger($('#tt-outer')[0], 'mouseover')
 
     var currentUid = $('#tt-content').text()
 
-    $('#tt-outer').trigger('mouseleave')
+    EventHandler.trigger($('#tt-outer')[0], 'mouseout')
     assert.strictEqual(currentUid, $('#tt-content').text())
 
     assert.ok(obj._hoverState === 'out', 'the tooltip hoverState should be set to "out"')
 
-    $('#tt-outer').trigger('mouseenter')
+    EventHandler.trigger($('#tt-outer')[0], 'mouseover')
     assert.ok(obj._hoverState === 'show', 'the tooltip hoverState should be set to "show"')
 
     assert.strictEqual(currentUid, $('#tt-content').text())
@@ -802,7 +796,7 @@ $(function () {
         assert.ok(false, 'should not fire any tooltip events')
       })
       .bootstrapTooltip('hide')
-    assert.strictEqual(typeof $tooltip.data('bs.tooltip'), 'undefined', 'should not initialize the tooltip')
+    assert.ok(Data.getData($tooltip[0], 'bs.tooltip') === null, 'should not initialize the tooltip')
   })
 
   QUnit.test('should not remove tooltip if multiple triggers are set and one is still active', function (assert) {
@@ -813,7 +807,8 @@ $(function () {
         trigger: 'click hover focus',
         animation: false
       })
-    var tooltip = $el.data('bs.tooltip')
+
+    var tooltip = Data.getData($el[0], 'bs.tooltip')
     var $tooltip = $(tooltip.getTipElement())
 
     function showingTooltip() {
@@ -821,28 +816,28 @@ $(function () {
     }
 
     var tests = [
-      ['mouseenter', 'mouseleave'],
+      ['mouseover', 'mouseout'],
 
       ['focusin', 'focusout'],
 
       ['click', 'click'],
 
-      ['mouseenter', 'focusin', 'focusout', 'mouseleave'],
-      ['mouseenter', 'focusin', 'mouseleave', 'focusout'],
+      ['mouseover', 'focusin', 'focusout', 'mouseout'],
+      ['mouseover', 'focusin', 'mouseout', 'focusout'],
 
-      ['focusin', 'mouseenter', 'mouseleave', 'focusout'],
-      ['focusin', 'mouseenter', 'focusout', 'mouseleave'],
+      ['focusin', 'mouseover', 'mouseout', 'focusout'],
+      ['focusin', 'mouseover', 'focusout', 'mouseout'],
 
-      ['click', 'focusin', 'mouseenter', 'focusout', 'mouseleave', 'click'],
-      ['mouseenter', 'click', 'focusin', 'focusout', 'mouseleave', 'click'],
-      ['mouseenter', 'focusin', 'click', 'click', 'mouseleave', 'focusout']
+      ['click', 'focusin', 'mouseover', 'focusout', 'mouseout', 'click'],
+      ['mouseover', 'click', 'focusin', 'focusout', 'mouseout', 'click'],
+      ['mouseover', 'focusin', 'click', 'click', 'mouseout', 'focusout']
     ]
 
     assert.ok(!showingTooltip())
 
     $.each(tests, function (idx, triggers) {
       for (var i = 0, len = triggers.length; i < len; i++) {
-        $el.trigger(triggers[i])
+        EventHandler.trigger($el[0], triggers[i])
         assert.equal(i < len - 1, showingTooltip())
       }
     })
@@ -857,20 +852,20 @@ $(function () {
         animation: false
       })
 
-    var tooltip = $el.data('bs.tooltip')
+    var tooltip = Data.getData($el[0], 'bs.tooltip')
     var $tooltip = $(tooltip.getTipElement())
 
     function showingTooltip() {
       return $tooltip.hasClass('show') || tooltip._hoverState === 'show'
     }
 
-    $el.trigger('click')
+    EventHandler.trigger($el[0], 'click')
     assert.ok(showingTooltip(), 'tooltip is faded in')
 
     $el.bootstrapTooltip('hide')
     assert.ok(!showingTooltip(), 'tooltip was faded out')
 
-    $el.trigger('click')
+    EventHandler.trigger($el[0], 'click')
     assert.ok(showingTooltip(), 'tooltip is faded in again')
   })
 
@@ -952,7 +947,7 @@ $(function () {
       .appendTo('#qunit-fixture')
       .bootstrapTooltip('show')
       .on('hidden.bs.tooltip', function () {
-        var tooltip = $el.data('bs.tooltip')
+        var tooltip = Data.getData($el[0], 'bs.tooltip')
         var $tooltip = $(tooltip.getTipElement())
         assert.ok($tooltip.hasClass('tooltip'))
         assert.ok($tooltip.hasClass('fade'))
@@ -968,7 +963,7 @@ $(function () {
     var $el = $('<a href="#" rel="tooltip" title="7"/>')
       .appendTo('#qunit-fixture')
       .on('shown.bs.tooltip', function () {
-        var tooltip = $el.data('bs.tooltip')
+        var tooltip = Data.getData($el[0], 'bs.tooltip')
         var $tooltip = $(tooltip.getTipElement())
         assert.strictEqual($tooltip.children().text(), '7')
         done()
@@ -990,11 +985,11 @@ $(function () {
       })
 
     $trigger.bootstrapTooltip('disable')
-    $trigger.trigger($.Event('click'))
+    EventHandler.trigger($trigger[0], 'click')
     setTimeout(function () {
       assert.strictEqual($('.tooltip').length === 0, true)
       $trigger.bootstrapTooltip('enable')
-      $trigger.trigger($.Event('click'))
+      EventHandler.trigger($trigger[0], 'click')
     }, 200)
   })
 
diff --git a/js/tests/visual/popover.html b/js/tests/visual/popover.html
index e422891dad..0acc6c1c06 100644
--- a/js/tests/visual/popover.html
+++ b/js/tests/visual/popover.html
@@ -33,7 +33,9 @@
 
     <script src="../../../node_modules/jquery/dist/jquery.slim.min.js"></script>
     <script src="../../../node_modules/popper.js/dist/umd/popper.min.js"></script>
+    <script src="../../dist/dom/data.js"></script>
     <script src="../../dist/dom/eventHandler.js"></script>
+    <script src="../../dist/dom/selectorEngine.js"></script>
     <script src="../../dist/util.js"></script>
     <script src="../../dist/tooltip.js"></script>
     <script src="../../dist/popover.js"></script>
diff --git a/js/tests/visual/tooltip.html b/js/tests/visual/tooltip.html
index c340ae274d..601732a38f 100644
--- a/js/tests/visual/tooltip.html
+++ b/js/tests/visual/tooltip.html
@@ -73,6 +73,9 @@
 
     <script src="../../../node_modules/jquery/dist/jquery.slim.min.js"></script>
     <script src="../../../node_modules/popper.js/dist/umd/popper.min.js"></script>
+    <script src="../../dist/dom/data.js"></script>
+    <script src="../../dist/dom/eventHandler.js"></script>
+    <script src="../../dist/dom/selectorEngine.js"></script>
     <script src="../../dist/util.js"></script>
     <script src="../../dist/tooltip.js"></script>
     <script>
-- 
GitLab