diff --git a/js/src/dom/selectorEngine.js b/js/src/dom/selectorEngine.js index d132d7d245dde5319d5d690aff95d5d8140af223..a3e88ad6eb661d9a62fbca60c1bf86631def7988 100644 --- a/js/src/dom/selectorEngine.js +++ b/js/src/dom/selectorEngine.js @@ -1,3 +1,5 @@ +import Util from '../util' + /** * -------------------------------------------------------------------------- * Bootstrap (v4.0.0-beta): dom/selectorEngine.js @@ -27,17 +29,13 @@ const SelectorEngine = (() => { if (!Element.prototype.closest) { fnClosest = (element, selector) => { let ancestor = element - if (!document.documentElement.contains(element)) { - return null - } - do { if (fnMatches.call(ancestor, selector)) { return ancestor } ancestor = ancestor.parentElement - } while (ancestor !== null) + } while (ancestor !== null && ancestor.nodeType === Node.ELEMENT_NODE) return null } @@ -48,12 +46,67 @@ const SelectorEngine = (() => { } } + const scopeSelectorRegex = /:scope\b/ + const supportScopeQuery = (() => { + const element = document.createElement('div') + try { + element.querySelectorAll(':scope *') + } catch (e) { + return false + } + + return true + })() + + let findFn = null + let findOneFn = null + if (supportScopeQuery) { + findFn = Element.prototype.querySelectorAll + findOneFn = Element.prototype.querySelector + } else { + findFn = function (selector) { + if (!scopeSelectorRegex.test(selector)) { + return this.querySelectorAll(selector) + } + + const hasId = Boolean(this.id) + if (!hasId) { + this.id = Util.getUID('scope') + } + + let nodeList = null + try { + selector = selector.replace(scopeSelectorRegex, `#${this.id}`) + nodeList = this.querySelectorAll(selector) + } finally { + if (!hasId) { + this.removeAttribute('id') + } + } + + return nodeList + } + + findOneFn = function (selector) { + if (!scopeSelectorRegex.test(selector)) { + return this.querySelector(selector) + } + + const matches = findFn.call(this, selector) + if (typeof matches[0] !== 'undefined') { + return matches[0] + } + + return null + } + } + return { matches(element, selector) { return fnMatches.call(element, selector) }, - find(selector, element = document) { + find(selector, element = document.documentElement) { if (typeof selector !== 'string') { return null } @@ -62,21 +115,24 @@ const SelectorEngine = (() => { return SelectorEngine.findOne(selector, element) } - return element.querySelectorAll(selector) + return findFn.call(element, selector) }, - findOne(selector, element = document) { + findOne(selector, element = document.documentElement) { if (typeof selector !== 'string') { return null } - let selectorType = 'querySelector' - if (selector.indexOf('#') === 0) { - selectorType = 'getElementById' - selector = selector.substr(1, selector.length) + return findOneFn.call(element, selector) + }, + + children(element, selector) { + if (typeof selector !== 'string') { + return null } - return element[selectorType](selector) + const children = Util.makeArray(element.children) + return children.filter((child) => this.matches(child, selector)) }, closest(element, selector) { diff --git a/js/src/tab.js b/js/src/tab.js index cb80997afc8bf106ee7a8d9a1448dd27a317afe9..934341348039501d9031cd153ccd79c1772e70f9 100644 --- a/js/src/tab.js +++ b/js/src/tab.js @@ -5,7 +5,9 @@ * -------------------------------------------------------------------------- */ -import $ from 'jquery' +import Data from './dom/data' +import EventHandler from './dom/eventHandler' +import SelectorEngine from './dom/selectorEngine' import Util from './util' /** @@ -19,7 +21,6 @@ const VERSION = '4.3.1' const DATA_KEY = 'bs.tab' const EVENT_KEY = `.${DATA_KEY}` const DATA_API_KEY = '.data-api' -const JQUERY_NO_CONFLICT = $.fn[NAME] const Event = { HIDE : `hide${EVENT_KEY}`, @@ -41,10 +42,10 @@ const Selector = { DROPDOWN : '.dropdown', NAV_LIST_GROUP : '.nav, .list-group', ACTIVE : '.active', - ACTIVE_UL : '> li > .active', + ACTIVE_UL : ':scope > li > .active', DATA_TOGGLE : '[data-toggle="tab"], [data-toggle="pill"], [data-toggle="list"]', DROPDOWN_TOGGLE : '.dropdown-toggle', - DROPDOWN_ACTIVE_CHILD : '> .dropdown-menu .active' + DROPDOWN_ACTIVE_CHILD : ':scope > .dropdown-menu .active' } /** @@ -56,6 +57,8 @@ const Selector = { class Tab { constructor(element) { this._element = element + + Data.setData(this._element, DATA_KEY, this) } // Getters @@ -68,39 +71,37 @@ class Tab { show() { if (this._element.parentNode && - this._element.parentNode.nodeType === Node.ELEMENT_NODE && - $(this._element).hasClass(ClassName.ACTIVE) || - $(this._element).hasClass(ClassName.DISABLED)) { + this._element.parentNode.nodeType === Node.ELEMENT_NODE && + this._element.classList.contains(ClassName.ACTIVE) || + this._element.classList.contains(ClassName.DISABLED)) { return } let target let previous - const listElement = $(this._element).closest(Selector.NAV_LIST_GROUP)[0] + const listElement = SelectorEngine.closest(this._element, Selector.NAV_LIST_GROUP) const selector = Util.getSelectorFromElement(this._element) if (listElement) { const itemSelector = listElement.nodeName === 'UL' || listElement.nodeName === 'OL' ? Selector.ACTIVE_UL : Selector.ACTIVE - previous = $.makeArray($(listElement).find(itemSelector)) + previous = Util.makeArray(SelectorEngine.find(itemSelector, listElement)) previous = previous[previous.length - 1] } - const hideEvent = $.Event(Event.HIDE, { - relatedTarget: this._element - }) - - const showEvent = $.Event(Event.SHOW, { - relatedTarget: previous - }) + let hideEvent = null if (previous) { - $(previous).trigger(hideEvent) + hideEvent = EventHandler.trigger(previous, Event.HIDE, { + relatedTarget: this._element + }) } - $(this._element).trigger(showEvent) + const showEvent = EventHandler.trigger(this._element, Event.SHOW, { + relatedTarget: previous + }) - if (showEvent.isDefaultPrevented() || - hideEvent.isDefaultPrevented()) { + if (showEvent.defaultPrevented || + hideEvent !== null && hideEvent.defaultPrevented) { return } @@ -114,16 +115,12 @@ class Tab { ) const complete = () => { - const hiddenEvent = $.Event(Event.HIDDEN, { + EventHandler.trigger(previous, Event.HIDDEN, { relatedTarget: this._element }) - - const shownEvent = $.Event(Event.SHOWN, { + EventHandler.trigger(this._element, Event.SHOWN, { relatedTarget: previous }) - - $(previous).trigger(hiddenEvent) - $(this._element).trigger(shownEvent) } if (target) { @@ -134,7 +131,7 @@ class Tab { } dispose() { - $.removeData(this._element, DATA_KEY) + Data.removeData(this._element, DATA_KEY) this._element = null } @@ -142,11 +139,13 @@ class Tab { _activate(element, container, callback) { const activeElements = container && (container.nodeName === 'UL' || container.nodeName === 'OL') - ? $(container).find(Selector.ACTIVE_UL) - : $(container).children(Selector.ACTIVE) + ? SelectorEngine.find(Selector.ACTIVE_UL, container) + : SelectorEngine.children(container, Selector.ACTIVE) + + const active = activeElements[0] + const isTransitioning = callback && + (active && active.classList.contains(ClassName.FADE)) - const active = activeElements[0] - const isTransitioning = callback && (active && $(active).hasClass(ClassName.FADE)) const complete = () => this._transitionComplete( element, active, @@ -155,10 +154,9 @@ class Tab { if (active && isTransitioning) { const transitionDuration = Util.getTransitionDurationFromElement(active) + active.classList.remove(ClassName.SHOW) - $(active) - .removeClass(ClassName.SHOW) - .one(Util.TRANSITION_END, complete) + EventHandler.one(active, Util.TRANSITION_END, complete) Util.emulateTransitionEnd(active, transitionDuration) } else { complete() @@ -167,14 +165,12 @@ class Tab { _transitionComplete(element, active, callback) { if (active) { - $(active).removeClass(ClassName.ACTIVE) + active.classList.remove(ClassName.ACTIVE) - const dropdownChild = $(active.parentNode).find( - Selector.DROPDOWN_ACTIVE_CHILD - )[0] + const dropdownChild = SelectorEngine.findOne(Selector.DROPDOWN_ACTIVE_CHILD, active.parentNode) if (dropdownChild) { - $(dropdownChild).removeClass(ClassName.ACTIVE) + dropdownChild.classList.remove(ClassName.ACTIVE) } if (active.getAttribute('role') === 'tab') { @@ -182,7 +178,7 @@ class Tab { } } - $(element).addClass(ClassName.ACTIVE) + element.classList.add(ClassName.ACTIVE) if (element.getAttribute('role') === 'tab') { element.setAttribute('aria-selected', true) } @@ -193,13 +189,12 @@ class Tab { element.classList.add(ClassName.SHOW) } - if (element.parentNode && $(element.parentNode).hasClass(ClassName.DROPDOWN_MENU)) { - const dropdownElement = $(element).closest(Selector.DROPDOWN)[0] + if (element.parentNode && element.parentNode.classList.contains(ClassName.DROPDOWN_MENU)) { + const dropdownElement = SelectorEngine.closest(element, Selector.DROPDOWN) if (dropdownElement) { - const dropdownToggleList = [].slice.call(dropdownElement.querySelectorAll(Selector.DROPDOWN_TOGGLE)) - - $(dropdownToggleList).addClass(ClassName.ACTIVE) + Util.makeArray(dropdownElement.querySelectorAll(Selector.DROPDOWN_TOGGLE)) + .forEach((dropdown) => dropdown.classList.add(ClassName.ACTIVE)) } element.setAttribute('aria-expanded', true) @@ -214,13 +209,7 @@ class Tab { static _jQueryInterface(config) { return this.each(function () { - const $this = $(this) - let data = $this.data(DATA_KEY) - - if (!data) { - data = new Tab(this) - $this.data(DATA_KEY, data) - } + const data = Data.getData(this, DATA_KEY) || new Tab(this) if (typeof config === 'string') { if (typeof data[config] === 'undefined') { @@ -238,11 +227,12 @@ class Tab { * ------------------------------------------------------------------------ */ -$(document) - .on(Event.CLICK_DATA_API, Selector.DATA_TOGGLE, function (event) { - event.preventDefault() - Tab._jQueryInterface.call($(this), 'show') - }) +EventHandler.on(document, Event.CLICK_DATA_API, Selector.DATA_TOGGLE, function (event) { + event.preventDefault() + + const data = Data.getData(this, DATA_KEY) || new Tab(this) + data.show() +}) /** * ------------------------------------------------------------------------ @@ -250,11 +240,15 @@ $(document) * ------------------------------------------------------------------------ */ -$.fn[NAME] = Tab._jQueryInterface -$.fn[NAME].Constructor = Tab -$.fn[NAME].noConflict = () => { - $.fn[NAME] = JQUERY_NO_CONFLICT - return Tab._jQueryInterface +const $ = Util.jQuery +if (typeof $ !== 'undefined') { + const JQUERY_NO_CONFLICT = $.fn[NAME] + $.fn[NAME] = Tab._jQueryInterface + $.fn[NAME].Constructor = Tab + $.fn[NAME].noConflict = () => { + $.fn[NAME] = JQUERY_NO_CONFLICT + return Tab._jQueryInterface + } } export default Tab diff --git a/js/tests/unit/tab.js b/js/tests/unit/tab.js index 4491e14948c4fe02dd161bcf549720dd734c4102..00dcac18f64f436d1254d61ec055078c6b4484b0 100644 --- a/js/tests/unit/tab.js +++ b/js/tests/unit/tab.js @@ -320,7 +320,7 @@ $(function () { '</ul>' var $tabs = $(tabsHTML).appendTo('#qunit-fixture') - $tabs.find('li:last-child a').trigger('click') + EventHandler.trigger($tabs.find('li:last-child a')[0], 'click') assert.notOk($tabs.find('li:first-child a').hasClass('active')) assert.ok($tabs.find('li:last-child a').hasClass('active')) }) @@ -339,7 +339,7 @@ $(function () { '</ul>' var $tabs = $(tabsHTML).appendTo('#qunit-fixture') - $tabs.find('li:first-child a').trigger('click') + EventHandler.trigger($tabs.find('li:first-child a')[0], 'click') assert.ok($tabs.find('li:first-child a').hasClass('active')) assert.notOk($tabs.find('li:last-child a').hasClass('active')) assert.notOk($tabs.find('li:last-child .dropdown-menu a:first-child').hasClass('active')) @@ -378,9 +378,10 @@ $(function () { $('#tab1').on('shown.bs.tab', function () { assert.ok($('#x-tab1').hasClass('active')) - $('#tabNested2').trigger($.Event('click')) + EventHandler.trigger($('#tabNested2')[0], 'click') }) - .trigger($.Event('click')) + + EventHandler.trigger($('#tab1')[0], 'click') }) QUnit.test('should not remove fade class if no active pane is present', function (assert) { @@ -410,9 +411,11 @@ $(function () { done() }) - .trigger($.Event('click')) + + EventHandler.trigger($('#tab-home')[0], 'click') }) - .trigger($.Event('click')) + + EventHandler.trigger($('#tab-profile')[0], 'click') }) QUnit.test('should handle removed tabs', function (assert) { diff --git a/js/tests/visual/tab.html b/js/tests/visual/tab.html index 3b8ce4026f49b9f983cd905720966657d1ad8be8..78e573403918bacc29c04f50f4e738b65c4f12c2 100644 --- a/js/tests/visual/tab.html +++ b/js/tests/visual/tab.html @@ -227,7 +227,10 @@ <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/manipulator.js"></script> + <script src="../../dist/dom/selectorEngine.js"></script> <script src="../../dist/util.js"></script> <script src="../../dist/tab.js"></script> <script src="../../dist/dropdown.js"></script>