From cc6e130fc1c6f794fcdb24737cb584ac2c937d33 Mon Sep 17 00:00:00 2001 From: Alessandro Chitolina 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 = $('@mdo').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 = $('
') .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 = $('
') .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 = $('') .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 @@ + + 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 @@ + + +