From f7b6b8390e627661a5b94198e4750bb39bc19188 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/eventHandler.js | 142 ++++++++++++------- js/src/popover.js | 58 ++++---- js/src/tooltip.js | 223 ++++++++++++++++-------------- js/src/util.js | 4 + js/tests/unit/dom/eventHandler.js | 14 ++ js/tests/unit/popover.js | 22 ++- js/tests/unit/tooltip.js | 99 +++++++------ js/tests/visual/popover.html | 2 + js/tests/visual/tooltip.html | 3 + 9 files changed, 319 insertions(+), 248 deletions(-) diff --git a/js/src/dom/eventHandler.js b/js/src/dom/eventHandler.js index 79b0878675a6..2533a547c52b 100644 --- a/js/src/dom/eventHandler.js +++ b/js/src/dom/eventHandler.js @@ -9,14 +9,12 @@ import Util from '../util' const EventHandler = (() => { - /** * ------------------------------------------------------------------------ * Polyfills * ------------------------------------------------------------------------ */ - // defaultPrevented is broken in IE. // https://connect.microsoft.com/IE/feedback/details/790389/event-defaultprevented-returns-false-after-preventdefault-was-called const workingDefaultPrevented = (() => { @@ -128,51 +126,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] } @@ -190,48 +262,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/popover.js b/js/src/popover.js index aeccdf7501ab..7a297eefc626 100644 --- a/js/src/popover.js +++ b/js/src/popover.js @@ -1,6 +1,7 @@ -import $ from 'jquery' +import Data from './dom/data' +import SelectorEngine from './dom/selectorEngine' import Tooltip from './tooltip' - +import Util from './util' /** * -------------------------------------------------------------------------- @@ -22,11 +23,10 @@ const Popover = (() => { const VERSION = '4.0.0-beta' 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') - const Default = $.extend({}, Tooltip.Default, { + const Default = Util.extend({}, Tooltip.Default, { placement : 'right', trigger : 'click', content : '', @@ -36,7 +36,7 @@ const Popover = (() => { + '
' }) - const DefaultType = $.extend({}, Tooltip.DefaultType, { + const DefaultType = Util.extend({}, Tooltip.DefaultType, { content : '(string|element|function)' }) @@ -111,22 +111,18 @@ const Popover = (() => { } 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()) - this.setElementContent($tip.find(Selector.CONTENT), this._getContent()) + this.setElementContent(SelectorEngine.findOne(Selector.TITLE, tip), this.getTitle()) + this.setElementContent(SelectorEngine.findOne(Selector.CONTENT, tip), this._getContent()) - $tip.removeClass(`${ClassName.FADE} ${ClassName.SHOW}`) + tip.classList.remove(ClassName.FADE) + tip.classList.remove(ClassName.SHOW) } // private @@ -139,20 +135,21 @@ const Popover = (() => { } _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) + }) } } - // static static _jQueryInterface(config) { return this.each(function () { - let data = $(this).data(DATA_KEY) - const _config = typeof config === 'object' ? config : null + let data = Data.getData(this, DATA_KEY) + const _config = typeof config === 'object' && config if (!data && /destroy|hide/.test(config)) { return @@ -160,7 +157,7 @@ const Popover = (() => { if (!data) { data = new Popover(this, _config) - $(this).data(DATA_KEY, data) + Data.setData(this, DATA_KEY, data) } if (typeof config === 'string') { @@ -180,15 +177,20 @@ const Popover = (() => { * ------------------------------------------------------------------------ */ - $.fn[NAME] = Popover._jQueryInterface - $.fn[NAME].Constructor = Popover - $.fn[NAME].noConflict = function () { - $.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 = function () { + $.fn[NAME] = JQUERY_NO_CONFLICT + return Popover._jQueryInterface + } } return Popover -})(jQuery) +})() export default Popover diff --git a/js/src/tooltip.js b/js/src/tooltip.js index cf89116a1265..7ee1e2bfc057 100644 --- a/js/src/tooltip.js +++ b/js/src/tooltip.js @@ -1,5 +1,7 @@ -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' @@ -31,10 +33,9 @@ const Tooltip = (() => { const VERSION = '4.0.0-beta' const DATA_KEY = 'bs.tooltip' const EVENT_KEY = `.${DATA_KEY}` - const JQUERY_NO_CONFLICT = $.fn[NAME] const TRANSITION_DURATION = 150 const CLASS_PREFIX = 'bs-tooltip' - const BSCLS_PREFIX_REGEX = new RegExp(`(^|\\s)${CLASS_PREFIX}\\S+`, 'g') + const BSCLS_PREFIX_REGEX = new RegExp(`(^|\\s)${CLASS_PREFIX}\\S+`, 'g') const DefaultType = { animation : 'boolean', @@ -134,7 +135,6 @@ const Tooltip = (() => { this.tip = null this._setListeners() - } @@ -190,14 +190,14 @@ const 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, + event.delegateTarget, this._getDelegateConfig() ) - $(event.currentTarget).data(dataKey, context) + Data.setData(event.delegateTarget, dataKey, context) } context._activeTrigger.click = !context._activeTrigger.click @@ -207,10 +207,8 @@ const Tooltip = (() => { } else { context._leave(null, context) } - } else { - - if ($(this.getTipElement()).hasClass(ClassName.SHOW)) { + if (this.getTipElement().classList.contains(ClassName.SHOW)) { this._leave(null, this) return } @@ -222,13 +220,13 @@ const 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 @@ -246,20 +244,15 @@ const 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 isInTheDom = $.contains( - this.element.ownerDocument.documentElement, - this.element - ) + const showEvent = EventHandler.trigger(this.element, this.constructor.Event.SHOW) + const isInTheDom = this.element.ownerDocument.documentElement.contains(this.element) - if (showEvent.isDefaultPrevented() || !isInTheDom) { + if (showEvent.defaultPrevented || !isInTheDom) { return } @@ -272,7 +265,7 @@ const Tooltip = (() => { this.setContent() if (this.config.animation) { - $(tip).addClass(ClassName.FADE) + tip.classList.add(ClassName.FADE) } const placement = typeof this.config.placement === 'function' ? @@ -282,15 +275,18 @@ const Tooltip = (() => { const attachment = this._getAttachment(placement) this.addAttachmentClass(attachment) - const container = this.config.container === false ? document.body : $(this.config.container) + let container = this.config.container || document.body + if (typeof container === 'string') { + container = SelectorEngine.findOne(this.config.container) + } - $(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 +311,16 @@ const Tooltip = (() => { } }) - $(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) { - $('body').children().on('mouseover', null, $.noop) + Util.makeArray(document.body.children).forEach((element) => { + EventHandler.on(element, 'mouseover', Util.noop) + }) } const complete = () => { @@ -332,18 +330,16 @@ const 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 (Util.supportsTransitionEnd() && $(this.tip).hasClass(ClassName.FADE)) { - $(this.tip) - .one(Util.TRANSITION_END, complete) - - Util.emulateTransitionEnd(this.tip, Tooltip._TRANSITION_DURATION) + if (Util.supportsTransitionEnd() && this.tip.classList.contains(ClassName.FADE)) { + EventHandler.one(this.tip, Util.TRANSITION_END, complete) + Util.emulateTransitionEnd(this.tip, TRANSITION_DURATION) } else { complete() } @@ -352,7 +348,6 @@ const Tooltip = (() => { hide(callback) { const tip = this.getTipElement() - const hideEvent = $.Event(this.constructor.Event.HIDE) const complete = () => { if (this._hoverState !== HoverState.SHOW && tip.parentNode) { tip.parentNode.removeChild(tip) @@ -360,7 +355,7 @@ const Tooltip = (() => { 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() } @@ -370,18 +365,19 @@ const 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) { - $('body').children().off('mouseover', null, $.noop) + Util.makeArray(document.body.children).forEach((element) => { + EventHandler.off(element, 'mouseover', Util.noop) + }) } this._activeTrigger[Trigger.CLICK] = false @@ -389,10 +385,8 @@ const Tooltip = (() => { this._activeTrigger[Trigger.HOVER] = false if (Util.supportsTransitionEnd() && - $(this.tip).hasClass(ClassName.FADE)) { - $(tip) - .one(Util.TRANSITION_END, complete) - + this.tip.classList.contains(ClassName.FADE)) { + EventHandler.one(tip, Util.TRANSITION_END, complete) Util.emulateTransitionEnd(tip, TRANSITION_DURATION) } else { complete() @@ -414,33 +408,50 @@ const 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.find(Selector.TOOLTIP_INNER), this.getTitle()) - $tip.removeClass(`${ClassName.FADE} ${ClassName.SHOW}`) + const tip = this.getTipElement() + 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 + } + const html = this.config.html if (typeof content === 'object' && (content.nodeType || content.jquery)) { + if (content.jquery) { + content = content[0] + } + // content is a DOM node or a jQuery if (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 } } else { - $element[html ? 'html' : 'text'](content) + element[html ? 'innerHTML' : 'innerText'] = content } } @@ -468,12 +479,11 @@ const 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) ) - } else if (trigger !== Trigger.MANUAL) { const eventIn = trigger === Trigger.HOVER ? this.constructor.Event.MOUSEENTER : @@ -482,29 +492,28 @@ const 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( - 'hide.bs.modal', - () => this.hide() - ) }) + EventHandler.on(SelectorEngine.closest(this.element, '.modal'), + 'hide.bs.modal', + () => this.hide() + ) + if (this.config.selector) { - this.config = $.extend({}, this.config, { - trigger : 'manual', - selector : '' + Util.extend(this.config, { + trigger : 'manual', + selector: '' }) } else { this._fixTitle() @@ -513,8 +522,7 @@ const Tooltip = (() => { _fixTitle() { const titleType = typeof this.element.getAttribute('data-original-title') - if (this.element.getAttribute('title') || - titleType !== 'string') { + if (this.element.getAttribute('title') || titleType !== 'string') { this.element.setAttribute( 'data-original-title', this.element.getAttribute('title') || '' @@ -525,15 +533,14 @@ const 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) { @@ -542,7 +549,7 @@ const Tooltip = (() => { ] = true } - if ($(context.getTipElement()).hasClass(ClassName.SHOW) || + if (context.getTipElement().classList.contains(ClassName.SHOW) || context._hoverState === HoverState.SHOW) { context._hoverState = HoverState.SHOW return @@ -567,14 +574,14 @@ const 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) { @@ -614,10 +621,15 @@ const Tooltip = (() => { } _getConfig(config) { - config = $.extend( + if (typeof config !== 'undefined' && + typeof config.container === 'object' && config.container.jquery) { + config.container = config.container[0] + } + + config = Util.extend( {}, this.constructor.Default, - $(this.element).data(), + Util.getDataAttributes(this.element), config ) @@ -660,10 +672,12 @@ const 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) + }) } } @@ -678,7 +692,7 @@ const Tooltip = (() => { if (tip.getAttribute('x-placement') !== null) { return } - $(tip).removeClass(ClassName.FADE) + tip.classList.remove(ClassName.FADE) this.config.animation = false this.hide() this.show() @@ -689,7 +703,7 @@ const 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)) { @@ -698,7 +712,7 @@ const Tooltip = (() => { if (!data) { data = new Tooltip(this, _config) - $(this).data(DATA_KEY, data) + Data.setData(this, DATA_KEY, data) } if (typeof config === 'string') { @@ -718,11 +732,16 @@ const Tooltip = (() => { * ------------------------------------------------------------------------ */ - $.fn[NAME] = Tooltip._jQueryInterface - $.fn[NAME].Constructor = Tooltip - $.fn[NAME].noConflict = function () { - $.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 = function () { + $.fn[NAME] = JQUERY_NO_CONFLICT + return Tooltip._jQueryInterface + } } return Tooltip diff --git a/js/src/util.js b/js/src/util.js index cbeee7761d97..bfbccffa2946 100644 --- a/js/src/util.js +++ b/js/src/util.js @@ -189,6 +189,10 @@ const Util = (() => { return false }, + // 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 0730e38468bb..7404d6c32b4a 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 6c0f99cba2c8..92a649ecdee2 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) { @@ -220,24 +220,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) { @@ -249,10 +245,10 @@ $(function () { trigger: 'click' }) - $div.find('a').trigger('click') + EventHandler.trigger($div.find('a')[0], 'click') assert.notEqual($('.popover').length, 0, 'popover was inserted') - $div.find('a').trigger('click') + EventHandler.trigger($div.find('a')[0], 'click') assert.strictEqual($('.popover').length, 0, 'popover was removed') }) @@ -304,7 +300,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) { @@ -403,11 +399,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 e4e6bdd6cc8b..09f8a76b1e0c 100644 --- a/js/tests/unit/tooltip.js +++ b/js/tests/unit/tooltip.js @@ -115,7 +115,7 @@ $(function () { $tooltip.bootstrapTooltip('hide') - assert.strictEqual($tooltip.data('bs.tooltip').tip.parentNode, null, 'tooltip removed') + assert.strictEqual(Data.getData($tooltip[0], 'bs.tooltip').tip.parentNode, null, 'tooltip removed') }) QUnit.test('should allow html entities', function (assert) { @@ -128,7 +128,7 @@ $(function () { assert.notEqual($('.tooltip b').length, 0, 'b tag was inserted') $tooltip.bootstrapTooltip('hide') - assert.strictEqual($tooltip.data('bs.tooltip').tip.parentNode, null, 'tooltip removed') + assert.strictEqual(Data.getData($tooltip[0], 'bs.tooltip').tip.parentNode, null, 'tooltip removed') }) QUnit.test('should allow DOMElement title (html: false)', function (assert) { @@ -170,7 +170,7 @@ $(function () { assert.ok($('.tooltip').hasClass('some-class'), 'custom class is present') $tooltip.bootstrapTooltip('hide') - assert.strictEqual($tooltip.data('bs.tooltip').tip.parentNode, null, 'tooltip removed') + assert.strictEqual(Data.getData($tooltip[0], 'bs.tooltip').tip.parentNode, null, 'tooltip removed') }) QUnit.test('should fire show event', function (assert) { @@ -294,22 +294,17 @@ $(function () { }) QUnit.test('should destroy tooltip', function (assert) { - assert.expect(7) + assert.expect(3) var $tooltip = $('
') .bootstrapTooltip() - .on('click.foo', 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) { @@ -380,7 +375,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() @@ -481,7 +476,7 @@ $(function () { animate: false }) .on('shown.bs.tooltip', function () { - var $tooltip = $($(this).data('bs.tooltip').tip) + var $tooltip = $(Data.getData(this, 'bs.tooltip').tip) if (/iPhone|iPad|iPod/.test(navigator.userAgent)) { assert.ok(Math.round($tooltip.offset().top + $tooltip.outerHeight()) <= Math.round($(this).offset().top)) } @@ -510,7 +505,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) { @@ -523,7 +518,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 () { @@ -531,7 +526,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) { @@ -544,11 +539,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 () { @@ -557,7 +552,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) { @@ -570,7 +565,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 () { @@ -578,7 +573,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) { @@ -591,7 +586,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 () { @@ -599,7 +594,7 @@ $(function () { done() }, 250) - $tooltip.trigger('mouseenter') + EventHandler.trigger($tooltip[0], 'mouseover') }) QUnit.test('should wait 200ms before hiding the tooltip', function (assert) { @@ -611,22 +606,22 @@ $(function () { .bootstrapTooltip({ delay: { show: 0, hide: 150 } }) 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) { @@ -648,11 +643,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()) }) @@ -675,18 +670,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()) @@ -701,7 +696,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) { @@ -709,34 +704,34 @@ $(function () { var $el = $('') .appendTo('#qunit-fixture') .bootstrapTooltip({ 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() { return $tooltip.hasClass('show') || tooltip._hoverState === 'show' } 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()) } }) @@ -748,18 +743,18 @@ $(function () { .appendTo('#qunit-fixture') .bootstrapTooltip({ 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() { 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') }) @@ -801,7 +796,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')) @@ -818,7 +813,7 @@ $(function () { .appendTo('#qunit-fixture') .bootstrapTooltip('show') .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() @@ -841,11 +836,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 ee517f1e007b..e8ae73189118 100644 --- a/js/tests/visual/popover.html +++ b/js/tests/visual/popover.html @@ -34,7 +34,9 @@

Popover Bootstrap Visual Test

+ + diff --git a/js/tests/visual/tooltip.html b/js/tests/visual/tooltip.html index fa84a20e4b38..fe1a490e1869 100644 --- a/js/tests/visual/tooltip.html +++ b/js/tests/visual/tooltip.html @@ -55,6 +55,9 @@

Tooltip Bootstrap Visual Test

+ + +