diff --git a/js/src/collapse.js b/js/src/collapse.js index a6b540de9e846d2160846595ab93baf7367fef63..9c7ff927766563606a4096351501dd08ffd4eb02 100644 --- a/js/src/collapse.js +++ b/js/src/collapse.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.collapse' const EVENT_KEY = `.${DATA_KEY}` const DATA_API_KEY = '.data-api' -const JQUERY_NO_CONFLICT = $.fn[NAME] const Default = { toggle : true, @@ -67,19 +68,19 @@ class Collapse { this._isTransitioning = false this._element = element this._config = this._getConfig(config) - this._triggerArray = [].slice.call(document.querySelectorAll( + this._triggerArray = Util.makeArray(SelectorEngine.find( `[data-toggle="collapse"][href="#${element.id}"],` + `[data-toggle="collapse"][data-target="#${element.id}"]` )) - const toggleList = [].slice.call(document.querySelectorAll(Selector.DATA_TOGGLE)) + const toggleList = Util.makeArray(document.querySelectorAll(Selector.DATA_TOGGLE)) for (let i = 0, len = toggleList.length; i < len; i++) { const elem = toggleList[i] const selector = Util.getSelectorFromElement(elem) - const filterElement = [].slice.call(document.querySelectorAll(selector)) + const filterElement = Util.makeArray(document.querySelectorAll(selector)) .filter((foundElem) => foundElem === element) - if (selector !== null && filterElement.length > 0) { + if (selector !== null && filterElement.length) { this._selector = selector this._triggerArray.push(elem) } @@ -109,7 +110,7 @@ class Collapse { // Public toggle() { - if ($(this._element).hasClass(ClassName.SHOW)) { + if (this._element.classList.contains(ClassName.SHOW)) { this.hide() } else { this.show() @@ -118,7 +119,7 @@ class Collapse { show() { if (this._isTransitioning || - $(this._element).hasClass(ClassName.SHOW)) { + this._element.classList.contains(ClassName.SHOW)) { return } @@ -126,7 +127,7 @@ class Collapse { let activesData if (this._parent) { - actives = [].slice.call(this._parent.querySelectorAll(Selector.ACTIVES)) + actives = Util.makeArray(this._parent.querySelectorAll(Selector.ACTIVES)) .filter((elem) => { if (typeof this._config.parent === 'string') { return elem.getAttribute('data-parent') === this._config.parent @@ -141,74 +142,70 @@ class Collapse { } if (actives) { - activesData = $(actives).not(this._selector).data(DATA_KEY) + activesData = Data.getData(actives[0], DATA_KEY) if (activesData && activesData._isTransitioning) { return } } - const startEvent = $.Event(Event.SHOW) - $(this._element).trigger(startEvent) - if (startEvent.isDefaultPrevented()) { + const startEvent = EventHandler.trigger(this._element, Event.SHOW) + if (startEvent.defaultPrevented) { return } if (actives) { - Collapse._jQueryInterface.call($(actives).not(this._selector), 'hide') + actives.forEach((elemActive) => Collapse._collapseInterface(elemActive, 'hide')) if (!activesData) { - $(actives).data(DATA_KEY, null) + Data.setData(actives[0], DATA_KEY, null) } } const dimension = this._getDimension() - $(this._element) - .removeClass(ClassName.COLLAPSE) - .addClass(ClassName.COLLAPSING) + this._element.classList.remove(ClassName.COLLAPSE) + this._element.classList.add(ClassName.COLLAPSING) this._element.style[dimension] = 0 if (this._triggerArray.length) { - $(this._triggerArray) - .removeClass(ClassName.COLLAPSED) - .attr('aria-expanded', true) + this._triggerArray.forEach((element) => { + element.classList.remove(ClassName.COLLAPSED) + element.setAttribute('aria-expanded', true) + }) } this.setTransitioning(true) const complete = () => { - $(this._element) - .removeClass(ClassName.COLLAPSING) - .addClass(ClassName.COLLAPSE) - .addClass(ClassName.SHOW) + this._element.classList.remove(ClassName.COLLAPSING) + this._element.classList.add(ClassName.COLLAPSE) + this._element.classList.add(ClassName.SHOW) this._element.style[dimension] = '' this.setTransitioning(false) - $(this._element).trigger(Event.SHOWN) + EventHandler.trigger(this._element, Event.SHOWN) } const capitalizedDimension = dimension[0].toUpperCase() + dimension.slice(1) const scrollSize = `scroll${capitalizedDimension}` const transitionDuration = Util.getTransitionDurationFromElement(this._element) - $(this._element) - .one(Util.TRANSITION_END, complete) + EventHandler.one(this._element, Util.TRANSITION_END, complete) - Util.emulateTransitionEnd(transitionDuration) + Util.emulateTransitionEnd(this._element, transitionDuration) this._element.style[dimension] = `${this._element[scrollSize]}px` } hide() { if (this._isTransitioning || - !$(this._element).hasClass(ClassName.SHOW)) { + !this._element.classList.contains(ClassName.SHOW)) { return } - const startEvent = $.Event(Event.HIDE) - $(this._element).trigger(startEvent) - if (startEvent.isDefaultPrevented()) { + const startEvent = EventHandler.trigger(this._element, Event.HIDE) + if (startEvent.defaultPrevented) { return } @@ -218,10 +215,9 @@ class Collapse { Util.reflow(this._element) - $(this._element) - .addClass(ClassName.COLLAPSING) - .removeClass(ClassName.COLLAPSE) - .removeClass(ClassName.SHOW) + this._element.classList.add(ClassName.COLLAPSING) + this._element.classList.remove(ClassName.COLLAPSE) + this._element.classList.remove(ClassName.SHOW) const triggerArrayLength = this._triggerArray.length if (triggerArrayLength > 0) { @@ -230,10 +226,11 @@ class Collapse { const selector = Util.getSelectorFromElement(trigger) if (selector !== null) { - const $elem = $([].slice.call(document.querySelectorAll(selector))) - if (!$elem.hasClass(ClassName.SHOW)) { - $(trigger).addClass(ClassName.COLLAPSED) - .attr('aria-expanded', false) + const elem = SelectorEngine.findOne(selector) + + if (!elem.classList.contains(ClassName.SHOW)) { + trigger.classList.add(ClassName.COLLAPSED) + trigger.setAttribute('aria-expanded', false) } } } @@ -243,19 +240,16 @@ class Collapse { const complete = () => { this.setTransitioning(false) - $(this._element) - .removeClass(ClassName.COLLAPSING) - .addClass(ClassName.COLLAPSE) - .trigger(Event.HIDDEN) + this._element.classList.remove(ClassName.COLLAPSING) + this._element.classList.add(ClassName.COLLAPSE) + EventHandler.trigger(this._element, Event.HIDDEN) } this._element.style[dimension] = '' const transitionDuration = Util.getTransitionDurationFromElement(this._element) - $(this._element) - .one(Util.TRANSITION_END, complete) - - Util.emulateTransitionEnd(transitionDuration) + EventHandler.one(this._element, Util.TRANSITION_END, complete) + Util.emulateTransitionEnd(this._element, transitionDuration) } setTransitioning(isTransitioning) { @@ -263,7 +257,7 @@ class Collapse { } dispose() { - $.removeData(this._element, DATA_KEY) + Data.removeData(this._element, DATA_KEY) this._config = null this._parent = null @@ -285,7 +279,7 @@ class Collapse { } _getDimension() { - const hasWidth = $(this._element).hasClass(Dimension.WIDTH) + const hasWidth = this._element.classList.contains(Dimension.WIDTH) return hasWidth ? Dimension.WIDTH : Dimension.HEIGHT } @@ -300,14 +294,14 @@ class Collapse { parent = this._config.parent[0] } } else { - parent = document.querySelector(this._config.parent) + parent = SelectorEngine.findOne(this._config.parent) } const selector = `[data-toggle="collapse"][data-parent="${this._config.parent}"]` - const children = [].slice.call(parent.querySelectorAll(selector)) - $(children).each((i, element) => { + const elements = Util.makeArray(SelectorEngine.find(selector, parent)) + elements.forEach((element) => { this._addAriaAndCollapsedClass( Collapse._getTargetFromElement(element), [element] @@ -318,12 +312,19 @@ class Collapse { } _addAriaAndCollapsedClass(element, triggerArray) { - const isOpen = $(element).hasClass(ClassName.SHOW) - - if (triggerArray.length) { - $(triggerArray) - .toggleClass(ClassName.COLLAPSED, !isOpen) - .attr('aria-expanded', isOpen) + if (element) { + const isOpen = element.classList.contains(ClassName.SHOW) + + if (triggerArray.length) { + triggerArray.forEach((elem) => { + if (!isOpen) { + elem.classList.add(ClassName.COLLAPSED) + } else { + elem.classList.remove(ClassName.COLLAPSED) + } + elem.setAttribute('aria-expanded', isOpen) + }) + } } } @@ -334,31 +335,34 @@ class Collapse { return selector ? document.querySelector(selector) : null } - static _jQueryInterface(config) { - return this.each(function () { - const $this = $(this) - let data = $this.data(DATA_KEY) - const _config = { - ...Default, - ...$this.data(), - ...typeof config === 'object' && config ? config : {} - } + static _collapseInterface(element, config) { + let data = Data.getData(element, DATA_KEY) + const _config = { + ...Default, + ...Util.getDataAttributes(element), + ...typeof config === 'object' && config ? config : {} + } - if (!data && _config.toggle && /show|hide/.test(config)) { - _config.toggle = false - } + if (!data && _config.toggle && /show|hide/.test(config)) { + _config.toggle = false + } - if (!data) { - data = new Collapse(this, _config) - $this.data(DATA_KEY, data) - } + if (!data) { + data = new Collapse(element, _config) + Data.setData(element, DATA_KEY, data) + } - if (typeof config === 'string') { - if (typeof data[config] === 'undefined') { - throw new TypeError(`No method named "${config}"`) - } - data[config]() + if (typeof config === 'string') { + if (typeof data[config] === 'undefined') { + throw new Error(`No method named "${config}"`) } + data[config]() + } + } + + static _jQueryInterface(config) { + return this.each(function () { + Collapse._collapseInterface(this, config) }) } } @@ -369,21 +373,31 @@ class Collapse { * ------------------------------------------------------------------------ */ -$(document).on(Event.CLICK_DATA_API, Selector.DATA_TOGGLE, function (event) { +EventHandler.on(document, Event.CLICK_DATA_API, Selector.DATA_TOGGLE, function (event) { // preventDefault only for <a> elements (which change the URL) not inside the collapsible element - if (event.currentTarget.tagName === 'A') { + if (event.target.tagName === 'A') { event.preventDefault() } - const $trigger = $(this) - const selector = Util.getSelectorFromElement(this) - const selectors = [].slice.call(document.querySelectorAll(selector)) + const triggerData = Util.getDataAttributes(this) + const selector = Util.getSelectorFromElement(this) + const selectorElements = Util.makeArray(SelectorEngine.find(selector)) + + selectorElements.forEach((element) => { + const data = Data.getData(element, DATA_KEY) + let config + if (data) { + // update parent attribute + if (data._parent === null && typeof triggerData.parent === 'string') { + data._config.parent = triggerData.parent + data._parent = data._getParent() + } + config = 'toggle' + } else { + config = triggerData + } - $(selectors).each(function () { - const $target = $(this) - const data = $target.data(DATA_KEY) - const config = data ? 'toggle' : $trigger.data() - Collapse._jQueryInterface.call($target, config) + Collapse._collapseInterface(element, config) }) }) @@ -391,13 +405,18 @@ $(document).on(Event.CLICK_DATA_API, Selector.DATA_TOGGLE, function (event) { * ------------------------------------------------------------------------ * jQuery * ------------------------------------------------------------------------ + * add .collapse to jQuery only if jQuery is present */ -$.fn[NAME] = Collapse._jQueryInterface -$.fn[NAME].Constructor = Collapse -$.fn[NAME].noConflict = () => { - $.fn[NAME] = JQUERY_NO_CONFLICT - return Collapse._jQueryInterface +const $ = Util.jQuery +if (typeof $ !== 'undefined') { + const JQUERY_NO_CONFLICT = $.fn[NAME] + $.fn[NAME] = Collapse._jQueryInterface + $.fn[NAME].Constructor = Collapse + $.fn[NAME].noConflict = () => { + $.fn[NAME] = JQUERY_NO_CONFLICT + return Collapse._jQueryInterface + } } export default Collapse diff --git a/js/src/util.js b/js/src/util.js index 607d50fd43c04e89cda4f4c53751af4f16bae24b..e0a81b9ec87c17e86b0979c0f1892fc5c51cc7f1 100644 --- a/js/src/util.js +++ b/js/src/util.js @@ -154,7 +154,14 @@ const Util = { if (typeof nodeList === 'undefined' || nodeList === null) { return [] } - return Array.prototype.slice.call(nodeList) + + const strRepresentation = Object.prototype.toString.call(nodeList) + if (strRepresentation === '[object NodeList]' || + strRepresentation === '[object HTMLCollection]' || strRepresentation === '[object Array]') { + return Array.prototype.slice.call(nodeList) + } + + return [nodeList] }, getDataAttributes(element) { diff --git a/js/tests/unit/collapse.js b/js/tests/unit/collapse.js index e7fb8893edae7be4dafd05078275a947ea3f0224..3df60200b91553f28349a2aae769589835bd8ee2 100644 --- a/js/tests/unit/collapse.js +++ b/js/tests/unit/collapse.js @@ -71,7 +71,7 @@ $(function () { assert.ok(!/height/i.test($el2.attr('style')), 'has height reset') done() }) - $target.trigger('click') + EventHandler.trigger($target[0], 'click') }) QUnit.test('should collapse only the first collapse', function (assert) { @@ -165,7 +165,7 @@ $(function () { done() }) - $target.trigger('click') + EventHandler.trigger($target[0], 'click') }) QUnit.test('should add "collapsed" class to target when collapse is hidden', function (assert) { @@ -181,7 +181,7 @@ $(function () { done() }) - $target.trigger('click') + EventHandler.trigger($target[0], 'click') }) QUnit.test('should remove "collapsed" class from all triggers targeting the collapse when the collapse is shown', function (assert) { @@ -199,7 +199,7 @@ $(function () { done() }) - $target.trigger('click') + EventHandler.trigger($target[0], 'click') }) QUnit.test('should add "collapsed" class to all triggers targeting the collapse when the collapse is hidden', function (assert) { @@ -217,7 +217,7 @@ $(function () { done() }) - $target.trigger('click') + EventHandler.trigger($target[0], 'click') }) QUnit.test('should not close a collapse when initialized with "show" option if already shown', function (assert) { @@ -309,7 +309,7 @@ $(function () { done() }) - $target3.trigger('click') + EventHandler.trigger($target3[0], 'click') }) QUnit.test('should allow dots in data-parent', function (assert) { @@ -343,7 +343,7 @@ $(function () { done() }) - $target3.trigger('click') + EventHandler.trigger($target3[0], 'click') }) QUnit.test('should set aria-expanded="true" on trigger/control when collapse is shown', function (assert) { @@ -359,7 +359,7 @@ $(function () { done() }) - $target.trigger('click') + EventHandler.trigger($target[0], 'click') }) QUnit.test('should set aria-expanded="false" on trigger/control when collapse is hidden', function (assert) { @@ -375,7 +375,7 @@ $(function () { done() }) - $target.trigger('click') + EventHandler.trigger($target[0], 'click') }) QUnit.test('should set aria-expanded="true" on all triggers targeting the collapse when the collapse is shown', function (assert) { @@ -393,7 +393,7 @@ $(function () { done() }) - $target.trigger('click') + EventHandler.trigger($target[0], 'click') }) QUnit.test('should set aria-expanded="false" on all triggers targeting the collapse when the collapse is hidden', function (assert) { @@ -411,7 +411,7 @@ $(function () { done() }) - $target.trigger('click') + EventHandler.trigger($target[0], 'click') }) QUnit.test('should change aria-expanded from active accordion trigger/control to "false" and set the trigger/control for the newly active one to "true"', function (assert) { @@ -445,7 +445,7 @@ $(function () { done() }) - $target3.trigger('click') + EventHandler.trigger($target3[0], 'click') }) QUnit.test('should not fire show event if show is prevented because other element is still transitioning', function (assert) { @@ -470,13 +470,12 @@ $(function () { var $target2 = $('<a role="button" data-toggle="collapse" href="#body2"/>').appendTo($groups.eq(1)) var $body2 = $('<div id="body2" class="collapse" data-parent="#accordion"/>').appendTo($groups.eq(1)) - $target2.trigger('click') + EventHandler.trigger($target2[0], 'click') - $body2 - .toggleClass('show collapsing') - .data('bs.collapse')._isTransitioning = 1 + $body2.toggleClass('show collapsing') + Data.getData($body2[0], 'bs.collapse')._isTransitioning = true - $target1.trigger('click') + EventHandler.trigger($target1[0], 'click') setTimeout(function () { assert.ok(!showFired, 'show event did not fire') @@ -541,9 +540,9 @@ $(function () { assert.ok($collapseTwo.hasClass('show'), '#collapseTwo is shown') done() }) - $triggerTwo.trigger($.Event('click')) + EventHandler.trigger($triggerTwo[0], 'click') }) - $trigger.trigger($.Event('click')) + EventHandler.trigger($trigger[0], 'click') }) QUnit.test('should allow accordion to contain nested elements', function (assert) { @@ -687,40 +686,40 @@ $(function () { var $collapseTwo = $('#collapseTwo') var $nestedCollapseOne = $('#nestedCollapseOne') - $collapseOne.one('shown.bs.collapse', function () { + EventHandler.one($collapseOne[0], 'shown.bs.collapse', function () { assert.ok($collapseOne.hasClass('show'), '#collapseOne is shown') assert.ok(!$collapseTwo.hasClass('show'), '#collapseTwo is not shown') assert.ok(!$('#nestedCollapseOne').hasClass('show'), '#nestedCollapseOne is not shown') - $nestedCollapseOne.one('shown.bs.collapse', function () { + + EventHandler.one($nestedCollapseOne[0], 'shown.bs.collapse', function () { assert.ok($collapseOne.hasClass('show'), '#collapseOne is shown') assert.ok(!$collapseTwo.hasClass('show'), '#collapseTwo is not shown') assert.ok($nestedCollapseOne.hasClass('show'), '#nestedCollapseOne is shown') - $collapseTwo.one('shown.bs.collapse', function () { + EventHandler.one($collapseTwo[0], 'shown.bs.collapse', function () { assert.ok(!$collapseOne.hasClass('show'), '#collapseOne is not shown') assert.ok($collapseTwo.hasClass('show'), '#collapseTwo is shown') assert.ok($nestedCollapseOne.hasClass('show'), '#nestedCollapseOne is shown') done() }) - $triggerTwo.trigger($.Event('click')) + EventHandler.trigger($triggerTwo[0], 'click') }) - $nestedTrigger.trigger($.Event('click')) + EventHandler.trigger($nestedTrigger[0], 'click') }) - $trigger.trigger($.Event('click')) + EventHandler.trigger($trigger[0], 'click') }) QUnit.test('should not prevent event for input', function (assert) { assert.expect(3) var done = assert.async() var $target = $('<input type="checkbox" data-toggle="collapse" data-target="#collapsediv1" />').appendTo('#qunit-fixture') + var $collapse = $('<div id="collapsediv1"/>').appendTo('#qunit-fixture') - $('<div id="collapsediv1"/>') - .appendTo('#qunit-fixture') - .on('shown.bs.collapse', function () { - assert.ok($(this).hasClass('show')) - assert.ok($target.attr('aria-expanded') === 'true') - assert.ok($target.prop('checked')) - done() - }) + EventHandler.one($collapse[0], 'shown.bs.collapse', function () { + assert.ok($collapse.hasClass('show')) + assert.ok($target.attr('aria-expanded') === 'true') + assert.ok($target.prop('checked')) + done() + }) $target.trigger($.Event('click')) }) @@ -750,11 +749,11 @@ $(function () { assert.ok($trigger3.hasClass('collapsed'), 'trigger3 has collapsed class') done() }) - $trigger1.trigger('click') + EventHandler.trigger($trigger1[0], 'click') }) - $trigger2.trigger('click') + EventHandler.trigger($trigger2[0], 'click') }) - $trigger3.trigger('click') + EventHandler.trigger($trigger3[0], 'click') }) QUnit.test('should set aria-expanded="true" to triggers targeting shown collaspe and aria-expanded="false" only when all the targeted collapses are shown', function (assert) { @@ -782,11 +781,11 @@ $(function () { assert.strictEqual($trigger3.attr('aria-expanded'), 'false', 'aria-expanded on trigger3 is "false"') done() }) - $trigger1.trigger('click') + EventHandler.trigger($trigger1[0], 'click') }) - $trigger2.trigger('click') + EventHandler.trigger($trigger2[0], 'click') }) - $trigger3.trigger('click') + EventHandler.trigger($trigger3[0], 'click') }) QUnit.test('should not prevent interactions inside the collapse element', function (assert) { @@ -798,19 +797,17 @@ $(function () { '<div id="collapsediv1" class="collapse">' + ' <input type="checkbox" id="testCheckbox" />' + '</div>' - - $(htmlCollapse) - .appendTo('#qunit-fixture') - .on('shown.bs.collapse', function () { - assert.ok($target.prop('checked'), '$trigger is checked') - var $testCheckbox = $('#testCheckbox') - $testCheckbox.trigger($.Event('click')) - setTimeout(function () { - assert.ok($testCheckbox.prop('checked'), '$testCheckbox is checked too') - done() - }, 5) - }) - + var $collapse = $(htmlCollapse).appendTo('#qunit-fixture') + + EventHandler.one($collapse[0], 'shown.bs.collapse', function () { + assert.ok($target.prop('checked'), '$trigger is checked') + var $testCheckbox = $('#testCheckbox') + $testCheckbox.trigger($.Event('click')) + setTimeout(function () { + assert.ok($testCheckbox.prop('checked'), '$testCheckbox is checked too') + done() + }, 5) + }) $target.trigger($.Event('click')) }) diff --git a/js/tests/visual/collapse.html b/js/tests/visual/collapse.html index e084bd08bc721cf1e53f88c1482b58916a63cb9f..49d2ae82ab64cf5e3431e0f70bfb1577e0082553 100644 --- a/js/tests/visual/collapse.html +++ b/js/tests/visual/collapse.html @@ -73,6 +73,8 @@ <script src="../../../node_modules/jquery/dist/jquery.slim.min.js"></script> <script src="../../dist/dom/eventHandler.js"></script> + <script src="../../dist/dom/selectorEngine.js"></script> + <script src="../../dist/dom/data.js"></script> <script src="../../dist/util.js"></script> <script src="../../dist/collapse.js"></script> </body>