From 3d12b541c488ea09efced2fb987fcbf384c656bb Mon Sep 17 00:00:00 2001 From: Johann-S Date: Wed, 2 Oct 2019 11:43:54 +0200 Subject: return to the original file structure to avoid breaking modularity --- build/build-plugins.js | 26 +- js/index.esm.js | 22 +- js/index.umd.js | 22 +- js/src/.eslintrc.json | 14 - js/src/alert.js | 188 ++++ js/src/alert/alert.js | 188 ---- js/src/alert/alert.spec.js | 173 --- js/src/button.js | 200 ++++ js/src/button/button.js | 200 ---- js/src/button/button.spec.js | 292 ------ js/src/carousel.js | 637 +++++++++++ js/src/carousel/carousel.js | 637 ----------- js/src/carousel/carousel.spec.js | 1201 --------------------- js/src/collapse.js | 441 ++++++++ js/src/collapse/collapse.js | 441 -------- js/src/collapse/collapse.spec.js | 826 --------------- js/src/dom/data.spec.js | 131 --- js/src/dom/event-handler.spec.js | 327 ------ js/src/dom/manipulator.spec.js | 158 --- js/src/dom/selector-engine.spec.js | 115 -- js/src/dropdown.js | 545 ++++++++++ js/src/dropdown/dropdown.js | 545 ---------- js/src/dropdown/dropdown.spec.js | 1564 ---------------------------- js/src/modal.js | 604 +++++++++++ js/src/modal/modal.js | 604 ----------- js/src/modal/modal.spec.js | 987 ------------------ js/src/popover.js | 194 ++++ js/src/popover/popover.js | 194 ---- js/src/popover/popover.spec.js | 251 ----- js/src/scrollspy.js | 357 +++++++ js/src/scrollspy/scrollspy.js | 357 ------- js/src/scrollspy/scrollspy.spec.js | 653 ------------ js/src/tab.js | 264 +++++ js/src/tab/tab.js | 264 ----- js/src/tab/tab.spec.js | 593 ----------- js/src/toast.js | 244 +++++ js/src/toast/toast.js | 244 ----- js/src/toast/toast.spec.js | 374 ------- js/src/tooltip.js | 827 +++++++++++++++ js/src/tooltip/tooltip.js | 827 --------------- js/src/tooltip/tooltip.spec.js | 1020 ------------------ js/src/util/index.spec.js | 382 ------- js/src/util/sanitizer.spec.js | 70 -- js/tests/README.md | 6 +- js/tests/karma.conf.js | 83 +- js/tests/units/.eslintrc.json | 14 + js/tests/units/alert.spec.js | 173 +++ js/tests/units/button.spec.js | 292 ++++++ js/tests/units/carousel.spec.js | 1201 +++++++++++++++++++++ js/tests/units/collapse.spec.js | 826 +++++++++++++++ js/tests/units/dom/data.spec.js | 131 +++ js/tests/units/dom/event-handler.spec.js | 327 ++++++ js/tests/units/dom/manipulator.spec.js | 158 +++ js/tests/units/dom/selector-engine.spec.js | 115 ++ js/tests/units/dropdown.spec.js | 1564 ++++++++++++++++++++++++++++ js/tests/units/modal.spec.js | 987 ++++++++++++++++++ js/tests/units/popover.spec.js | 251 +++++ js/tests/units/scrollspy.spec.js | 653 ++++++++++++ js/tests/units/tab.spec.js | 593 +++++++++++ js/tests/units/toast.spec.js | 374 +++++++ js/tests/units/tooltip.spec.js | 1020 ++++++++++++++++++ js/tests/units/util/index.spec.js | 382 +++++++ js/tests/units/util/sanitizer.spec.js | 70 ++ package.json | 1 - 64 files changed, 13705 insertions(+), 13719 deletions(-) delete mode 100644 js/src/.eslintrc.json create mode 100644 js/src/alert.js delete mode 100644 js/src/alert/alert.js delete mode 100644 js/src/alert/alert.spec.js create mode 100644 js/src/button.js delete mode 100644 js/src/button/button.js delete mode 100644 js/src/button/button.spec.js create mode 100644 js/src/carousel.js delete mode 100644 js/src/carousel/carousel.js delete mode 100644 js/src/carousel/carousel.spec.js create mode 100644 js/src/collapse.js delete mode 100644 js/src/collapse/collapse.js delete mode 100644 js/src/collapse/collapse.spec.js delete mode 100644 js/src/dom/data.spec.js delete mode 100644 js/src/dom/event-handler.spec.js delete mode 100644 js/src/dom/manipulator.spec.js delete mode 100644 js/src/dom/selector-engine.spec.js create mode 100644 js/src/dropdown.js delete mode 100644 js/src/dropdown/dropdown.js delete mode 100644 js/src/dropdown/dropdown.spec.js create mode 100644 js/src/modal.js delete mode 100644 js/src/modal/modal.js delete mode 100644 js/src/modal/modal.spec.js create mode 100644 js/src/popover.js delete mode 100644 js/src/popover/popover.js delete mode 100644 js/src/popover/popover.spec.js create mode 100644 js/src/scrollspy.js delete mode 100644 js/src/scrollspy/scrollspy.js delete mode 100644 js/src/scrollspy/scrollspy.spec.js create mode 100644 js/src/tab.js delete mode 100644 js/src/tab/tab.js delete mode 100644 js/src/tab/tab.spec.js create mode 100644 js/src/toast.js delete mode 100644 js/src/toast/toast.js delete mode 100644 js/src/toast/toast.spec.js create mode 100644 js/src/tooltip.js delete mode 100644 js/src/tooltip/tooltip.js delete mode 100644 js/src/tooltip/tooltip.spec.js delete mode 100644 js/src/util/index.spec.js delete mode 100644 js/src/util/sanitizer.spec.js create mode 100644 js/tests/units/.eslintrc.json create mode 100644 js/tests/units/alert.spec.js create mode 100644 js/tests/units/button.spec.js create mode 100644 js/tests/units/carousel.spec.js create mode 100644 js/tests/units/collapse.spec.js create mode 100644 js/tests/units/dom/data.spec.js create mode 100644 js/tests/units/dom/event-handler.spec.js create mode 100644 js/tests/units/dom/manipulator.spec.js create mode 100644 js/tests/units/dom/selector-engine.spec.js create mode 100644 js/tests/units/dropdown.spec.js create mode 100644 js/tests/units/modal.spec.js create mode 100644 js/tests/units/popover.spec.js create mode 100644 js/tests/units/scrollspy.spec.js create mode 100644 js/tests/units/tab.spec.js create mode 100644 js/tests/units/toast.spec.js create mode 100644 js/tests/units/tooltip.spec.js create mode 100644 js/tests/units/util/index.spec.js create mode 100644 js/tests/units/util/sanitizer.spec.js diff --git a/build/build-plugins.js b/build/build-plugins.js index 7aa2a4e52..bf64021de 100644 --- a/build/build-plugins.js +++ b/build/build-plugins.js @@ -32,19 +32,19 @@ const bsPlugins = { Manipulator: path.resolve(__dirname, '../js/src/dom/manipulator.js'), Polyfill: path.resolve(__dirname, '../js/src/dom/polyfill.js'), SelectorEngine: path.resolve(__dirname, '../js/src/dom/selector-engine.js'), - Alert: path.resolve(__dirname, '../js/src/alert/alert.js'), - Button: path.resolve(__dirname, '../js/src/button/button.js'), - Carousel: path.resolve(__dirname, '../js/src/carousel/carousel.js'), - Collapse: path.resolve(__dirname, '../js/src/collapse/collapse.js'), - Dropdown: path.resolve(__dirname, '../js/src/dropdown/dropdown.js'), - Modal: path.resolve(__dirname, '../js/src/modal/modal.js'), - Popover: path.resolve(__dirname, '../js/src/popover/popover.js'), - ScrollSpy: path.resolve(__dirname, '../js/src/scrollspy/scrollspy.js'), - Tab: path.resolve(__dirname, '../js/src/tab/tab.js'), - Toast: path.resolve(__dirname, '../js/src/toast/toast.js'), - Tooltip: path.resolve(__dirname, '../js/src/tooltip/tooltip.js') + Alert: path.resolve(__dirname, '../js/src/alert.js'), + Button: path.resolve(__dirname, '../js/src/button.js'), + Carousel: path.resolve(__dirname, '../js/src/carousel.js'), + Collapse: path.resolve(__dirname, '../js/src/collapse.js'), + Dropdown: path.resolve(__dirname, '../js/src/dropdown.js'), + Modal: path.resolve(__dirname, '../js/src/modal.js'), + Popover: path.resolve(__dirname, '../js/src/popover.js'), + ScrollSpy: path.resolve(__dirname, '../js/src/scrollspy.js'), + Tab: path.resolve(__dirname, '../js/src/tab.js'), + Toast: path.resolve(__dirname, '../js/src/toast.js'), + Tooltip: path.resolve(__dirname, '../js/src/tooltip.js') } -const rootPath = '../js/dist/' +const rootPath = path.resolve(__dirname, '../js/dist/') const defaultPluginConfig = { external: [ @@ -172,7 +172,7 @@ function build(plugin) { name: plugin, sourcemap: true, globals, - file: path.resolve(__dirname, `${pluginPath}${pluginFilename}`) + file: path.resolve(__dirname, `${pluginPath}/${pluginFilename}`) }) .then(() => console.log(`Building ${plugin} plugin... Done!`)) .catch(error => console.error(`${plugin}: ${error}`)) diff --git a/js/index.esm.js b/js/index.esm.js index 18b12a454..e49218a1e 100644 --- a/js/index.esm.js +++ b/js/index.esm.js @@ -5,17 +5,17 @@ * -------------------------------------------------------------------------- */ -import Alert from './src/alert/alert' -import Button from './src/button/button' -import Carousel from './src/carousel/carousel' -import Collapse from './src/collapse/collapse' -import Dropdown from './src/dropdown/dropdown' -import Modal from './src/modal/modal' -import Popover from './src/popover/popover' -import ScrollSpy from './src/scrollspy/scrollspy' -import Tab from './src/tab/tab' -import Toast from './src/toast/toast' -import Tooltip from './src/tooltip/tooltip' +import Alert from './src/alert' +import Button from './src/button' +import Carousel from './src/carousel' +import Collapse from './src/collapse' +import Dropdown from './src/dropdown' +import Modal from './src/modal' +import Popover from './src/popover' +import ScrollSpy from './src/scrollspy' +import Tab from './src/tab' +import Toast from './src/toast' +import Tooltip from './src/tooltip' export { Alert, diff --git a/js/index.umd.js b/js/index.umd.js index 17657f38f..0a1d5ac0b 100644 --- a/js/index.umd.js +++ b/js/index.umd.js @@ -5,17 +5,17 @@ * -------------------------------------------------------------------------- */ -import Alert from './src/alert/alert' -import Button from './src/button/button' -import Carousel from './src/carousel/carousel' -import Collapse from './src/collapse/collapse' -import Dropdown from './src/dropdown/dropdown' -import Modal from './src/modal/modal' -import Popover from './src/popover/popover' -import ScrollSpy from './src/scrollspy/scrollspy' -import Tab from './src/tab/tab' -import Toast from './src/toast/toast' -import Tooltip from './src/tooltip/tooltip' +import Alert from './src/alert' +import Button from './src/button' +import Carousel from './src/carousel' +import Collapse from './src/collapse' +import Dropdown from './src/dropdown' +import Modal from './src/modal' +import Popover from './src/popover' +import ScrollSpy from './src/scrollspy' +import Tab from './src/tab' +import Toast from './src/toast' +import Tooltip from './src/tooltip' export default { Alert, diff --git a/js/src/.eslintrc.json b/js/src/.eslintrc.json deleted file mode 100644 index 18ffdc003..000000000 --- a/js/src/.eslintrc.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "root": true, - "extends": [ - "../../.eslintrc.json" - ], - "overrides": [ - { - "files": ["**/*.spec.js"], - "env": { - "jasmine": true - } - } - ] -} diff --git a/js/src/alert.js b/js/src/alert.js new file mode 100644 index 000000000..dbd931b52 --- /dev/null +++ b/js/src/alert.js @@ -0,0 +1,188 @@ +/** + * -------------------------------------------------------------------------- + * Bootstrap (v4.3.1): alert.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + * -------------------------------------------------------------------------- + */ + +import { + getjQuery, + TRANSITION_END, + emulateTransitionEnd, + getElementFromSelector, + getTransitionDurationFromElement +} from './util/index' +import Data from './dom/data' +import EventHandler from './dom/event-handler' +import SelectorEngine from './dom/selector-engine' + +/** + * ------------------------------------------------------------------------ + * Constants + * ------------------------------------------------------------------------ + */ + +const NAME = 'alert' +const VERSION = '4.3.1' +const DATA_KEY = 'bs.alert' +const EVENT_KEY = `.${DATA_KEY}` +const DATA_API_KEY = '.data-api' + +const Selector = { + DISMISS: '[data-dismiss="alert"]' +} + +const Event = { + CLOSE: `close${EVENT_KEY}`, + CLOSED: `closed${EVENT_KEY}`, + CLICK_DATA_API: `click${EVENT_KEY}${DATA_API_KEY}` +} + +const ClassName = { + ALERT: 'alert', + FADE: 'fade', + SHOW: 'show' +} + +/** + * ------------------------------------------------------------------------ + * Class Definition + * ------------------------------------------------------------------------ + */ + +class Alert { + constructor(element) { + this._element = element + + if (this._element) { + Data.setData(element, DATA_KEY, this) + } + } + + // Getters + + static get VERSION() { + return VERSION + } + + // Public + + close(element) { + let rootElement = this._element + if (element) { + rootElement = this._getRootElement(element) + } + + const customEvent = this._triggerCloseEvent(rootElement) + + if (customEvent === null || customEvent.defaultPrevented) { + return + } + + this._removeElement(rootElement) + } + + dispose() { + Data.removeData(this._element, DATA_KEY) + this._element = null + } + + // Private + + _getRootElement(element) { + let parent = getElementFromSelector(element) + + if (!parent) { + parent = SelectorEngine.closest(element, `.${ClassName.ALERT}`) + } + + return parent + } + + _triggerCloseEvent(element) { + return EventHandler.trigger(element, Event.CLOSE) + } + + _removeElement(element) { + element.classList.remove(ClassName.SHOW) + + if (!element.classList.contains(ClassName.FADE)) { + this._destroyElement(element) + return + } + + const transitionDuration = getTransitionDurationFromElement(element) + + EventHandler + .one(element, TRANSITION_END, () => this._destroyElement(element)) + emulateTransitionEnd(element, transitionDuration) + } + + _destroyElement(element) { + if (element.parentNode) { + element.parentNode.removeChild(element) + } + + EventHandler.trigger(element, Event.CLOSED) + } + + // Static + + static jQueryInterface(config) { + return this.each(function () { + let data = Data.getData(this, DATA_KEY) + + if (!data) { + data = new Alert(this) + } + + if (config === 'close') { + data[config](this) + } + }) + } + + static handleDismiss(alertInstance) { + return function (event) { + if (event) { + event.preventDefault() + } + + alertInstance.close(this) + } + } + + static getInstance(element) { + return Data.getData(element, DATA_KEY) + } +} + +/** + * ------------------------------------------------------------------------ + * Data Api implementation + * ------------------------------------------------------------------------ + */ +EventHandler + .on(document, Event.CLICK_DATA_API, Selector.DISMISS, Alert.handleDismiss(new Alert())) + +const $ = getjQuery() + +/** + * ------------------------------------------------------------------------ + * jQuery + * ------------------------------------------------------------------------ + * add .alert to jQuery only if jQuery is present + */ + +/* istanbul ignore if */ +if ($) { + const JQUERY_NO_CONFLICT = $.fn[NAME] + $.fn[NAME] = Alert.jQueryInterface + $.fn[NAME].Constructor = Alert + $.fn[NAME].noConflict = () => { + $.fn[NAME] = JQUERY_NO_CONFLICT + return Alert.jQueryInterface + } +} + +export default Alert diff --git a/js/src/alert/alert.js b/js/src/alert/alert.js deleted file mode 100644 index 024528b81..000000000 --- a/js/src/alert/alert.js +++ /dev/null @@ -1,188 +0,0 @@ -/** - * -------------------------------------------------------------------------- - * Bootstrap (v4.3.1): alert.js - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) - * -------------------------------------------------------------------------- - */ - -import { - getjQuery, - TRANSITION_END, - emulateTransitionEnd, - getElementFromSelector, - getTransitionDurationFromElement -} from '../util/index' -import Data from '../dom/data' -import EventHandler from '../dom/event-handler' -import SelectorEngine from '../dom/selector-engine' - -/** - * ------------------------------------------------------------------------ - * Constants - * ------------------------------------------------------------------------ - */ - -const NAME = 'alert' -const VERSION = '4.3.1' -const DATA_KEY = 'bs.alert' -const EVENT_KEY = `.${DATA_KEY}` -const DATA_API_KEY = '.data-api' - -const Selector = { - DISMISS: '[data-dismiss="alert"]' -} - -const Event = { - CLOSE: `close${EVENT_KEY}`, - CLOSED: `closed${EVENT_KEY}`, - CLICK_DATA_API: `click${EVENT_KEY}${DATA_API_KEY}` -} - -const ClassName = { - ALERT: 'alert', - FADE: 'fade', - SHOW: 'show' -} - -/** - * ------------------------------------------------------------------------ - * Class Definition - * ------------------------------------------------------------------------ - */ - -class Alert { - constructor(element) { - this._element = element - - if (this._element) { - Data.setData(element, DATA_KEY, this) - } - } - - // Getters - - static get VERSION() { - return VERSION - } - - // Public - - close(element) { - let rootElement = this._element - if (element) { - rootElement = this._getRootElement(element) - } - - const customEvent = this._triggerCloseEvent(rootElement) - - if (customEvent === null || customEvent.defaultPrevented) { - return - } - - this._removeElement(rootElement) - } - - dispose() { - Data.removeData(this._element, DATA_KEY) - this._element = null - } - - // Private - - _getRootElement(element) { - let parent = getElementFromSelector(element) - - if (!parent) { - parent = SelectorEngine.closest(element, `.${ClassName.ALERT}`) - } - - return parent - } - - _triggerCloseEvent(element) { - return EventHandler.trigger(element, Event.CLOSE) - } - - _removeElement(element) { - element.classList.remove(ClassName.SHOW) - - if (!element.classList.contains(ClassName.FADE)) { - this._destroyElement(element) - return - } - - const transitionDuration = getTransitionDurationFromElement(element) - - EventHandler - .one(element, TRANSITION_END, () => this._destroyElement(element)) - emulateTransitionEnd(element, transitionDuration) - } - - _destroyElement(element) { - if (element.parentNode) { - element.parentNode.removeChild(element) - } - - EventHandler.trigger(element, Event.CLOSED) - } - - // Static - - static jQueryInterface(config) { - return this.each(function () { - let data = Data.getData(this, DATA_KEY) - - if (!data) { - data = new Alert(this) - } - - if (config === 'close') { - data[config](this) - } - }) - } - - static handleDismiss(alertInstance) { - return function (event) { - if (event) { - event.preventDefault() - } - - alertInstance.close(this) - } - } - - static getInstance(element) { - return Data.getData(element, DATA_KEY) - } -} - -/** - * ------------------------------------------------------------------------ - * Data Api implementation - * ------------------------------------------------------------------------ - */ -EventHandler - .on(document, Event.CLICK_DATA_API, Selector.DISMISS, Alert.handleDismiss(new Alert())) - -const $ = getjQuery() - -/** - * ------------------------------------------------------------------------ - * jQuery - * ------------------------------------------------------------------------ - * add .alert to jQuery only if jQuery is present - */ - -/* istanbul ignore if */ -if ($) { - const JQUERY_NO_CONFLICT = $.fn[NAME] - $.fn[NAME] = Alert.jQueryInterface - $.fn[NAME].Constructor = Alert - $.fn[NAME].noConflict = () => { - $.fn[NAME] = JQUERY_NO_CONFLICT - return Alert.jQueryInterface - } -} - -export default Alert diff --git a/js/src/alert/alert.spec.js b/js/src/alert/alert.spec.js deleted file mode 100644 index 61d656bd0..000000000 --- a/js/src/alert/alert.spec.js +++ /dev/null @@ -1,173 +0,0 @@ -import Alert from './alert' -import { makeArray, getTransitionDurationFromElement } from '../util/index' - -/** Test helpers */ -import { getFixture, clearFixture, jQueryMock } from '../../tests/helpers/fixture' - -describe('Alert', () => { - let fixtureEl - - beforeAll(() => { - fixtureEl = getFixture() - }) - - afterEach(() => { - clearFixture() - }) - - it('should return version', () => { - expect(typeof Alert.VERSION).toEqual('string') - }) - - describe('data-api', () => { - it('should close an alert without instantiate it manually', () => { - fixtureEl.innerHTML = [ - '
', - ' ', - '
' - ].join('') - - const button = document.querySelector('button') - - button.click() - expect(makeArray(document.querySelectorAll('.alert')).length).toEqual(0) - }) - - it('should close an alert without instantiate it manually with the parent selector', () => { - fixtureEl.innerHTML = [ - '
', - ' ', - '
' - ].join('') - - const button = document.querySelector('button') - - button.click() - expect(makeArray(document.querySelectorAll('.alert')).length).toEqual(0) - }) - }) - - describe('close', () => { - it('should close an alert', done => { - const spy = jasmine.createSpy('spy', getTransitionDurationFromElement) - fixtureEl.innerHTML = '
' - - const alertEl = document.querySelector('.alert') - const alert = new Alert(alertEl) - - alertEl.addEventListener('closed.bs.alert', () => { - expect(makeArray(document.querySelectorAll('.alert')).length).toEqual(0) - expect(spy).not.toHaveBeenCalled() - done() - }) - - alert.close() - }) - - it('should close alert with fade class', done => { - fixtureEl.innerHTML = '
' - - const alertEl = document.querySelector('.alert') - const alert = new Alert(alertEl) - - alertEl.addEventListener('transitionend', () => { - expect().nothing() - }) - - alertEl.addEventListener('closed.bs.alert', () => { - expect(makeArray(document.querySelectorAll('.alert')).length).toEqual(0) - done() - }) - - alert.close() - }) - - it('should not remove alert if close event is prevented', done => { - fixtureEl.innerHTML = '
' - - const alertEl = document.querySelector('.alert') - const alert = new Alert(alertEl) - - const endTest = () => { - setTimeout(() => { - expect(alert._removeElement).not.toHaveBeenCalled() - done() - }, 10) - } - - spyOn(alert, '_removeElement') - - alertEl.addEventListener('close.bs.alert', event => { - event.preventDefault() - endTest() - }) - - alertEl.addEventListener('closed.bs.alert', () => { - endTest() - }) - - alert.close() - }) - }) - - describe('dispose', () => { - it('should dispose an alert', () => { - fixtureEl.innerHTML = '
' - - const alertEl = document.querySelector('.alert') - const alert = new Alert(alertEl) - - expect(Alert.getInstance(alertEl)).toBeDefined() - - alert.dispose() - - expect(Alert.getInstance(alertEl)).toBeNull() - }) - }) - - describe('jQueryInterface', () => { - it('should handle config passed and toggle existing alert', () => { - fixtureEl.innerHTML = '
' - - const alertEl = fixtureEl.querySelector('.alert') - const alert = new Alert(alertEl) - - spyOn(alert, 'close') - - jQueryMock.fn.alert = Alert.jQueryInterface - jQueryMock.elements = [alertEl] - - jQueryMock.fn.alert.call(jQueryMock, 'close') - - expect(alert.close).toHaveBeenCalled() - }) - - it('should create new alert instance and call close', () => { - fixtureEl.innerHTML = '
' - - const alertEl = fixtureEl.querySelector('.alert') - - jQueryMock.fn.alert = Alert.jQueryInterface - jQueryMock.elements = [alertEl] - - jQueryMock.fn.alert.call(jQueryMock, 'close') - - expect(Alert.getInstance(alertEl)).toBeDefined() - expect(fixtureEl.querySelector('.alert')).toBeNull() - }) - - it('should just create an alert instance without calling close', () => { - fixtureEl.innerHTML = '
' - - const alertEl = fixtureEl.querySelector('.alert') - - jQueryMock.fn.alert = Alert.jQueryInterface - jQueryMock.elements = [alertEl] - - jQueryMock.fn.alert.call(jQueryMock) - - expect(Alert.getInstance(alertEl)).toBeDefined() - expect(fixtureEl.querySelector('.alert')).not.toBeNull() - }) - }) -}) diff --git a/js/src/button.js b/js/src/button.js new file mode 100644 index 000000000..6edd5cb64 --- /dev/null +++ b/js/src/button.js @@ -0,0 +1,200 @@ +/** + * -------------------------------------------------------------------------- + * Bootstrap (v4.3.1): button.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + * -------------------------------------------------------------------------- + */ + +import { getjQuery } from './util/index' +import Data from './dom/data' +import EventHandler from './dom/event-handler' +import SelectorEngine from './dom/selector-engine' + +/** + * ------------------------------------------------------------------------ + * Constants + * ------------------------------------------------------------------------ + */ + +const NAME = 'button' +const VERSION = '4.3.1' +const DATA_KEY = 'bs.button' +const EVENT_KEY = `.${DATA_KEY}` +const DATA_API_KEY = '.data-api' + +const ClassName = { + ACTIVE: 'active', + BUTTON: 'btn', + FOCUS: 'focus' +} + +const Selector = { + DATA_TOGGLE_CARROT: '[data-toggle^="button"]', + DATA_TOGGLE: '[data-toggle="buttons"]', + INPUT: 'input:not([type="hidden"])', + ACTIVE: '.active', + BUTTON: '.btn' +} + +const Event = { + CLICK_DATA_API: `click${EVENT_KEY}${DATA_API_KEY}`, + FOCUS_DATA_API: `focus${EVENT_KEY}${DATA_API_KEY}`, + BLUR_DATA_API: `blur${EVENT_KEY}${DATA_API_KEY}` +} + +/** + * ------------------------------------------------------------------------ + * Class Definition + * ------------------------------------------------------------------------ + */ + +class Button { + constructor(element) { + this._element = element + Data.setData(element, DATA_KEY, this) + } + + // Getters + + static get VERSION() { + return VERSION + } + + // Public + + toggle() { + let triggerChangeEvent = true + let addAriaPressed = true + + const rootElement = SelectorEngine.closest( + this._element, + Selector.DATA_TOGGLE + ) + + if (rootElement) { + const input = SelectorEngine.findOne(Selector.INPUT, this._element) + + if (input && input.type === 'radio') { + if (input.checked && + this._element.classList.contains(ClassName.ACTIVE)) { + triggerChangeEvent = false + } else { + const activeElement = SelectorEngine.findOne(Selector.ACTIVE, rootElement) + + if (activeElement) { + activeElement.classList.remove(ClassName.ACTIVE) + } + } + + if (triggerChangeEvent) { + if (input.hasAttribute('disabled') || + rootElement.hasAttribute('disabled') || + input.classList.contains('disabled') || + rootElement.classList.contains('disabled')) { + return + } + + input.checked = !this._element.classList.contains(ClassName.ACTIVE) + EventHandler.trigger(input, 'change') + } + + input.focus() + addAriaPressed = false + } + } + + if (addAriaPressed) { + this._element.setAttribute('aria-pressed', + !this._element.classList.contains(ClassName.ACTIVE)) + } + + if (triggerChangeEvent) { + this._element.classList.toggle(ClassName.ACTIVE) + } + } + + dispose() { + Data.removeData(this._element, DATA_KEY) + this._element = null + } + + // Static + + static jQueryInterface(config) { + return this.each(function () { + let data = Data.getData(this, DATA_KEY) + + if (!data) { + data = new Button(this) + } + + if (config === 'toggle') { + data[config]() + } + }) + } + + static getInstance(element) { + return Data.getData(element, DATA_KEY) + } +} + +/** + * ------------------------------------------------------------------------ + * Data Api implementation + * ------------------------------------------------------------------------ + */ + +EventHandler.on(document, Event.CLICK_DATA_API, Selector.DATA_TOGGLE_CARROT, event => { + event.preventDefault() + + let button = event.target + if (!button.classList.contains(ClassName.BUTTON)) { + button = SelectorEngine.closest(button, Selector.BUTTON) + } + + let data = Data.getData(button, DATA_KEY) + if (!data) { + data = new Button(button) + } + + data.toggle() +}) + +EventHandler.on(document, Event.FOCUS_DATA_API, Selector.DATA_TOGGLE_CARROT, event => { + const button = SelectorEngine.closest(event.target, Selector.BUTTON) + + if (button) { + button.classList.add(ClassName.FOCUS) + } +}) + +EventHandler.on(document, Event.BLUR_DATA_API, Selector.DATA_TOGGLE_CARROT, event => { + const button = SelectorEngine.closest(event.target, Selector.BUTTON) + + if (button) { + button.classList.remove(ClassName.FOCUS) + } +}) + +const $ = getjQuery() + +/** + * ------------------------------------------------------------------------ + * jQuery + * ------------------------------------------------------------------------ + * add .button to jQuery only if jQuery is present + */ +/* istanbul ignore if */ +if ($) { + const JQUERY_NO_CONFLICT = $.fn[NAME] + $.fn[NAME] = Button.jQueryInterface + $.fn[NAME].Constructor = Button + + $.fn[NAME].noConflict = () => { + $.fn[NAME] = JQUERY_NO_CONFLICT + return Button.jQueryInterface + } +} + +export default Button diff --git a/js/src/button/button.js b/js/src/button/button.js deleted file mode 100644 index 4418ba6b8..000000000 --- a/js/src/button/button.js +++ /dev/null @@ -1,200 +0,0 @@ -/** - * -------------------------------------------------------------------------- - * Bootstrap (v4.3.1): button.js - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) - * -------------------------------------------------------------------------- - */ - -import { getjQuery } from '../util/index' -import Data from '../dom/data' -import EventHandler from '../dom/event-handler' -import SelectorEngine from '../dom/selector-engine' - -/** - * ------------------------------------------------------------------------ - * Constants - * ------------------------------------------------------------------------ - */ - -const NAME = 'button' -const VERSION = '4.3.1' -const DATA_KEY = 'bs.button' -const EVENT_KEY = `.${DATA_KEY}` -const DATA_API_KEY = '.data-api' - -const ClassName = { - ACTIVE: 'active', - BUTTON: 'btn', - FOCUS: 'focus' -} - -const Selector = { - DATA_TOGGLE_CARROT: '[data-toggle^="button"]', - DATA_TOGGLE: '[data-toggle="buttons"]', - INPUT: 'input:not([type="hidden"])', - ACTIVE: '.active', - BUTTON: '.btn' -} - -const Event = { - CLICK_DATA_API: `click${EVENT_KEY}${DATA_API_KEY}`, - FOCUS_DATA_API: `focus${EVENT_KEY}${DATA_API_KEY}`, - BLUR_DATA_API: `blur${EVENT_KEY}${DATA_API_KEY}` -} - -/** - * ------------------------------------------------------------------------ - * Class Definition - * ------------------------------------------------------------------------ - */ - -class Button { - constructor(element) { - this._element = element - Data.setData(element, DATA_KEY, this) - } - - // Getters - - static get VERSION() { - return VERSION - } - - // Public - - toggle() { - let triggerChangeEvent = true - let addAriaPressed = true - - const rootElement = SelectorEngine.closest( - this._element, - Selector.DATA_TOGGLE - ) - - if (rootElement) { - const input = SelectorEngine.findOne(Selector.INPUT, this._element) - - if (input && input.type === 'radio') { - if (input.checked && - this._element.classList.contains(ClassName.ACTIVE)) { - triggerChangeEvent = false - } else { - const activeElement = SelectorEngine.findOne(Selector.ACTIVE, rootElement) - - if (activeElement) { - activeElement.classList.remove(ClassName.ACTIVE) - } - } - - if (triggerChangeEvent) { - if (input.hasAttribute('disabled') || - rootElement.hasAttribute('disabled') || - input.classList.contains('disabled') || - rootElement.classList.contains('disabled')) { - return - } - - input.checked = !this._element.classList.contains(ClassName.ACTIVE) - EventHandler.trigger(input, 'change') - } - - input.focus() - addAriaPressed = false - } - } - - if (addAriaPressed) { - this._element.setAttribute('aria-pressed', - !this._element.classList.contains(ClassName.ACTIVE)) - } - - if (triggerChangeEvent) { - this._element.classList.toggle(ClassName.ACTIVE) - } - } - - dispose() { - Data.removeData(this._element, DATA_KEY) - this._element = null - } - - // Static - - static jQueryInterface(config) { - return this.each(function () { - let data = Data.getData(this, DATA_KEY) - - if (!data) { - data = new Button(this) - } - - if (config === 'toggle') { - data[config]() - } - }) - } - - static getInstance(element) { - return Data.getData(element, DATA_KEY) - } -} - -/** - * ------------------------------------------------------------------------ - * Data Api implementation - * ------------------------------------------------------------------------ - */ - -EventHandler.on(document, Event.CLICK_DATA_API, Selector.DATA_TOGGLE_CARROT, event => { - event.preventDefault() - - let button = event.target - if (!button.classList.contains(ClassName.BUTTON)) { - button = SelectorEngine.closest(button, Selector.BUTTON) - } - - let data = Data.getData(button, DATA_KEY) - if (!data) { - data = new Button(button) - } - - data.toggle() -}) - -EventHandler.on(document, Event.FOCUS_DATA_API, Selector.DATA_TOGGLE_CARROT, event => { - const button = SelectorEngine.closest(event.target, Selector.BUTTON) - - if (button) { - button.classList.add(ClassName.FOCUS) - } -}) - -EventHandler.on(document, Event.BLUR_DATA_API, Selector.DATA_TOGGLE_CARROT, event => { - const button = SelectorEngine.closest(event.target, Selector.BUTTON) - - if (button) { - button.classList.remove(ClassName.FOCUS) - } -}) - -const $ = getjQuery() - -/** - * ------------------------------------------------------------------------ - * jQuery - * ------------------------------------------------------------------------ - * add .button to jQuery only if jQuery is present - */ -/* istanbul ignore if */ -if ($) { - const JQUERY_NO_CONFLICT = $.fn[NAME] - $.fn[NAME] = Button.jQueryInterface - $.fn[NAME].Constructor = Button - - $.fn[NAME].noConflict = () => { - $.fn[NAME] = JQUERY_NO_CONFLICT - return Button.jQueryInterface - } -} - -export default Button diff --git a/js/src/button/button.spec.js b/js/src/button/button.spec.js deleted file mode 100644 index 622881185..000000000 --- a/js/src/button/button.spec.js +++ /dev/null @@ -1,292 +0,0 @@ -import Button from './button' -import EventHandler from '../dom/event-handler' - -/** Test helpers */ -import { - getFixture, - clearFixture, - createEvent, - jQueryMock -} from '../../tests/helpers/fixture' - -describe('Button', () => { - let fixtureEl - - beforeAll(() => { - fixtureEl = getFixture() - }) - - afterEach(() => { - clearFixture() - }) - - describe('VERSION', () => { - it('should return plugin version', () => { - expect(Button.VERSION).toEqual(jasmine.any(String)) - }) - }) - - describe('data-api', () => { - it('should toggle active class on click', () => { - fixtureEl.innerHTML = [ - '', - '' - ].join('') - - const btn = fixtureEl.querySelector('.btn') - const divTest = fixtureEl.querySelector('.test') - const btnTestParent = fixtureEl.querySelector('.testParent') - - expect(btn.classList.contains('active')).toEqual(false) - - btn.click() - - expect(btn.classList.contains('active')).toEqual(true) - - btn.click() - - expect(btn.classList.contains('active')).toEqual(false) - - divTest.click() - - expect(btnTestParent.classList.contains('active')).toEqual(true) - }) - - it('should trigger input change event when toggled button has input field', done => { - fixtureEl.innerHTML = [ - '
', - ' ', - '
' - ].join('') - - const input = fixtureEl.querySelector('input') - const label = fixtureEl.querySelector('label') - - input.addEventListener('change', () => { - expect().nothing() - done() - }) - - label.click() - }) - - it('should not trigger input change event when input already checked and button is active', () => { - fixtureEl.innerHTML = [ - '' - ].join('') - - const button = fixtureEl.querySelector('button') - - spyOn(EventHandler, 'trigger') - - button.click() - - expect(EventHandler.trigger).not.toHaveBeenCalled() - }) - - it('should remove active when an other radio button is clicked', () => { - fixtureEl.innerHTML = [ - '
', - ' ', - ' ', - ' ', - '
' - ].join('') - - const option1 = fixtureEl.querySelector('#option1') - const option2 = fixtureEl.querySelector('#option2') - - expect(option1.checked).toEqual(true) - expect(option1.parentElement.classList.contains('active')).toEqual(true) - - const clickEvent = createEvent('click') - - option2.dispatchEvent(clickEvent) - - expect(option1.checked).toEqual(false) - expect(option1.parentElement.classList.contains('active')).toEqual(false) - expect(option2.checked).toEqual(true) - expect(option2.parentElement.classList.contains('active')).toEqual(true) - }) - - it('should do nothing if the child is not an input', () => { - fixtureEl.innerHTML = [ - '
', - ' ', - ' ', - ' ', - '
' - ].join('') - - const option2 = fixtureEl.querySelector('#option2') - const clickEvent = createEvent('click') - - option2.dispatchEvent(clickEvent) - - expect().nothing() - }) - - it('should add focus class on focus event', () => { - fixtureEl.innerHTML = '' - - const btn = fixtureEl.querySelector('.btn') - const input = fixtureEl.querySelector('input') - - const focusEvent = createEvent('focus') - input.dispatchEvent(focusEvent) - - expect(btn.classList.contains('focus')).toEqual(true) - }) - - it('should not add focus class', () => { - fixtureEl.innerHTML = '' - - const btn = fixtureEl.querySelector('button') - const input = fixtureEl.querySelector('input') - - const focusEvent = createEvent('focus') - input.dispatchEvent(focusEvent) - - expect(btn.classList.contains('focus')).toEqual(false) - }) - - it('should remove focus class on blur event', () => { - fixtureEl.innerHTML = '' - - const btn = fixtureEl.querySelector('.btn') - const input = fixtureEl.querySelector('input') - - const focusEvent = createEvent('blur') - input.dispatchEvent(focusEvent) - - expect(btn.classList.contains('focus')).toEqual(false) - }) - - it('should not remove focus class on blur event', () => { - fixtureEl.innerHTML = '' - - const btn = fixtureEl.querySelector('button') - const input = fixtureEl.querySelector('input') - - const focusEvent = createEvent('blur') - input.dispatchEvent(focusEvent) - - expect(btn.classList.contains('focus')).toEqual(true) - }) - }) - - describe('toggle', () => { - it('should toggle aria-pressed', () => { - fixtureEl.innerHTML = '' - - const btnEl = fixtureEl.querySelector('.btn') - const button = new Button(btnEl) - - expect(btnEl.getAttribute('aria-pressed')).toEqual('false') - expect(btnEl.classList.contains('active')).toEqual(false) - - button.toggle() - - expect(btnEl.getAttribute('aria-pressed')).toEqual('true') - expect(btnEl.classList.contains('active')).toEqual(true) - }) - - it('should handle disabled attribute on non-button elements', () => { - fixtureEl.innerHTML = [ - '
', - ' ', - '
' - ].join('') - - const btnGroupEl = fixtureEl.querySelector('.btn-group') - const btnDanger = fixtureEl.querySelector('.btn-danger') - const input = fixtureEl.querySelector('input') - - const button = new Button(btnGroupEl) - - button.toggle() - - expect(btnDanger.hasAttribute('disabled')).toEqual(true) - expect(input.checked).toEqual(false) - }) - }) - - describe('dispose', () => { - it('should dispose a button', () => { - fixtureEl.innerHTML = '' - - const btnEl = fixtureEl.querySelector('.btn') - const button = new Button(btnEl) - - expect(Button.getInstance(btnEl)).toBeDefined() - - button.dispose() - - expect(Button.getInstance(btnEl)).toBeNull() - }) - }) - - describe('jQueryInterface', () => { - it('should handle config passed and toggle existing button', () => { - fixtureEl.innerHTML = '' - - const btnEl = fixtureEl.querySelector('.btn') - const button = new Button(btnEl) - - spyOn(button, 'toggle') - - jQueryMock.fn.button = Button.jQueryInterface - jQueryMock.elements = [btnEl] - - jQueryMock.fn.button.call(jQueryMock, 'toggle') - - expect(button.toggle).toHaveBeenCalled() - }) - - it('should create new button instance and call toggle', () => { - fixtureEl.innerHTML = '' - - const btnEl = fixtureEl.querySelector('.btn') - - jQueryMock.fn.button = Button.jQueryInterface - jQueryMock.elements = [btnEl] - - jQueryMock.fn.button.call(jQueryMock, 'toggle') - - expect(Button.getInstance(btnEl)).toBeDefined() - expect(btnEl.classList.contains('active')).toEqual(true) - }) - - it('should just create a button instance without calling toggle', () => { - fixtureEl.innerHTML = '' - - const btnEl = fixtureEl.querySelector('.btn') - - jQueryMock.fn.button = Button.jQueryInterface - jQueryMock.elements = [btnEl] - - jQueryMock.fn.button.call(jQueryMock) - - expect(Button.getInstance(btnEl)).toBeDefined() - expect(btnEl.classList.contains('active')).toEqual(false) - }) - }) -}) diff --git a/js/src/carousel.js b/js/src/carousel.js new file mode 100644 index 000000000..5034f1798 --- /dev/null +++ b/js/src/carousel.js @@ -0,0 +1,637 @@ +/** + * -------------------------------------------------------------------------- + * Bootstrap (v4.3.1): carousel.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + * -------------------------------------------------------------------------- + */ + +import { + getjQuery, + TRANSITION_END, + emulateTransitionEnd, + getElementFromSelector, + getTransitionDurationFromElement, + isVisible, + makeArray, + reflow, + triggerTransitionEnd, + typeCheckConfig +} from './util/index' +import Data from './dom/data' +import EventHandler from './dom/event-handler' +import Manipulator from './dom/manipulator' +import SelectorEngine from './dom/selector-engine' + +/** + * ------------------------------------------------------------------------ + * Constants + * ------------------------------------------------------------------------ + */ + +const NAME = 'carousel' +const VERSION = '4.3.1' +const DATA_KEY = 'bs.carousel' +const EVENT_KEY = `.${DATA_KEY}` +const DATA_API_KEY = '.data-api' +const ARROW_LEFT_KEYCODE = 37 // KeyboardEvent.which value for left arrow key +const ARROW_RIGHT_KEYCODE = 39 // KeyboardEvent.which value for right arrow key +const TOUCHEVENT_COMPAT_WAIT = 500 // Time for mouse compat events to fire after touch +const SWIPE_THRESHOLD = 40 + +const Default = { + interval: 5000, + keyboard: true, + slide: false, + pause: 'hover', + wrap: true, + touch: true +} + +const DefaultType = { + interval: '(number|boolean)', + keyboard: 'boolean', + slide: '(boolean|string)', + pause: '(string|boolean)', + wrap: 'boolean', + touch: 'boolean' +} + +const Direction = { + NEXT: 'next', + PREV: 'prev', + LEFT: 'left', + RIGHT: 'right' +} + +const Event = { + SLIDE: `slide${EVENT_KEY}`, + SLID: `slid${EVENT_KEY}`, + KEYDOWN: `keydown${EVENT_KEY}`, + MOUSEENTER: `mouseenter${EVENT_KEY}`, + MOUSELEAVE: `mouseleave${EVENT_KEY}`, + TOUCHSTART: `touchstart${EVENT_KEY}`, + TOUCHMOVE: `touchmove${EVENT_KEY}`, + TOUCHEND: `touchend${EVENT_KEY}`, + POINTERDOWN: `pointerdown${EVENT_KEY}`, + POINTERUP: `pointerup${EVENT_KEY}`, + DRAG_START: `dragstart${EVENT_KEY}`, + LOAD_DATA_API: `load${EVENT_KEY}${DATA_API_KEY}`, + CLICK_DATA_API: `click${EVENT_KEY}${DATA_API_KEY}` +} + +const ClassName = { + CAROUSEL: 'carousel', + ACTIVE: 'active', + SLIDE: 'slide', + RIGHT: 'carousel-item-right', + LEFT: 'carousel-item-left', + NEXT: 'carousel-item-next', + PREV: 'carousel-item-prev', + ITEM: 'carousel-item', + POINTER_EVENT: 'pointer-event' +} + +const Selector = { + ACTIVE: '.active', + ACTIVE_ITEM: '.active.carousel-item', + ITEM: '.carousel-item', + ITEM_IMG: '.carousel-item img', + NEXT_PREV: '.carousel-item-next, .carousel-item-prev', + INDICATORS: '.carousel-indicators', + DATA_SLIDE: '[data-slide], [data-slide-to]', + DATA_RIDE: '[data-ride="carousel"]' +} + +const PointerType = { + TOUCH: 'touch', + PEN: 'pen' +} + +/** + * ------------------------------------------------------------------------ + * Class Definition + * ------------------------------------------------------------------------ + */ +class Carousel { + constructor(element, config) { + this._items = null + this._interval = null + this._activeElement = null + this._isPaused = false + this._isSliding = false + this.touchTimeout = null + this.touchStartX = 0 + this.touchDeltaX = 0 + + this._config = this._getConfig(config) + this._element = element + this._indicatorsElement = SelectorEngine.findOne(Selector.INDICATORS, this._element) + this._touchSupported = 'ontouchstart' in document.documentElement || navigator.maxTouchPoints > 0 + this._pointerEvent = Boolean(window.PointerEvent || window.MSPointerEvent) + + this._addEventListeners() + Data.setData(element, DATA_KEY, this) + } + + // Getters + + static get VERSION() { + return VERSION + } + + static get Default() { + return Default + } + + // Public + + next() { + if (!this._isSliding) { + this._slide(Direction.NEXT) + } + } + + nextWhenVisible() { + // Don't call next when the page isn't visible + // or the carousel or its parent isn't visible + if (!document.hidden && isVisible(this._element)) { + this.next() + } + } + + prev() { + if (!this._isSliding) { + this._slide(Direction.PREV) + } + } + + pause(event) { + if (!event) { + this._isPaused = true + } + + if (SelectorEngine.findOne(Selector.NEXT_PREV, this._element)) { + triggerTransitionEnd(this._element) + this.cycle(true) + } + + clearInterval(this._interval) + this._interval = null + } + + cycle(event) { + if (!event) { + this._isPaused = false + } + + if (this._interval) { + clearInterval(this._interval) + this._interval = null + } + + if (this._config && this._config.interval && !this._isPaused) { + this._interval = setInterval( + (document.visibilityState ? this.nextWhenVisible : this.next).bind(this), + this._config.interval + ) + } + } + + to(index) { + this._activeElement = SelectorEngine.findOne(Selector.ACTIVE_ITEM, this._element) + const activeIndex = this._getItemIndex(this._activeElement) + + if (index > this._items.length - 1 || index < 0) { + return + } + + if (this._isSliding) { + EventHandler.one(this._element, Event.SLID, () => this.to(index)) + return + } + + if (activeIndex === index) { + this.pause() + this.cycle() + return + } + + const direction = index > activeIndex ? + Direction.NEXT : + Direction.PREV + + this._slide(direction, this._items[index]) + } + + dispose() { + EventHandler.off(this._element, EVENT_KEY) + Data.removeData(this._element, DATA_KEY) + + this._items = null + this._config = null + this._element = null + this._interval = null + this._isPaused = null + this._isSliding = null + this._activeElement = null + this._indicatorsElement = null + } + + // Private + + _getConfig(config) { + config = { + ...Default, + ...config + } + typeCheckConfig(NAME, config, DefaultType) + return config + } + + _handleSwipe() { + const absDeltax = Math.abs(this.touchDeltaX) + + if (absDeltax <= SWIPE_THRESHOLD) { + return + } + + const direction = absDeltax / this.touchDeltaX + + this.touchDeltaX = 0 + + // swipe left + if (direction > 0) { + this.prev() + } + + // swipe right + if (direction < 0) { + this.next() + } + } + + _addEventListeners() { + if (this._config.keyboard) { + EventHandler + .on(this._element, Event.KEYDOWN, event => this._keydown(event)) + } + + if (this._config.pause === 'hover') { + EventHandler + .on(this._element, Event.MOUSEENTER, event => this.pause(event)) + EventHandler + .on(this._element, Event.MOUSELEAVE, event => this.cycle(event)) + } + + if (this._config.touch && this._touchSupported) { + this._addTouchEventListeners() + } + } + + _addTouchEventListeners() { + const start = event => { + if (this._pointerEvent && PointerType[event.pointerType.toUpperCase()]) { + this.touchStartX = event.clientX + } else if (!this._pointerEvent) { + this.touchStartX = event.touches[0].clientX + } + } + + const move = event => { + // ensure swiping with one touch and not pinching + if (event.touches && event.touches.length > 1) { + this.touchDeltaX = 0 + } else { + this.touchDeltaX = event.touches[0].clientX - this.touchStartX + } + } + + const end = event => { + if (this._pointerEvent && PointerType[event.pointerType.toUpperCase()]) { + this.touchDeltaX = event.clientX - this.touchStartX + } + + this._handleSwipe() + if (this._config.pause === 'hover') { + // If it's a touch-enabled device, mouseenter/leave are fired as + // part of the mouse compatibility events on first tap - the carousel + // would stop cycling until user tapped out of it; + // here, we listen for touchend, explicitly pause the carousel + // (as if it's the second time we tap on it, mouseenter compat event + // is NOT fired) and after a timeout (to allow for mouse compatibility + // events to fire) we explicitly restart cycling + + this.pause() + if (this.touchTimeout) { + clearTimeout(this.touchTimeout) + } + + this.touchTimeout = setTimeout(event => this.cycle(event), TOUCHEVENT_COMPAT_WAIT + this._config.interval) + } + } + + makeArray(SelectorEngine.find(Selector.ITEM_IMG, this._element)).forEach(itemImg => { + EventHandler.on(itemImg, Event.DRAG_START, e => e.preventDefault()) + }) + + if (this._pointerEvent) { + EventHandler.on(this._element, Event.POINTERDOWN, event => start(event)) + EventHandler.on(this._element, Event.POINTERUP, event => end(event)) + + this._element.classList.add(ClassName.POINTER_EVENT) + } else { + EventHandler.on(this._element, Event.TOUCHSTART, event => start(event)) + EventHandler.on(this._element, Event.TOUCHMOVE, event => move(event)) + EventHandler.on(this._element, Event.TOUCHEND, event => end(event)) + } + } + + _keydown(event) { + if (/input|textarea/i.test(event.target.tagName)) { + return + } + + switch (event.which) { + case ARROW_LEFT_KEYCODE: + event.preventDefault() + this.prev() + break + case ARROW_RIGHT_KEYCODE: + event.preventDefault() + this.next() + break + default: + } + } + + _getItemIndex(element) { + this._items = element && element.parentNode ? + makeArray(SelectorEngine.find(Selector.ITEM, element.parentNode)) : + [] + + return this._items.indexOf(element) + } + + _getItemByDirection(direction, activeElement) { + const isNextDirection = direction === Direction.NEXT + const isPrevDirection = direction === Direction.PREV + const activeIndex = this._getItemIndex(activeElement) + const lastItemIndex = this._items.length - 1 + const isGoingToWrap = (isPrevDirection && activeIndex === 0) || + (isNextDirection && activeIndex === lastItemIndex) + + if (isGoingToWrap && !this._config.wrap) { + return activeElement + } + + const delta = direction === Direction.PREV ? -1 : 1 + const itemIndex = (activeIndex + delta) % this._items.length + + return itemIndex === -1 ? + this._items[this._items.length - 1] : + this._items[itemIndex] + } + + _triggerSlideEvent(relatedTarget, eventDirectionName) { + const targetIndex = this._getItemIndex(relatedTarget) + const fromIndex = this._getItemIndex(SelectorEngine.findOne(Selector.ACTIVE_ITEM, this._element)) + + return EventHandler.trigger(this._element, Event.SLIDE, { + relatedTarget, + direction: eventDirectionName, + from: fromIndex, + to: targetIndex + }) + } + + _setActiveIndicatorElement(element) { + if (this._indicatorsElement) { + const indicators = SelectorEngine.find(Selector.ACTIVE, this._indicatorsElement) + for (let i = 0; i < indicators.length; i++) { + indicators[i].classList.remove(ClassName.ACTIVE) + } + + const nextIndicator = this._indicatorsElement.children[ + this._getItemIndex(element) + ] + + if (nextIndicator) { + nextIndicator.classList.add(ClassName.ACTIVE) + } + } + } + + _slide(direction, element) { + const activeElement = SelectorEngine.findOne(Selector.ACTIVE_ITEM, this._element) + const activeElementIndex = this._getItemIndex(activeElement) + const nextElement = element || (activeElement && + this._getItemByDirection(direction, activeElement)) + + const nextElementIndex = this._getItemIndex(nextElement) + const isCycling = Boolean(this._interval) + + let directionalClassName + let orderClassName + let eventDirectionName + + if (direction === Direction.NEXT) { + directionalClassName = ClassName.LEFT + orderClassName = ClassName.NEXT + eventDirectionName = Direction.LEFT + } else { + directionalClassName = ClassName.RIGHT + orderClassName = ClassName.PREV + eventDirectionName = Direction.RIGHT + } + + if (nextElement && nextElement.classList.contains(ClassName.ACTIVE)) { + this._isSliding = false + return + } + + const slideEvent = this._triggerSlideEvent(nextElement, eventDirectionName) + if (slideEvent.defaultPrevented) { + return + } + + if (!activeElement || !nextElement) { + // Some weirdness is happening, so we bail + return + } + + this._isSliding = true + + if (isCycling) { + this.pause() + } + + this._setActiveIndicatorElement(nextElement) + + if (this._element.classList.contains(ClassName.SLIDE)) { + nextElement.classList.add(orderClassName) + + reflow(nextElement) + + activeElement.classList.add(directionalClassName) + nextElement.classList.add(directionalClassName) + + const nextElementInterval = parseInt(nextElement.getAttribute('data-interval'), 10) + if (nextElementInterval) { + this._config.defaultInterval = this._config.defaultInterval || this._config.interval + this._config.interval = nextElementInterval + } else { + this._config.interval = this._config.defaultInterval || this._config.interval + } + + const transitionDuration = getTransitionDurationFromElement(activeElement) + + EventHandler + .one(activeElement, TRANSITION_END, () => { + nextElement.classList.remove(directionalClassName) + nextElement.classList.remove(orderClassName) + nextElement.classList.add(ClassName.ACTIVE) + + activeElement.classList.remove(ClassName.ACTIVE) + activeElement.classList.remove(orderClassName) + activeElement.classList.remove(directionalClassName) + + this._isSliding = false + + setTimeout(() => { + EventHandler.trigger(this._element, Event.SLID, { + relatedTarget: nextElement, + direction: eventDirectionName, + from: activeElementIndex, + to: nextElementIndex + }) + }, 0) + }) + + emulateTransitionEnd(activeElement, transitionDuration) + } else { + activeElement.classList.remove(ClassName.ACTIVE) + nextElement.classList.add(ClassName.ACTIVE) + + this._isSliding = false + EventHandler.trigger(this._element, Event.SLID, { + relatedTarget: nextElement, + direction: eventDirectionName, + from: activeElementIndex, + to: nextElementIndex + }) + } + + if (isCycling) { + this.cycle() + } + } + + // Static + + static carouselInterface(element, config) { + let data = Data.getData(element, DATA_KEY) + let _config = { + ...Default, + ...Manipulator.getDataAttributes(element) + } + + if (typeof config === 'object') { + _config = { + ..._config, + ...config + } + } + + const action = typeof config === 'string' ? config : _config.slide + + if (!data) { + data = new Carousel(element, _config) + } + + if (typeof config === 'number') { + data.to(config) + } else if (typeof action === 'string') { + if (typeof data[action] === 'undefined') { + throw new TypeError(`No method named "${action}"`) + } + + data[action]() + } else if (_config.interval && _config.ride) { + data.pause() + data.cycle() + } + } + + static jQueryInterface(config) { + return this.each(function () { + Carousel.carouselInterface(this, config) + }) + } + + static dataApiClickHandler(event) { + const target = getElementFromSelector(this) + + if (!target || !target.classList.contains(ClassName.CAROUSEL)) { + return + } + + const config = { + ...Manipulator.getDataAttributes(target), + ...Manipulator.getDataAttributes(this) + } + const slideIndex = this.getAttribute('data-slide-to') + + if (slideIndex) { + config.interval = false + } + + Carousel.carouselInterface(target, config) + + if (slideIndex) { + Data.getData(target, DATA_KEY).to(slideIndex) + } + + event.preventDefault() + } + + static getInstance(element) { + return Data.getData(element, DATA_KEY) + } +} + +/** + * ------------------------------------------------------------------------ + * Data Api implementation + * ------------------------------------------------------------------------ + */ + +EventHandler + .on(document, Event.CLICK_DATA_API, Selector.DATA_SLIDE, Carousel.dataApiClickHandler) + +EventHandler.on(window, Event.LOAD_DATA_API, () => { + const carousels = makeArray(SelectorEngine.find(Selector.DATA_RIDE)) + for (let i = 0, len = carousels.length; i < len; i++) { + Carousel.carouselInterface(carousels[i], Data.getData(carousels[i], DATA_KEY)) + } +}) + +const $ = getjQuery() + +/** + * ------------------------------------------------------------------------ + * jQuery + * ------------------------------------------------------------------------ + * add .carousel to jQuery only if jQuery is present + */ +/* istanbul ignore if */ +if ($) { + const JQUERY_NO_CONFLICT = $.fn[NAME] + $.fn[NAME] = Carousel.jQueryInterface + $.fn[NAME].Constructor = Carousel + $.fn[NAME].noConflict = () => { + $.fn[NAME] = JQUERY_NO_CONFLICT + return Carousel.jQueryInterface + } +} + +export default Carousel diff --git a/js/src/carousel/carousel.js b/js/src/carousel/carousel.js deleted file mode 100644 index 723bf57c6..000000000 --- a/js/src/carousel/carousel.js +++ /dev/null @@ -1,637 +0,0 @@ -/** - * -------------------------------------------------------------------------- - * Bootstrap (v4.3.1): carousel.js - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) - * -------------------------------------------------------------------------- - */ - -import { - getjQuery, - TRANSITION_END, - emulateTransitionEnd, - getElementFromSelector, - getTransitionDurationFromElement, - isVisible, - makeArray, - reflow, - triggerTransitionEnd, - typeCheckConfig -} from '../util/index' -import Data from '../dom/data' -import EventHandler from '../dom/event-handler' -import Manipulator from '../dom/manipulator' -import SelectorEngine from '../dom/selector-engine' - -/** - * ------------------------------------------------------------------------ - * Constants - * ------------------------------------------------------------------------ - */ - -const NAME = 'carousel' -const VERSION = '4.3.1' -const DATA_KEY = 'bs.carousel' -const EVENT_KEY = `.${DATA_KEY}` -const DATA_API_KEY = '.data-api' -const ARROW_LEFT_KEYCODE = 37 // KeyboardEvent.which value for left arrow key -const ARROW_RIGHT_KEYCODE = 39 // KeyboardEvent.which value for right arrow key -const TOUCHEVENT_COMPAT_WAIT = 500 // Time for mouse compat events to fire after touch -const SWIPE_THRESHOLD = 40 - -const Default = { - interval: 5000, - keyboard: true, - slide: false, - pause: 'hover', - wrap: true, - touch: true -} - -const DefaultType = { - interval: '(number|boolean)', - keyboard: 'boolean', - slide: '(boolean|string)', - pause: '(string|boolean)', - wrap: 'boolean', - touch: 'boolean' -} - -const Direction = { - NEXT: 'next', - PREV: 'prev', - LEFT: 'left', - RIGHT: 'right' -} - -const Event = { - SLIDE: `slide${EVENT_KEY}`, - SLID: `slid${EVENT_KEY}`, - KEYDOWN: `keydown${EVENT_KEY}`, - MOUSEENTER: `mouseenter${EVENT_KEY}`, - MOUSELEAVE: `mouseleave${EVENT_KEY}`, - TOUCHSTART: `touchstart${EVENT_KEY}`, - TOUCHMOVE: `touchmove${EVENT_KEY}`, - TOUCHEND: `touchend${EVENT_KEY}`, - POINTERDOWN: `pointerdown${EVENT_KEY}`, - POINTERUP: `pointerup${EVENT_KEY}`, - DRAG_START: `dragstart${EVENT_KEY}`, - LOAD_DATA_API: `load${EVENT_KEY}${DATA_API_KEY}`, - CLICK_DATA_API: `click${EVENT_KEY}${DATA_API_KEY}` -} - -const ClassName = { - CAROUSEL: 'carousel', - ACTIVE: 'active', - SLIDE: 'slide', - RIGHT: 'carousel-item-right', - LEFT: 'carousel-item-left', - NEXT: 'carousel-item-next', - PREV: 'carousel-item-prev', - ITEM: 'carousel-item', - POINTER_EVENT: 'pointer-event' -} - -const Selector = { - ACTIVE: '.active', - ACTIVE_ITEM: '.active.carousel-item', - ITEM: '.carousel-item', - ITEM_IMG: '.carousel-item img', - NEXT_PREV: '.carousel-item-next, .carousel-item-prev', - INDICATORS: '.carousel-indicators', - DATA_SLIDE: '[data-slide], [data-slide-to]', - DATA_RIDE: '[data-ride="carousel"]' -} - -const PointerType = { - TOUCH: 'touch', - PEN: 'pen' -} - -/** - * ------------------------------------------------------------------------ - * Class Definition - * ------------------------------------------------------------------------ - */ -class Carousel { - constructor(element, config) { - this._items = null - this._interval = null - this._activeElement = null - this._isPaused = false - this._isSliding = false - this.touchTimeout = null - this.touchStartX = 0 - this.touchDeltaX = 0 - - this._config = this._getConfig(config) - this._element = element - this._indicatorsElement = SelectorEngine.findOne(Selector.INDICATORS, this._element) - this._touchSupported = 'ontouchstart' in document.documentElement || navigator.maxTouchPoints > 0 - this._pointerEvent = Boolean(window.PointerEvent || window.MSPointerEvent) - - this._addEventListeners() - Data.setData(element, DATA_KEY, this) - } - - // Getters - - static get VERSION() { - return VERSION - } - - static get Default() { - return Default - } - - // Public - - next() { - if (!this._isSliding) { - this._slide(Direction.NEXT) - } - } - - nextWhenVisible() { - // Don't call next when the page isn't visible - // or the carousel or its parent isn't visible - if (!document.hidden && isVisible(this._element)) { - this.next() - } - } - - prev() { - if (!this._isSliding) { - this._slide(Direction.PREV) - } - } - - pause(event) { - if (!event) { - this._isPaused = true - } - - if (SelectorEngine.findOne(Selector.NEXT_PREV, this._element)) { - triggerTransitionEnd(this._element) - this.cycle(true) - } - - clearInterval(this._interval) - this._interval = null - } - - cycle(event) { - if (!event) { - this._isPaused = false - } - - if (this._interval) { - clearInterval(this._interval) - this._interval = null - } - - if (this._config && this._config.interval && !this._isPaused) { - this._interval = setInterval( - (document.visibilityState ? this.nextWhenVisible : this.next).bind(this), - this._config.interval - ) - } - } - - to(index) { - this._activeElement = SelectorEngine.findOne(Selector.ACTIVE_ITEM, this._element) - const activeIndex = this._getItemIndex(this._activeElement) - - if (index > this._items.length - 1 || index < 0) { - return - } - - if (this._isSliding) { - EventHandler.one(this._element, Event.SLID, () => this.to(index)) - return - } - - if (activeIndex === index) { - this.pause() - this.cycle() - return - } - - const direction = index > activeIndex ? - Direction.NEXT : - Direction.PREV - - this._slide(direction, this._items[index]) - } - - dispose() { - EventHandler.off(this._element, EVENT_KEY) - Data.removeData(this._element, DATA_KEY) - - this._items = null - this._config = null - this._element = null - this._interval = null - this._isPaused = null - this._isSliding = null - this._activeElement = null - this._indicatorsElement = null - } - - // Private - - _getConfig(config) { - config = { - ...Default, - ...config - } - typeCheckConfig(NAME, config, DefaultType) - return config - } - - _handleSwipe() { - const absDeltax = Math.abs(this.touchDeltaX) - - if (absDeltax <= SWIPE_THRESHOLD) { - return - } - - const direction = absDeltax / this.touchDeltaX - - this.touchDeltaX = 0 - - // swipe left - if (direction > 0) { - this.prev() - } - - // swipe right - if (direction < 0) { - this.next() - } - } - - _addEventListeners() { - if (this._config.keyboard) { - EventHandler - .on(this._element, Event.KEYDOWN, event => this._keydown(event)) - } - - if (this._config.pause === 'hover') { - EventHandler - .on(this._element, Event.MOUSEENTER, event => this.pause(event)) - EventHandler - .on(this._element, Event.MOUSELEAVE, event => this.cycle(event)) - } - - if (this._config.touch && this._touchSupported) { - this._addTouchEventListeners() - } - } - - _addTouchEventListeners() { - const start = event => { - if (this._pointerEvent && PointerType[event.pointerType.toUpperCase()]) { - this.touchStartX = event.clientX - } else if (!this._pointerEvent) { - this.touchStartX = event.touches[0].clientX - } - } - - const move = event => { - // ensure swiping with one touch and not pinching - if (event.touches && event.touches.length > 1) { - this.touchDeltaX = 0 - } else { - this.touchDeltaX = event.touches[0].clientX - this.touchStartX - } - } - - const end = event => { - if (this._pointerEvent && PointerType[event.pointerType.toUpperCase()]) { - this.touchDeltaX = event.clientX - this.touchStartX - } - - this._handleSwipe() - if (this._config.pause === 'hover') { - // If it's a touch-enabled device, mouseenter/leave are fired as - // part of the mouse compatibility events on first tap - the carousel - // would stop cycling until user tapped out of it; - // here, we listen for touchend, explicitly pause the carousel - // (as if it's the second time we tap on it, mouseenter compat event - // is NOT fired) and after a timeout (to allow for mouse compatibility - // events to fire) we explicitly restart cycling - - this.pause() - if (this.touchTimeout) { - clearTimeout(this.touchTimeout) - } - - this.touchTimeout = setTimeout(event => this.cycle(event), TOUCHEVENT_COMPAT_WAIT + this._config.interval) - } - } - - makeArray(SelectorEngine.find(Selector.ITEM_IMG, this._element)).forEach(itemImg => { - EventHandler.on(itemImg, Event.DRAG_START, e => e.preventDefault()) - }) - - if (this._pointerEvent) { - EventHandler.on(this._element, Event.POINTERDOWN, event => start(event)) - EventHandler.on(this._element, Event.POINTERUP, event => end(event)) - - this._element.classList.add(ClassName.POINTER_EVENT) - } else { - EventHandler.on(this._element, Event.TOUCHSTART, event => start(event)) - EventHandler.on(this._element, Event.TOUCHMOVE, event => move(event)) - EventHandler.on(this._element, Event.TOUCHEND, event => end(event)) - } - } - - _keydown(event) { - if (/input|textarea/i.test(event.target.tagName)) { - return - } - - switch (event.which) { - case ARROW_LEFT_KEYCODE: - event.preventDefault() - this.prev() - break - case ARROW_RIGHT_KEYCODE: - event.preventDefault() - this.next() - break - default: - } - } - - _getItemIndex(element) { - this._items = element && element.parentNode ? - makeArray(SelectorEngine.find(Selector.ITEM, element.parentNode)) : - [] - - return this._items.indexOf(element) - } - - _getItemByDirection(direction, activeElement) { - const isNextDirection = direction === Direction.NEXT - const isPrevDirection = direction === Direction.PREV - const activeIndex = this._getItemIndex(activeElement) - const lastItemIndex = this._items.length - 1 - const isGoingToWrap = (isPrevDirection && activeIndex === 0) || - (isNextDirection && activeIndex === lastItemIndex) - - if (isGoingToWrap && !this._config.wrap) { - return activeElement - } - - const delta = direction === Direction.PREV ? -1 : 1 - const itemIndex = (activeIndex + delta) % this._items.length - - return itemIndex === -1 ? - this._items[this._items.length - 1] : - this._items[itemIndex] - } - - _triggerSlideEvent(relatedTarget, eventDirectionName) { - const targetIndex = this._getItemIndex(relatedTarget) - const fromIndex = this._getItemIndex(SelectorEngine.findOne(Selector.ACTIVE_ITEM, this._element)) - - return EventHandler.trigger(this._element, Event.SLIDE, { - relatedTarget, - direction: eventDirectionName, - from: fromIndex, - to: targetIndex - }) - } - - _setActiveIndicatorElement(element) { - if (this._indicatorsElement) { - const indicators = SelectorEngine.find(Selector.ACTIVE, this._indicatorsElement) - for (let i = 0; i < indicators.length; i++) { - indicators[i].classList.remove(ClassName.ACTIVE) - } - - const nextIndicator = this._indicatorsElement.children[ - this._getItemIndex(element) - ] - - if (nextIndicator) { - nextIndicator.classList.add(ClassName.ACTIVE) - } - } - } - - _slide(direction, element) { - const activeElement = SelectorEngine.findOne(Selector.ACTIVE_ITEM, this._element) - const activeElementIndex = this._getItemIndex(activeElement) - const nextElement = element || (activeElement && - this._getItemByDirection(direction, activeElement)) - - const nextElementIndex = this._getItemIndex(nextElement) - const isCycling = Boolean(this._interval) - - let directionalClassName - let orderClassName - let eventDirectionName - - if (direction === Direction.NEXT) { - directionalClassName = ClassName.LEFT - orderClassName = ClassName.NEXT - eventDirectionName = Direction.LEFT - } else { - directionalClassName = ClassName.RIGHT - orderClassName = ClassName.PREV - eventDirectionName = Direction.RIGHT - } - - if (nextElement && nextElement.classList.contains(ClassName.ACTIVE)) { - this._isSliding = false - return - } - - const slideEvent = this._triggerSlideEvent(nextElement, eventDirectionName) - if (slideEvent.defaultPrevented) { - return - } - - if (!activeElement || !nextElement) { - // Some weirdness is happening, so we bail - return - } - - this._isSliding = true - - if (isCycling) { - this.pause() - } - - this._setActiveIndicatorElement(nextElement) - - if (this._element.classList.contains(ClassName.SLIDE)) { - nextElement.classList.add(orderClassName) - - reflow(nextElement) - - activeElement.classList.add(directionalClassName) - nextElement.classList.add(directionalClassName) - - const nextElementInterval = parseInt(nextElement.getAttribute('data-interval'), 10) - if (nextElementInterval) { - this._config.defaultInterval = this._config.defaultInterval || this._config.interval - this._config.interval = nextElementInterval - } else { - this._config.interval = this._config.defaultInterval || this._config.interval - } - - const transitionDuration = getTransitionDurationFromElement(activeElement) - - EventHandler - .one(activeElement, TRANSITION_END, () => { - nextElement.classList.remove(directionalClassName) - nextElement.classList.remove(orderClassName) - nextElement.classList.add(ClassName.ACTIVE) - - activeElement.classList.remove(ClassName.ACTIVE) - activeElement.classList.remove(orderClassName) - activeElement.classList.remove(directionalClassName) - - this._isSliding = false - - setTimeout(() => { - EventHandler.trigger(this._element, Event.SLID, { - relatedTarget: nextElement, - direction: eventDirectionName, - from: activeElementIndex, - to: nextElementIndex - }) - }, 0) - }) - - emulateTransitionEnd(activeElement, transitionDuration) - } else { - activeElement.classList.remove(ClassName.ACTIVE) - nextElement.classList.add(ClassName.ACTIVE) - - this._isSliding = false - EventHandler.trigger(this._element, Event.SLID, { - relatedTarget: nextElement, - direction: eventDirectionName, - from: activeElementIndex, - to: nextElementIndex - }) - } - - if (isCycling) { - this.cycle() - } - } - - // Static - - static carouselInterface(element, config) { - let data = Data.getData(element, DATA_KEY) - let _config = { - ...Default, - ...Manipulator.getDataAttributes(element) - } - - if (typeof config === 'object') { - _config = { - ..._config, - ...config - } - } - - const action = typeof config === 'string' ? config : _config.slide - - if (!data) { - data = new Carousel(element, _config) - } - - if (typeof config === 'number') { - data.to(config) - } else if (typeof action === 'string') { - if (typeof data[action] === 'undefined') { - throw new TypeError(`No method named "${action}"`) - } - - data[action]() - } else if (_config.interval && _config.ride) { - data.pause() - data.cycle() - } - } - - static jQueryInterface(config) { - return this.each(function () { - Carousel.carouselInterface(this, config) - }) - } - - static dataApiClickHandler(event) { - const target = getElementFromSelector(this) - - if (!target || !target.classList.contains(ClassName.CAROUSEL)) { - return - } - - const config = { - ...Manipulator.getDataAttributes(target), - ...Manipulator.getDataAttributes(this) - } - const slideIndex = this.getAttribute('data-slide-to') - - if (slideIndex) { - config.interval = false - } - - Carousel.carouselInterface(target, config) - - if (slideIndex) { - Data.getData(target, DATA_KEY).to(slideIndex) - } - - event.preventDefault() - } - - static getInstance(element) { - return Data.getData(element, DATA_KEY) - } -} - -/** - * ------------------------------------------------------------------------ - * Data Api implementation - * ------------------------------------------------------------------------ - */ - -EventHandler - .on(document, Event.CLICK_DATA_API, Selector.DATA_SLIDE, Carousel.dataApiClickHandler) - -EventHandler.on(window, Event.LOAD_DATA_API, () => { - const carousels = makeArray(SelectorEngine.find(Selector.DATA_RIDE)) - for (let i = 0, len = carousels.length; i < len; i++) { - Carousel.carouselInterface(carousels[i], Data.getData(carousels[i], DATA_KEY)) - } -}) - -const $ = getjQuery() - -/** - * ------------------------------------------------------------------------ - * jQuery - * ------------------------------------------------------------------------ - * add .carousel to jQuery only if jQuery is present - */ -/* istanbul ignore if */ -if ($) { - const JQUERY_NO_CONFLICT = $.fn[NAME] - $.fn[NAME] = Carousel.jQueryInterface - $.fn[NAME].Constructor = Carousel - $.fn[NAME].noConflict = () => { - $.fn[NAME] = JQUERY_NO_CONFLICT - return Carousel.jQueryInterface - } -} - -export default Carousel diff --git a/js/src/carousel/carousel.spec.js b/js/src/carousel/carousel.spec.js deleted file mode 100644 index 4c13b6d22..000000000 --- a/js/src/carousel/carousel.spec.js +++ /dev/null @@ -1,1201 +0,0 @@ -import Carousel from './carousel' -import EventHandler from '../dom/event-handler' - -/** Test helpers */ -import { getFixture, clearFixture, createEvent, jQueryMock } from '../../tests/helpers/fixture' - -describe('Carousel', () => { - const { Simulator, PointerEvent, MSPointerEvent } = window - const originWinPointerEvent = PointerEvent || MSPointerEvent - const supportPointerEvent = Boolean(PointerEvent || MSPointerEvent) - - window.MSPointerEvent = null - const cssStyleCarousel = '.carousel.pointer-event { -ms-touch-action: none; touch-action: none; }' - - const stylesCarousel = document.createElement('style') - stylesCarousel.type = 'text/css' - stylesCarousel.appendChild(document.createTextNode(cssStyleCarousel)) - - const clearPointerEvents = () => { - window.PointerEvent = null - } - - const restorePointerEvents = () => { - window.PointerEvent = originWinPointerEvent - } - - let fixtureEl - - beforeAll(() => { - fixtureEl = getFixture() - }) - - afterEach(() => { - clearFixture() - }) - - describe('VERSION', () => { - it('should return plugin version', () => { - expect(Carousel.VERSION).toEqual(jasmine.any(String)) - }) - }) - - describe('Default', () => { - it('should return plugin default config', () => { - expect(Carousel.Default).toEqual(jasmine.any(Object)) - }) - }) - - describe('constructor', () => { - it('should go to next item if right arrow key is pressed', done => { - fixtureEl.innerHTML = [ - '' - ].join('') - - const carouselEl = fixtureEl.querySelector('#myCarousel') - const carousel = new Carousel(carouselEl, { - keyboard: true - }) - - spyOn(carousel, '_keydown').and.callThrough() - - carouselEl.addEventListener('slid.bs.carousel', () => { - expect(fixtureEl.querySelector('.active')).toEqual(fixtureEl.querySelector('#item2')) - expect(carousel._keydown).toHaveBeenCalled() - done() - }) - - const keyDown = createEvent('keydown') - keyDown.which = 39 - - carouselEl.dispatchEvent(keyDown) - }) - - it('should go to previous item if left arrow key is pressed', done => { - fixtureEl.innerHTML = [ - '' - ].join('') - - const carouselEl = fixtureEl.querySelector('#myCarousel') - const carousel = new Carousel(carouselEl, { - keyboard: true - }) - - spyOn(carousel, '_keydown').and.callThrough() - - carouselEl.addEventListener('slid.bs.carousel', () => { - expect(fixtureEl.querySelector('.active')).toEqual(fixtureEl.querySelector('#item1')) - expect(carousel._keydown).toHaveBeenCalled() - done() - }) - - const keyDown = createEvent('keydown') - keyDown.which = 37 - - carouselEl.dispatchEvent(keyDown) - }) - - it('should not prevent keydown if key is not ARROW_LEFT or ARROW_RIGHT', done => { - fixtureEl.innerHTML = [ - '' - ].join('') - - const carouselEl = fixtureEl.querySelector('#myCarousel') - const carousel = new Carousel(carouselEl, { - keyboard: true - }) - - spyOn(carousel, '_keydown').and.callThrough() - - carouselEl.addEventListener('keydown', event => { - expect(carousel._keydown).toHaveBeenCalled() - expect(event.defaultPrevented).toEqual(false) - done() - }) - - const keyDown = createEvent('keydown') - keyDown.which = 40 - - carouselEl.dispatchEvent(keyDown) - }) - - it('should ignore keyboard events within s and ', - ' ', - ' ', - ' ', - ' ', - '' - ].join('') - - const carouselEl = fixtureEl.querySelector('#myCarousel') - const input = fixtureEl.querySelector('input') - const textarea = fixtureEl.querySelector('textarea') - const carousel = new Carousel(carouselEl, { - keyboard: true - }) - - const spyKeyDown = spyOn(carousel, '_keydown').and.callThrough() - const spyPrev = spyOn(carousel, 'prev') - const spyNext = spyOn(carousel, 'next') - - const keyDown = createEvent('keydown', { bubbles: true, cancelable: true }) - keyDown.which = 39 - Object.defineProperty(keyDown, 'target', { - value: input, - writable: true, - configurable: true - }) - - input.dispatchEvent(keyDown) - - expect(spyKeyDown).toHaveBeenCalled() - expect(spyPrev).not.toHaveBeenCalled() - expect(spyNext).not.toHaveBeenCalled() - - spyKeyDown.calls.reset() - spyPrev.calls.reset() - spyNext.calls.reset() - - Object.defineProperty(keyDown, 'target', { - value: textarea - }) - textarea.dispatchEvent(keyDown) - - expect(spyKeyDown).toHaveBeenCalled() - expect(spyPrev).not.toHaveBeenCalled() - expect(spyNext).not.toHaveBeenCalled() - }) - - it('should wrap around from end to start when wrap option is true', done => { - fixtureEl.innerHTML = [ - '' - ].join('') - - const carouselEl = fixtureEl.querySelector('#myCarousel') - const carousel = new Carousel(carouselEl, { wrap: true }) - const getActiveId = () => { - return carouselEl.querySelector('.carousel-item.active').getAttribute('id') - } - - carouselEl.addEventListener('slid.bs.carousel', e => { - const activeId = getActiveId() - - if (activeId === 'two') { - carousel.next() - return - } - - if (activeId === 'three') { - carousel.next() - return - } - - if (activeId === 'one') { - // carousel wrapped around and slid from 3rd to 1st slide - expect(activeId).toEqual('one') - expect(e.from + 1).toEqual(3) - done() - } - }) - - carousel.next() - }) - - it('should stay at the start when the prev method is called and wrap is false', done => { - fixtureEl.innerHTML = [ - '' - ].join('') - - const carouselEl = fixtureEl.querySelector('#myCarousel') - const firstElement = fixtureEl.querySelector('#one') - const carousel = new Carousel(carouselEl, { wrap: false }) - - carouselEl.addEventListener('slid.bs.carousel', () => { - throw new Error('carousel slid when it should not have slid') - }) - - carousel.prev() - - setTimeout(() => { - expect(firstElement.classList.contains('active')).toEqual(true) - done() - }, 10) - }) - - it('should not add touch event listeners if touch = false', () => { - fixtureEl.innerHTML = '
' - - const carouselEl = fixtureEl.querySelector('div') - - spyOn(Carousel.prototype, '_addTouchEventListeners') - - const carousel = new Carousel(carouselEl, { - touch: false - }) - - expect(carousel._addTouchEventListeners).not.toHaveBeenCalled() - }) - - it('should not add touch event listeners if touch supported = false', () => { - fixtureEl.innerHTML = '
' - - const carouselEl = fixtureEl.querySelector('div') - - const carousel = new Carousel(carouselEl) - - EventHandler.off(carouselEl, '.bs-carousel') - carousel._touchSupported = false - - spyOn(carousel, '_addTouchEventListeners') - - carousel._addEventListeners() - - expect(carousel._addTouchEventListeners).not.toHaveBeenCalled() - }) - - it('should add touch event listeners by default', () => { - fixtureEl.innerHTML = '
' - - const carouselEl = fixtureEl.querySelector('div') - - spyOn(Carousel.prototype, '_addTouchEventListeners') - - document.documentElement.ontouchstart = () => {} - const carousel = new Carousel(carouselEl) - - expect(carousel._addTouchEventListeners).toHaveBeenCalled() - }) - - it('should allow swiperight and call prev with pointer events', done => { - if (!supportPointerEvent) { - expect().nothing() - done() - return - } - - document.documentElement.ontouchstart = () => {} - document.head.appendChild(stylesCarousel) - Simulator.setType('pointer') - - fixtureEl.innerHTML = [ - '' - ].join('') - - const carouselEl = fixtureEl.querySelector('.carousel') - const item = fixtureEl.querySelector('#item') - const carousel = new Carousel(carouselEl) - - spyOn(carousel, 'prev').and.callThrough() - - carouselEl.addEventListener('slid.bs.carousel', () => { - expect(item.classList.contains('active')).toEqual(true) - expect(carousel.prev).toHaveBeenCalled() - document.head.removeChild(stylesCarousel) - delete document.documentElement.ontouchstart - done() - }) - - Simulator.gestures.swipe(carouselEl, { - deltaX: 300, - deltaY: 0 - }) - }) - - it('should allow swipeleft and call next with pointer events', done => { - if (!supportPointerEvent) { - expect().nothing() - done() - return - } - - document.documentElement.ontouchstart = () => {} - document.head.appendChild(stylesCarousel) - Simulator.setType('pointer') - - fixtureEl.innerHTML = [ - '' - ].join('') - - const carouselEl = fixtureEl.querySelector('.carousel') - const item = fixtureEl.querySelector('#item') - const carousel = new Carousel(carouselEl) - - spyOn(carousel, 'next').and.callThrough() - - carouselEl.addEventListener('slid.bs.carousel', () => { - expect(item.classList.contains('active')).toEqual(false) - expect(carousel.next).toHaveBeenCalled() - document.head.removeChild(stylesCarousel) - delete document.documentElement.ontouchstart - done() - }) - - Simulator.gestures.swipe(carouselEl, { - pos: [300, 10], - deltaX: -300, - deltaY: 0 - }) - }) - - it('should allow swiperight and call prev with touch events', done => { - Simulator.setType('touch') - clearPointerEvents() - document.documentElement.ontouchstart = () => {} - - fixtureEl.innerHTML = [ - '' - ].join('') - - const carouselEl = fixtureEl.querySelector('.carousel') - const item = fixtureEl.querySelector('#item') - const carousel = new Carousel(carouselEl) - - spyOn(carousel, 'prev').and.callThrough() - - carouselEl.addEventListener('slid.bs.carousel', () => { - expect(item.classList.contains('active')).toEqual(true) - expect(carousel.prev).toHaveBeenCalled() - delete document.documentElement.ontouchstart - restorePointerEvents() - done() - }) - - Simulator.gestures.swipe(carouselEl, { - deltaX: 300, - deltaY: 0 - }) - }) - - it('should allow swipeleft and call next with touch events', done => { - Simulator.setType('touch') - clearPointerEvents() - document.documentElement.ontouchstart = () => {} - - fixtureEl.innerHTML = [ - '' - ].join('') - - const carouselEl = fixtureEl.querySelector('.carousel') - const item = fixtureEl.querySelector('#item') - const carousel = new Carousel(carouselEl) - - spyOn(carousel, 'next').and.callThrough() - - carouselEl.addEventListener('slid.bs.carousel', () => { - expect(item.classList.contains('active')).toEqual(false) - expect(carousel.next).toHaveBeenCalled() - delete document.documentElement.ontouchstart - restorePointerEvents() - done() - }) - - Simulator.gestures.swipe(carouselEl, { - pos: [300, 10], - deltaX: -300, - deltaY: 0 - }) - }) - - it('should not allow pinch with touch events', done => { - Simulator.setType('touch') - clearPointerEvents() - document.documentElement.ontouchstart = () => {} - - fixtureEl.innerHTML = '' - - const carouselEl = fixtureEl.querySelector('.carousel') - const carousel = new Carousel(carouselEl) - - Simulator.gestures.swipe(carouselEl, { - pos: [300, 10], - deltaX: -300, - deltaY: 0, - touches: 2 - }, () => { - restorePointerEvents() - delete document.documentElement.ontouchstart - expect(carousel.touchDeltaX).toEqual(0) - done() - }) - }) - - it('should call pause method on mouse over with pause equal to hover', done => { - fixtureEl.innerHTML = '' - - const carouselEl = fixtureEl.querySelector('.carousel') - const carousel = new Carousel(carouselEl) - - spyOn(carousel, 'pause') - - const mouseOverEvent = createEvent('mouseover') - carouselEl.dispatchEvent(mouseOverEvent) - - setTimeout(() => { - expect(carousel.pause).toHaveBeenCalled() - done() - }, 10) - }) - - it('should call cycle on mouse out with pause equal to hover', done => { - fixtureEl.innerHTML = '' - - const carouselEl = fixtureEl.querySelector('.carousel') - const carousel = new Carousel(carouselEl) - - spyOn(carousel, 'cycle') - - const mouseOutEvent = createEvent('mouseout') - carouselEl.dispatchEvent(mouseOutEvent) - - setTimeout(() => { - expect(carousel.cycle).toHaveBeenCalled() - done() - }, 10) - }) - }) - - describe('next', () => { - it('should not slide if the carousel is sliding', () => { - fixtureEl.innerHTML = '
' - - const carouselEl = fixtureEl.querySelector('div') - const carousel = new Carousel(carouselEl, {}) - - spyOn(carousel, '_slide') - - carousel._isSliding = true - carousel.next() - - expect(carousel._slide).not.toHaveBeenCalled() - }) - - it('should not fire slid when slide is prevented', done => { - fixtureEl.innerHTML = '
' - - const carouselEl = fixtureEl.querySelector('div') - const carousel = new Carousel(carouselEl, {}) - let slidEvent = false - - const doneTest = () => { - setTimeout(() => { - expect(slidEvent).toEqual(false) - done() - }, 20) - } - - carouselEl.addEventListener('slide.bs.carousel', e => { - e.preventDefault() - doneTest() - }) - - carouselEl.addEventListener('slid.bs.carousel', () => { - slidEvent = true - }) - - carousel.next() - }) - - it('should fire slide event with: direction, relatedTarget, from and to', done => { - fixtureEl.innerHTML = [ - '' - ].join('') - - const carouselEl = fixtureEl.querySelector('#myCarousel') - const carousel = new Carousel(carouselEl, {}) - - const onSlide = e => { - expect(e.direction).toEqual('left') - expect(e.relatedTarget.classList.contains('carousel-item')).toEqual(true) - expect(e.from).toEqual(0) - expect(e.to).toEqual(1) - - carouselEl.removeEventListener('slide.bs.carousel', onSlide) - carouselEl.addEventListener('slide.bs.carousel', onSlide2) - - carousel.prev() - } - - const onSlide2 = e => { - expect(e.direction).toEqual('right') - done() - } - - carouselEl.addEventListener('slide.bs.carousel', onSlide) - carousel.next() - }) - - it('should fire slid event with: direction, relatedTarget, from and to', done => { - fixtureEl.innerHTML = [ - '' - ].join('') - - const carouselEl = fixtureEl.querySelector('#myCarousel') - const carousel = new Carousel(carouselEl, {}) - - const onSlid = e => { - expect(e.direction).toEqual('left') - expect(e.relatedTarget.classList.contains('carousel-item')).toEqual(true) - expect(e.from).toEqual(0) - expect(e.to).toEqual(1) - - carouselEl.removeEventListener('slid.bs.carousel', onSlid) - carouselEl.addEventListener('slid.bs.carousel', onSlid2) - - carousel.prev() - } - - const onSlid2 = e => { - expect(e.direction).toEqual('right') - done() - } - - carouselEl.addEventListener('slid.bs.carousel', onSlid) - carousel.next() - }) - - it('should get interval from data attribute in individual item', () => { - fixtureEl.innerHTML = [ - '' - ].join('') - - const carouselEl = fixtureEl.querySelector('#myCarousel') - const carousel = new Carousel(carouselEl, { - interval: 1814 - }) - - expect(carousel._config.interval).toEqual(1814) - - carousel.next() - - expect(carousel._config.interval).toEqual(7) - }) - - it('should update indicators if present', done => { - fixtureEl.innerHTML = [ - '' - ].join('') - - const carouselEl = fixtureEl.querySelector('#myCarousel') - const secondIndicator = fixtureEl.querySelector('#secondIndicator') - const carousel = new Carousel(carouselEl) - - carouselEl.addEventListener('slid.bs.carousel', () => { - expect(secondIndicator.classList.contains('active')).toEqual(true) - done() - }) - - carousel.next() - }) - }) - - describe('nextWhenVisible', () => { - it('should not call next when the page is not visible', () => { - fixtureEl.innerHTML = [ - '
', - ' ', - '
' - ].join('') - - const carouselEl = fixtureEl.querySelector('.carousel') - const carousel = new Carousel(carouselEl) - - spyOn(carousel, 'next') - - carousel.nextWhenVisible() - - expect(carousel.next).not.toHaveBeenCalled() - }) - }) - - describe('prev', () => { - it('should not slide if the carousel is sliding', () => { - fixtureEl.innerHTML = '
' - - const carouselEl = fixtureEl.querySelector('div') - const carousel = new Carousel(carouselEl, {}) - - spyOn(carousel, '_slide') - - carousel._isSliding = true - carousel.prev() - - expect(carousel._slide).not.toHaveBeenCalled() - }) - }) - - describe('pause', () => { - it('should call cycle if the carousel have carousel-item-next and carousel-item-prev class', () => { - fixtureEl.innerHTML = [ - '' - ].join('') - - const carouselEl = fixtureEl.querySelector('#myCarousel') - const carousel = new Carousel(carouselEl) - - spyOn(carousel, 'cycle') - spyOn(window, 'clearInterval') - - carousel.pause() - - expect(carousel.cycle).toHaveBeenCalledWith(true) - expect(window.clearInterval).toHaveBeenCalled() - expect(carousel._isPaused).toEqual(true) - }) - - it('should not call cycle if nothing is in transition', () => { - fixtureEl.innerHTML = [ - '' - ].join('') - - const carouselEl = fixtureEl.querySelector('#myCarousel') - const carousel = new Carousel(carouselEl) - - spyOn(carousel, 'cycle') - spyOn(window, 'clearInterval') - - carousel.pause() - - expect(carousel.cycle).not.toHaveBeenCalled() - expect(window.clearInterval).toHaveBeenCalled() - expect(carousel._isPaused).toEqual(true) - }) - - it('should not set is paused at true if an event is passed', () => { - fixtureEl.innerHTML = [ - '' - ].join('') - - const carouselEl = fixtureEl.querySelector('#myCarousel') - const carousel = new Carousel(carouselEl) - const event = createEvent('mouseenter') - - spyOn(window, 'clearInterval') - - carousel.pause(event) - - expect(window.clearInterval).toHaveBeenCalled() - expect(carousel._isPaused).toEqual(false) - }) - }) - - describe('cycle', () => { - it('should set an interval', () => { - fixtureEl.innerHTML = [ - '' - ].join('') - - const carouselEl = fixtureEl.querySelector('#myCarousel') - const carousel = new Carousel(carouselEl) - - spyOn(window, 'setInterval').and.callThrough() - - carousel.cycle() - - expect(window.setInterval).toHaveBeenCalled() - }) - - it('should not set interval if the carousel is paused', () => { - fixtureEl.innerHTML = [ - '' - ].join('') - - const carouselEl = fixtureEl.querySelector('#myCarousel') - const carousel = new Carousel(carouselEl) - - spyOn(window, 'setInterval').and.callThrough() - - carousel._isPaused = true - carousel.cycle(true) - - expect(window.setInterval).not.toHaveBeenCalled() - }) - - it('should clear interval if there is one', () => { - fixtureEl.innerHTML = [ - '' - ].join('') - - const carouselEl = fixtureEl.querySelector('#myCarousel') - const carousel = new Carousel(carouselEl) - - carousel._interval = setInterval(() => {}, 10) - - spyOn(window, 'setInterval').and.callThrough() - spyOn(window, 'clearInterval').and.callThrough() - - carousel.cycle() - - expect(window.setInterval).toHaveBeenCalled() - expect(window.clearInterval).toHaveBeenCalled() - }) - }) - - describe('to', () => { - it('should go directement to the provided index', done => { - fixtureEl.innerHTML = [ - '' - ].join('') - - const carouselEl = fixtureEl.querySelector('#myCarousel') - const carousel = new Carousel(carouselEl, {}) - - expect(fixtureEl.querySelector('.active')).toEqual(fixtureEl.querySelector('#item1')) - - carousel.to(2) - - carouselEl.addEventListener('slid.bs.carousel', () => { - expect(fixtureEl.querySelector('.active')).toEqual(fixtureEl.querySelector('#item3')) - done() - }) - }) - - it('should return to a previous slide if the provided index is lower than the current', done => { - fixtureEl.innerHTML = [ - '' - ].join('') - - const carouselEl = fixtureEl.querySelector('#myCarousel') - const carousel = new Carousel(carouselEl, {}) - - expect(fixtureEl.querySelector('.active')).toEqual(fixtureEl.querySelector('#item3')) - - carousel.to(1) - - carouselEl.addEventListener('slid.bs.carousel', () => { - expect(fixtureEl.querySelector('.active')).toEqual(fixtureEl.querySelector('#item2')) - done() - }) - }) - - it('should do nothing if a wrong index is provided', () => { - fixtureEl.innerHTML = [ - '' - ].join('') - - const carouselEl = fixtureEl.querySelector('#myCarousel') - const carousel = new Carousel(carouselEl, {}) - - const spy = spyOn(carousel, '_slide') - - carousel.to(25) - - expect(spy).not.toHaveBeenCalled() - - spy.calls.reset() - - carousel.to(-5) - - expect(spy).not.toHaveBeenCalled() - }) - - it('should call pause and cycle is the provided is the same compare to the current one', () => { - fixtureEl.innerHTML = [ - '' - ].join('') - - const carouselEl = fixtureEl.querySelector('#myCarousel') - const carousel = new Carousel(carouselEl, {}) - - spyOn(carousel, '_slide') - spyOn(carousel, 'pause') - spyOn(carousel, 'cycle') - - carousel.to(0) - - expect(carousel._slide).not.toHaveBeenCalled() - expect(carousel.pause).toHaveBeenCalled() - expect(carousel.cycle).toHaveBeenCalled() - }) - - it('should wait before performing to if a slide is sliding', done => { - fixtureEl.innerHTML = [ - '' - ].join('') - - const carouselEl = fixtureEl.querySelector('#myCarousel') - const carousel = new Carousel(carouselEl, {}) - - spyOn(EventHandler, 'one').and.callThrough() - spyOn(carousel, '_slide') - - carousel._isSliding = true - carousel.to(1) - - expect(carousel._slide).not.toHaveBeenCalled() - expect(EventHandler.one).toHaveBeenCalled() - - spyOn(carousel, 'to') - - EventHandler.trigger(carouselEl, 'slid.bs.carousel') - - setTimeout(() => { - expect(carousel.to).toHaveBeenCalledWith(1) - done() - }) - }) - }) - - describe('dispose', () => { - it('should destroy a carousel', () => { - fixtureEl.innerHTML = [ - '' - ].join('') - - const carouselEl = fixtureEl.querySelector('#myCarousel') - const carousel = new Carousel(carouselEl) - - spyOn(EventHandler, 'off').and.callThrough() - - carousel.dispose() - - expect(EventHandler.off).toHaveBeenCalled() - }) - }) - - describe('jQueryInterface', () => { - it('should create a carousel', () => { - fixtureEl.innerHTML = '
' - - const div = fixtureEl.querySelector('div') - - jQueryMock.fn.carousel = Carousel.jQueryInterface - jQueryMock.elements = [div] - - jQueryMock.fn.carousel.call(jQueryMock) - - expect(Carousel.getInstance(div)).toBeDefined() - }) - - it('should not re create a carousel', () => { - fixtureEl.innerHTML = '
' - - const div = fixtureEl.querySelector('div') - const carousel = new Carousel(div) - - jQueryMock.fn.carousel = Carousel.jQueryInterface - jQueryMock.elements = [div] - - jQueryMock.fn.carousel.call(jQueryMock) - - expect(Carousel.getInstance(div)).toEqual(carousel) - }) - - it('should call to if the config is a number', () => { - fixtureEl.innerHTML = '
' - - const div = fixtureEl.querySelector('div') - const carousel = new Carousel(div) - const slideTo = 2 - - spyOn(carousel, 'to') - - jQueryMock.fn.carousel = Carousel.jQueryInterface - jQueryMock.elements = [div] - - jQueryMock.fn.carousel.call(jQueryMock, slideTo) - - expect(carousel.to).toHaveBeenCalledWith(slideTo) - }) - - it('should throw error on undefined method', () => { - fixtureEl.innerHTML = '
' - - const div = fixtureEl.querySelector('div') - const action = 'undefinedMethod' - - jQueryMock.fn.carousel = Carousel.jQueryInterface - jQueryMock.elements = [div] - - try { - jQueryMock.fn.carousel.call(jQueryMock, action) - } catch (error) { - expect(error.message).toEqual(`No method named "${action}"`) - } - }) - }) - - describe('data-api', () => { - it('should init carousels with data-ride="carousel" on load', () => { - fixtureEl.innerHTML = '
' - - const carouselEl = fixtureEl.querySelector('div') - const loadEvent = createEvent('load') - - window.dispatchEvent(loadEvent) - - expect(Carousel.getInstance(carouselEl)).toBeDefined() - }) - - it('should create carousel and go to the next slide on click', done => { - fixtureEl.innerHTML = [ - '' - ].join('') - - const next = fixtureEl.querySelector('#next') - const item2 = fixtureEl.querySelector('#item2') - - next.click() - - setTimeout(() => { - expect(item2.classList.contains('active')).toEqual(true) - done() - }, 10) - }) - - it('should create carousel and go to the next slide on click with data-slide-to', done => { - fixtureEl.innerHTML = [ - '' - ].join('') - - const next = fixtureEl.querySelector('#next') - const item2 = fixtureEl.querySelector('#item2') - - next.click() - - setTimeout(() => { - expect(item2.classList.contains('active')).toEqual(true) - done() - }, 10) - }) - - it('should do nothing if no selector on click on arrows', () => { - fixtureEl.innerHTML = [ - '' - ].join('') - - const next = fixtureEl.querySelector('#next') - - next.click() - - expect().nothing() - }) - - it('should do nothing if no carousel class on click on arrows', () => { - fixtureEl.innerHTML = [ - '
', - ' ', - ' ', - ' ', - '
' - ].join('') - - const next = fixtureEl.querySelector('#next') - - next.click() - - expect().nothing() - }) - }) -}) diff --git a/js/src/collapse.js b/js/src/collapse.js new file mode 100644 index 000000000..f533885ec --- /dev/null +++ b/js/src/collapse.js @@ -0,0 +1,441 @@ +/** + * -------------------------------------------------------------------------- + * Bootstrap (v4.3.1): collapse.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + * -------------------------------------------------------------------------- + */ + +import { + getjQuery, + TRANSITION_END, + emulateTransitionEnd, + getSelectorFromElement, + getElementFromSelector, + getTransitionDurationFromElement, + isElement, + makeArray, + reflow, + typeCheckConfig +} from './util/index' +import Data from './dom/data' +import EventHandler from './dom/event-handler' +import Manipulator from './dom/manipulator' +import SelectorEngine from './dom/selector-engine' + +/** + * ------------------------------------------------------------------------ + * Constants + * ------------------------------------------------------------------------ + */ + +const NAME = 'collapse' +const VERSION = '4.3.1' +const DATA_KEY = 'bs.collapse' +const EVENT_KEY = `.${DATA_KEY}` +const DATA_API_KEY = '.data-api' + +const Default = { + toggle: true, + parent: '' +} + +const DefaultType = { + toggle: 'boolean', + parent: '(string|element)' +} + +const Event = { + SHOW: `show${EVENT_KEY}`, + SHOWN: `shown${EVENT_KEY}`, + HIDE: `hide${EVENT_KEY}`, + HIDDEN: `hidden${EVENT_KEY}`, + CLICK_DATA_API: `click${EVENT_KEY}${DATA_API_KEY}` +} + +const ClassName = { + SHOW: 'show', + COLLAPSE: 'collapse', + COLLAPSING: 'collapsing', + COLLAPSED: 'collapsed' +} + +const Dimension = { + WIDTH: 'width', + HEIGHT: 'height' +} + +const Selector = { + ACTIVES: '.show, .collapsing', + DATA_TOGGLE: '[data-toggle="collapse"]' +} + +/** + * ------------------------------------------------------------------------ + * Class Definition + * ------------------------------------------------------------------------ + */ + +class Collapse { + constructor(element, config) { + this._isTransitioning = false + this._element = element + this._config = this._getConfig(config) + this._triggerArray = makeArray(SelectorEngine.find( + `[data-toggle="collapse"][href="#${element.id}"],` + + `[data-toggle="collapse"][data-target="#${element.id}"]` + )) + + const toggleList = makeArray(SelectorEngine.find(Selector.DATA_TOGGLE)) + for (let i = 0, len = toggleList.length; i < len; i++) { + const elem = toggleList[i] + const selector = getSelectorFromElement(elem) + const filterElement = makeArray(SelectorEngine.find(selector)) + .filter(foundElem => foundElem === element) + + if (selector !== null && filterElement.length) { + this._selector = selector + this._triggerArray.push(elem) + } + } + + this._parent = this._config.parent ? this._getParent() : null + + if (!this._config.parent) { + this._addAriaAndCollapsedClass(this._element, this._triggerArray) + } + + if (this._config.toggle) { + this.toggle() + } + + Data.setData(element, DATA_KEY, this) + } + + // Getters + + static get VERSION() { + return VERSION + } + + static get Default() { + return Default + } + + // Public + + toggle() { + if (this._element.classList.contains(ClassName.SHOW)) { + this.hide() + } else { + this.show() + } + } + + show() { + if (this._isTransitioning || + this._element.classList.contains(ClassName.SHOW)) { + return + } + + let actives + let activesData + + if (this._parent) { + actives = makeArray(SelectorEngine.find(Selector.ACTIVES, this._parent)) + .filter(elem => { + if (typeof this._config.parent === 'string') { + return elem.getAttribute('data-parent') === this._config.parent + } + + return elem.classList.contains(ClassName.COLLAPSE) + }) + + if (actives.length === 0) { + actives = null + } + } + + const container = SelectorEngine.findOne(this._selector) + if (actives) { + const tempActiveData = actives.filter(elem => container !== elem) + activesData = tempActiveData[0] ? Data.getData(tempActiveData[0], DATA_KEY) : null + + if (activesData && activesData._isTransitioning) { + return + } + } + + const startEvent = EventHandler.trigger(this._element, Event.SHOW) + if (startEvent.defaultPrevented) { + return + } + + if (actives) { + actives.forEach(elemActive => { + if (container !== elemActive) { + Collapse.collapseInterface(elemActive, 'hide') + } + + if (!activesData) { + Data.setData(elemActive, DATA_KEY, null) + } + }) + } + + const dimension = this._getDimension() + + this._element.classList.remove(ClassName.COLLAPSE) + this._element.classList.add(ClassName.COLLAPSING) + + this._element.style[dimension] = 0 + + if (this._triggerArray.length) { + this._triggerArray.forEach(element => { + element.classList.remove(ClassName.COLLAPSED) + element.setAttribute('aria-expanded', true) + }) + } + + this.setTransitioning(true) + + const complete = () => { + 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) + + EventHandler.trigger(this._element, Event.SHOWN) + } + + const capitalizedDimension = dimension[0].toUpperCase() + dimension.slice(1) + const scrollSize = `scroll${capitalizedDimension}` + const transitionDuration = getTransitionDurationFromElement(this._element) + + EventHandler.one(this._element, TRANSITION_END, complete) + + emulateTransitionEnd(this._element, transitionDuration) + this._element.style[dimension] = `${this._element[scrollSize]}px` + } + + hide() { + if (this._isTransitioning || + !this._element.classList.contains(ClassName.SHOW)) { + return + } + + const startEvent = EventHandler.trigger(this._element, Event.HIDE) + if (startEvent.defaultPrevented) { + return + } + + const dimension = this._getDimension() + + this._element.style[dimension] = `${this._element.getBoundingClientRect()[dimension]}px` + + reflow(this._element) + + 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) { + for (let i = 0; i < triggerArrayLength; i++) { + const trigger = this._triggerArray[i] + const elem = getElementFromSelector(trigger) + + if (elem && !elem.classList.contains(ClassName.SHOW)) { + trigger.classList.add(ClassName.COLLAPSED) + trigger.setAttribute('aria-expanded', false) + } + } + } + + this.setTransitioning(true) + + const complete = () => { + this.setTransitioning(false) + this._element.classList.remove(ClassName.COLLAPSING) + this._element.classList.add(ClassName.COLLAPSE) + EventHandler.trigger(this._element, Event.HIDDEN) + } + + this._element.style[dimension] = '' + const transitionDuration = getTransitionDurationFromElement(this._element) + + EventHandler.one(this._element, TRANSITION_END, complete) + emulateTransitionEnd(this._element, transitionDuration) + } + + setTransitioning(isTransitioning) { + this._isTransitioning = isTransitioning + } + + dispose() { + Data.removeData(this._element, DATA_KEY) + + this._config = null + this._parent = null + this._element = null + this._triggerArray = null + this._isTransitioning = null + } + + // Private + + _getConfig(config) { + config = { + ...Default, + ...config + } + config.toggle = Boolean(config.toggle) // Coerce string values + typeCheckConfig(NAME, config, DefaultType) + return config + } + + _getDimension() { + const hasWidth = this._element.classList.contains(Dimension.WIDTH) + return hasWidth ? Dimension.WIDTH : Dimension.HEIGHT + } + + _getParent() { + let { parent } = this._config + + if (isElement(parent)) { + // it's a jQuery object + if (typeof parent.jquery !== 'undefined' || typeof parent[0] !== 'undefined') { + parent = parent[0] + } + } else { + parent = SelectorEngine.findOne(parent) + } + + const selector = `[data-toggle="collapse"][data-parent="${parent}"]` + + makeArray(SelectorEngine.find(selector, parent)) + .forEach(element => { + const selected = getElementFromSelector(element) + + this._addAriaAndCollapsedClass( + selected, + [element] + ) + }) + + return parent + } + + _addAriaAndCollapsedClass(element, triggerArray) { + if (element) { + const isOpen = element.classList.contains(ClassName.SHOW) + + if (triggerArray.length) { + triggerArray.forEach(elem => { + if (isOpen) { + elem.classList.remove(ClassName.COLLAPSED) + } else { + elem.classList.add(ClassName.COLLAPSED) + } + + elem.setAttribute('aria-expanded', isOpen) + }) + } + } + } + + // Static + + static collapseInterface(element, config) { + let data = Data.getData(element, DATA_KEY) + const _config = { + ...Default, + ...Manipulator.getDataAttributes(element), + ...typeof config === 'object' && config ? config : {} + } + + if (!data && _config.toggle && /show|hide/.test(config)) { + _config.toggle = false + } + + if (!data) { + data = new Collapse(element, _config) + } + + if (typeof config === 'string') { + if (typeof data[config] === 'undefined') { + throw new TypeError(`No method named "${config}"`) + } + + data[config]() + } + } + + static jQueryInterface(config) { + return this.each(function () { + Collapse.collapseInterface(this, config) + }) + } + + static getInstance(element) { + return Data.getData(element, DATA_KEY) + } +} + +/** + * ------------------------------------------------------------------------ + * Data Api implementation + * ------------------------------------------------------------------------ + */ + +EventHandler.on(document, Event.CLICK_DATA_API, Selector.DATA_TOGGLE, function (event) { + // preventDefault only for elements (which change the URL) not inside the collapsible element + if (event.target.tagName === 'A') { + event.preventDefault() + } + + const triggerData = Manipulator.getDataAttributes(this) + const selector = getSelectorFromElement(this) + const selectorElements = 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 + } + + Collapse.collapseInterface(element, config) + }) +}) + +const $ = getjQuery() + +/** + * ------------------------------------------------------------------------ + * jQuery + * ------------------------------------------------------------------------ + * add .collapse to jQuery only if jQuery is present + */ +/* istanbul ignore if */ +if ($) { + 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/collapse/collapse.js b/js/src/collapse/collapse.js deleted file mode 100644 index 4de7b5282..000000000 --- a/js/src/collapse/collapse.js +++ /dev/null @@ -1,441 +0,0 @@ -/** - * -------------------------------------------------------------------------- - * Bootstrap (v4.3.1): collapse.js - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) - * -------------------------------------------------------------------------- - */ - -import { - getjQuery, - TRANSITION_END, - emulateTransitionEnd, - getSelectorFromElement, - getElementFromSelector, - getTransitionDurationFromElement, - isElement, - makeArray, - reflow, - typeCheckConfig -} from '../util/index' -import Data from '../dom/data' -import EventHandler from '../dom/event-handler' -import Manipulator from '../dom/manipulator' -import SelectorEngine from '../dom/selector-engine' - -/** - * ------------------------------------------------------------------------ - * Constants - * ------------------------------------------------------------------------ - */ - -const NAME = 'collapse' -const VERSION = '4.3.1' -const DATA_KEY = 'bs.collapse' -const EVENT_KEY = `.${DATA_KEY}` -const DATA_API_KEY = '.data-api' - -const Default = { - toggle: true, - parent: '' -} - -const DefaultType = { - toggle: 'boolean', - parent: '(string|element)' -} - -const Event = { - SHOW: `show${EVENT_KEY}`, - SHOWN: `shown${EVENT_KEY}`, - HIDE: `hide${EVENT_KEY}`, - HIDDEN: `hidden${EVENT_KEY}`, - CLICK_DATA_API: `click${EVENT_KEY}${DATA_API_KEY}` -} - -const ClassName = { - SHOW: 'show', - COLLAPSE: 'collapse', - COLLAPSING: 'collapsing', - COLLAPSED: 'collapsed' -} - -const Dimension = { - WIDTH: 'width', - HEIGHT: 'height' -} - -const Selector = { - ACTIVES: '.show, .collapsing', - DATA_TOGGLE: '[data-toggle="collapse"]' -} - -/** - * ------------------------------------------------------------------------ - * Class Definition - * ------------------------------------------------------------------------ - */ - -class Collapse { - constructor(element, config) { - this._isTransitioning = false - this._element = element - this._config = this._getConfig(config) - this._triggerArray = makeArray(SelectorEngine.find( - `[data-toggle="collapse"][href="#${element.id}"],` + - `[data-toggle="collapse"][data-target="#${element.id}"]` - )) - - const toggleList = makeArray(SelectorEngine.find(Selector.DATA_TOGGLE)) - for (let i = 0, len = toggleList.length; i < len; i++) { - const elem = toggleList[i] - const selector = getSelectorFromElement(elem) - const filterElement = makeArray(SelectorEngine.find(selector)) - .filter(foundElem => foundElem === element) - - if (selector !== null && filterElement.length) { - this._selector = selector - this._triggerArray.push(elem) - } - } - - this._parent = this._config.parent ? this._getParent() : null - - if (!this._config.parent) { - this._addAriaAndCollapsedClass(this._element, this._triggerArray) - } - - if (this._config.toggle) { - this.toggle() - } - - Data.setData(element, DATA_KEY, this) - } - - // Getters - - static get VERSION() { - return VERSION - } - - static get Default() { - return Default - } - - // Public - - toggle() { - if (this._element.classList.contains(ClassName.SHOW)) { - this.hide() - } else { - this.show() - } - } - - show() { - if (this._isTransitioning || - this._element.classList.contains(ClassName.SHOW)) { - return - } - - let actives - let activesData - - if (this._parent) { - actives = makeArray(SelectorEngine.find(Selector.ACTIVES, this._parent)) - .filter(elem => { - if (typeof this._config.parent === 'string') { - return elem.getAttribute('data-parent') === this._config.parent - } - - return elem.classList.contains(ClassName.COLLAPSE) - }) - - if (actives.length === 0) { - actives = null - } - } - - const container = SelectorEngine.findOne(this._selector) - if (actives) { - const tempActiveData = actives.filter(elem => container !== elem) - activesData = tempActiveData[0] ? Data.getData(tempActiveData[0], DATA_KEY) : null - - if (activesData && activesData._isTransitioning) { - return - } - } - - const startEvent = EventHandler.trigger(this._element, Event.SHOW) - if (startEvent.defaultPrevented) { - return - } - - if (actives) { - actives.forEach(elemActive => { - if (container !== elemActive) { - Collapse.collapseInterface(elemActive, 'hide') - } - - if (!activesData) { - Data.setData(elemActive, DATA_KEY, null) - } - }) - } - - const dimension = this._getDimension() - - this._element.classList.remove(ClassName.COLLAPSE) - this._element.classList.add(ClassName.COLLAPSING) - - this._element.style[dimension] = 0 - - if (this._triggerArray.length) { - this._triggerArray.forEach(element => { - element.classList.remove(ClassName.COLLAPSED) - element.setAttribute('aria-expanded', true) - }) - } - - this.setTransitioning(true) - - const complete = () => { - 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) - - EventHandler.trigger(this._element, Event.SHOWN) - } - - const capitalizedDimension = dimension[0].toUpperCase() + dimension.slice(1) - const scrollSize = `scroll${capitalizedDimension}` - const transitionDuration = getTransitionDurationFromElement(this._element) - - EventHandler.one(this._element, TRANSITION_END, complete) - - emulateTransitionEnd(this._element, transitionDuration) - this._element.style[dimension] = `${this._element[scrollSize]}px` - } - - hide() { - if (this._isTransitioning || - !this._element.classList.contains(ClassName.SHOW)) { - return - } - - const startEvent = EventHandler.trigger(this._element, Event.HIDE) - if (startEvent.defaultPrevented) { - return - } - - const dimension = this._getDimension() - - this._element.style[dimension] = `${this._element.getBoundingClientRect()[dimension]}px` - - reflow(this._element) - - 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) { - for (let i = 0; i < triggerArrayLength; i++) { - const trigger = this._triggerArray[i] - const elem = getElementFromSelector(trigger) - - if (elem && !elem.classList.contains(ClassName.SHOW)) { - trigger.classList.add(ClassName.COLLAPSED) - trigger.setAttribute('aria-expanded', false) - } - } - } - - this.setTransitioning(true) - - const complete = () => { - this.setTransitioning(false) - this._element.classList.remove(ClassName.COLLAPSING) - this._element.classList.add(ClassName.COLLAPSE) - EventHandler.trigger(this._element, Event.HIDDEN) - } - - this._element.style[dimension] = '' - const transitionDuration = getTransitionDurationFromElement(this._element) - - EventHandler.one(this._element, TRANSITION_END, complete) - emulateTransitionEnd(this._element, transitionDuration) - } - - setTransitioning(isTransitioning) { - this._isTransitioning = isTransitioning - } - - dispose() { - Data.removeData(this._element, DATA_KEY) - - this._config = null - this._parent = null - this._element = null - this._triggerArray = null - this._isTransitioning = null - } - - // Private - - _getConfig(config) { - config = { - ...Default, - ...config - } - config.toggle = Boolean(config.toggle) // Coerce string values - typeCheckConfig(NAME, config, DefaultType) - return config - } - - _getDimension() { - const hasWidth = this._element.classList.contains(Dimension.WIDTH) - return hasWidth ? Dimension.WIDTH : Dimension.HEIGHT - } - - _getParent() { - let { parent } = this._config - - if (isElement(parent)) { - // it's a jQuery object - if (typeof parent.jquery !== 'undefined' || typeof parent[0] !== 'undefined') { - parent = parent[0] - } - } else { - parent = SelectorEngine.findOne(parent) - } - - const selector = `[data-toggle="collapse"][data-parent="${parent}"]` - - makeArray(SelectorEngine.find(selector, parent)) - .forEach(element => { - const selected = getElementFromSelector(element) - - this._addAriaAndCollapsedClass( - selected, - [element] - ) - }) - - return parent - } - - _addAriaAndCollapsedClass(element, triggerArray) { - if (element) { - const isOpen = element.classList.contains(ClassName.SHOW) - - if (triggerArray.length) { - triggerArray.forEach(elem => { - if (isOpen) { - elem.classList.remove(ClassName.COLLAPSED) - } else { - elem.classList.add(ClassName.COLLAPSED) - } - - elem.setAttribute('aria-expanded', isOpen) - }) - } - } - } - - // Static - - static collapseInterface(element, config) { - let data = Data.getData(element, DATA_KEY) - const _config = { - ...Default, - ...Manipulator.getDataAttributes(element), - ...typeof config === 'object' && config ? config : {} - } - - if (!data && _config.toggle && /show|hide/.test(config)) { - _config.toggle = false - } - - if (!data) { - data = new Collapse(element, _config) - } - - if (typeof config === 'string') { - if (typeof data[config] === 'undefined') { - throw new TypeError(`No method named "${config}"`) - } - - data[config]() - } - } - - static jQueryInterface(config) { - return this.each(function () { - Collapse.collapseInterface(this, config) - }) - } - - static getInstance(element) { - return Data.getData(element, DATA_KEY) - } -} - -/** - * ------------------------------------------------------------------------ - * Data Api implementation - * ------------------------------------------------------------------------ - */ - -EventHandler.on(document, Event.CLICK_DATA_API, Selector.DATA_TOGGLE, function (event) { - // preventDefault only for elements (which change the URL) not inside the collapsible element - if (event.target.tagName === 'A') { - event.preventDefault() - } - - const triggerData = Manipulator.getDataAttributes(this) - const selector = getSelectorFromElement(this) - const selectorElements = 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 - } - - Collapse.collapseInterface(element, config) - }) -}) - -const $ = getjQuery() - -/** - * ------------------------------------------------------------------------ - * jQuery - * ------------------------------------------------------------------------ - * add .collapse to jQuery only if jQuery is present - */ -/* istanbul ignore if */ -if ($) { - 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/collapse/collapse.spec.js b/js/src/collapse/collapse.spec.js deleted file mode 100644 index 154bc2c6b..000000000 --- a/js/src/collapse/collapse.spec.js +++ /dev/null @@ -1,826 +0,0 @@ -import Collapse from './collapse' -import EventHandler from '../dom/event-handler' -import { makeArray } from '../util/index' - -/** Test helpers */ -import { getFixture, clearFixture, jQueryMock } from '../../tests/helpers/fixture' - -describe('Collapse', () => { - let fixtureEl - - beforeAll(() => { - fixtureEl = getFixture() - }) - - afterEach(() => { - clearFixture() - }) - - describe('VERSION', () => { - it('should return plugin version', () => { - expect(Collapse.VERSION).toEqual(jasmine.any(String)) - }) - }) - - describe('Default', () => { - it('should return plugin default config', () => { - expect(Collapse.Default).toEqual(jasmine.any(Object)) - }) - }) - - describe('constructor', () => { - it('should allow jquery object in parent config', () => { - fixtureEl.innerHTML = [ - '
', - '
', - ' Toggle item', - '
Lorem ipsum
', - '
', - '
' - ].join('') - - const collapseEl = fixtureEl.querySelector('div.collapse') - const myCollapseEl = fixtureEl.querySelector('.my-collapse') - const fakejQueryObject = { - 0: myCollapseEl - } - const collapse = new Collapse(collapseEl, { - parent: fakejQueryObject - }) - - expect(collapse._config.parent).toEqual(fakejQueryObject) - expect(collapse._getParent()).toEqual(myCollapseEl) - }) - - it('should allow non jquery object in parent config', () => { - fixtureEl.innerHTML = [ - '
', - '
', - ' Toggle item', - '
Lorem ipsum
', - '
', - '
' - ].join('') - - const collapseEl = fixtureEl.querySelector('div.collapse') - const myCollapseEl = fixtureEl.querySelector('.my-collapse') - const collapse = new Collapse(collapseEl, { - parent: myCollapseEl - }) - - expect(collapse._config.parent).toEqual(myCollapseEl) - }) - - it('should allow string selector in parent config', () => { - fixtureEl.innerHTML = [ - '
', - '
', - ' Toggle item', - '
Lorem ipsum
', - '
', - '
' - ].join('') - - const collapseEl = fixtureEl.querySelector('div.collapse') - const myCollapseEl = fixtureEl.querySelector('.my-collapse') - const collapse = new Collapse(collapseEl, { - parent: 'div.my-collapse' - }) - - expect(collapse._config.parent).toEqual('div.my-collapse') - expect(collapse._getParent()).toEqual(myCollapseEl) - }) - }) - - describe('toggle', () => { - it('should call show method if show class is not present', () => { - fixtureEl.innerHTML = '
' - - const collapseEl = fixtureEl.querySelector('div') - const collapse = new Collapse(collapseEl) - - spyOn(collapse, 'show') - - collapse.toggle() - - expect(collapse.show).toHaveBeenCalled() - }) - - it('should call hide method if show class is present', () => { - fixtureEl.innerHTML = '
' - - const collapseEl = fixtureEl.querySelector('.show') - const collapse = new Collapse(collapseEl, { - toggle: false - }) - - spyOn(collapse, 'hide') - - collapse.toggle() - - expect(collapse.hide).toHaveBeenCalled() - }) - - it('should find collapse children if they have collapse class too not only data-parent', done => { - fixtureEl.innerHTML = [ - '
', - '
', - ' Toggle item 1', - '
Lorem ipsum 1
', - '
', - '
', - ' Toggle item 2', - '
Lorem ipsum 2
', - '
', - '
' - ].join('') - - const parent = fixtureEl.querySelector('.my-collapse') - const collapseEl1 = fixtureEl.querySelector('#collapse1') - const collapseEl2 = fixtureEl.querySelector('#collapse2') - - const collapseList = makeArray(fixtureEl.querySelectorAll('.collapse')) - .map(el => new Collapse(el, { - parent, - toggle: false - })) - - collapseEl2.addEventListener('shown.bs.collapse', () => { - expect(collapseEl2.classList.contains('show')).toEqual(true) - expect(collapseEl1.classList.contains('show')).toEqual(false) - done() - }) - - collapseList[1].toggle() - }) - }) - - describe('show', () => { - it('should do nothing if is transitioning', () => { - fixtureEl.innerHTML = '
' - - spyOn(EventHandler, 'trigger') - - const collapseEl = fixtureEl.querySelector('div') - const collapse = new Collapse(collapseEl, { - toggle: false - }) - - collapse._isTransitioning = true - collapse.show() - - expect(EventHandler.trigger).not.toHaveBeenCalled() - }) - - it('should do nothing if already shown', () => { - fixtureEl.innerHTML = '
' - - spyOn(EventHandler, 'trigger') - - const collapseEl = fixtureEl.querySelector('div') - const collapse = new Collapse(collapseEl, { - toggle: false - }) - - collapse.show() - - expect(EventHandler.trigger).not.toHaveBeenCalled() - }) - - it('should show a collapsed element', done => { - fixtureEl.innerHTML = '
' - - const collapseEl = fixtureEl.querySelector('div') - const collapse = new Collapse(collapseEl, { - toggle: false - }) - - collapseEl.addEventListener('show.bs.collapse', () => { - expect(collapseEl.style.height).toEqual('0px') - }) - collapseEl.addEventListener('shown.bs.collapse', () => { - expect(collapseEl.classList.contains('show')).toEqual(true) - expect(collapseEl.style.height).toEqual('') - done() - }) - - collapse.show() - }) - - it('should show a collapsed element on width', done => { - fixtureEl.innerHTML = '
' - - const collapseEl = fixtureEl.querySelector('div') - const collapse = new Collapse(collapseEl, { - toggle: false - }) - - collapseEl.addEventListener('show.bs.collapse', () => { - expect(collapseEl.style.width).toEqual('0px') - }) - collapseEl.addEventListener('shown.bs.collapse', () => { - expect(collapseEl.classList.contains('show')).toEqual(true) - expect(collapseEl.style.width).toEqual('') - done() - }) - - collapse.show() - }) - - it('should collapse only the first collapse', done => { - fixtureEl.innerHTML = [ - '
', - '
', - '
', - '
', - '
', - '
' - ].join('') - - const el1 = fixtureEl.querySelector('#collapse1') - const el2 = fixtureEl.querySelector('#collapse2') - const collapse = new Collapse(el1, { - toggle: false - }) - - el1.addEventListener('shown.bs.collapse', () => { - expect(el1.classList.contains('show')).toEqual(true) - expect(el2.classList.contains('show')).toEqual(true) - done() - }) - - collapse.show() - }) - - it('should not fire shown when show is prevented', done => { - fixtureEl.innerHTML = '
' - - const collapseEl = fixtureEl.querySelector('div') - const collapse = new Collapse(collapseEl, { - toggle: false - }) - - const expectEnd = () => { - setTimeout(() => { - expect().nothing() - done() - }, 10) - } - - collapseEl.addEventListener('show.bs.collapse', e => { - e.preventDefault() - expectEnd() - }) - - collapseEl.addEventListener('shown.bs.collapse', () => { - throw new Error('should not fire shown event') - }) - - collapse.show() - }) - }) - - describe('hide', () => { - it('should do nothing if is transitioning', () => { - fixtureEl.innerHTML = '
' - - spyOn(EventHandler, 'trigger') - - const collapseEl = fixtureEl.querySelector('div') - const collapse = new Collapse(collapseEl, { - toggle: false - }) - - collapse._isTransitioning = true - collapse.hide() - - expect(EventHandler.trigger).not.toHaveBeenCalled() - }) - - it('should do nothing if already shown', () => { - fixtureEl.innerHTML = '
' - - spyOn(EventHandler, 'trigger') - - const collapseEl = fixtureEl.querySelector('div') - const collapse = new Collapse(collapseEl, { - toggle: false - }) - - collapse.hide() - - expect(EventHandler.trigger).not.toHaveBeenCalled() - }) - - it('should hide a collapsed element', done => { - fixtureEl.innerHTML = '
' - - const collapseEl = fixtureEl.querySelector('div') - const collapse = new Collapse(collapseEl, { - toggle: false - }) - - collapseEl.addEventListener('hidden.bs.collapse', () => { - expect(collapseEl.classList.contains('show')).toEqual(false) - expect(collapseEl.style.height).toEqual('') - done() - }) - - collapse.hide() - }) - - it('should not fire hidden when hide is prevented', done => { - fixtureEl.innerHTML = '
' - - const collapseEl = fixtureEl.querySelector('div') - const collapse = new Collapse(collapseEl, { - toggle: false - }) - - const expectEnd = () => { - setTimeout(() => { - expect().nothing() - done() - }, 10) - } - - collapseEl.addEventListener('hide.bs.collapse', e => { - e.preventDefault() - expectEnd() - }) - - collapseEl.addEventListener('hidden.bs.collapse', () => { - throw new Error('should not fire hidden event') - }) - - collapse.hide() - }) - }) - - describe('dispose', () => { - it('should destroy a collapse', () => { - fixtureEl.innerHTML = '
' - - const collapseEl = fixtureEl.querySelector('div') - const collapse = new Collapse(collapseEl, { - toggle: false - }) - - expect(Collapse.getInstance(collapseEl)).toEqual(collapse) - - collapse.dispose() - - expect(Collapse.getInstance(collapseEl)).toEqual(null) - }) - }) - - describe('data-api', () => { - it('should show multiple collapsed elements', done => { - fixtureEl.innerHTML = [ - '', - '
', - '
' - ].join('') - - const trigger = fixtureEl.querySelector('a') - const collapse1 = fixtureEl.querySelector('#collapse1') - const collapse2 = fixtureEl.querySelector('#collapse2') - - collapse2.addEventListener('shown.bs.collapse', () => { - expect(trigger.getAttribute('aria-expanded')).toEqual('true') - expect(trigger.classList.contains('collapsed')).toEqual(false) - expect(collapse1.classList.contains('show')).toEqual(true) - expect(collapse1.classList.contains('show')).toEqual(true) - done() - }) - - trigger.click() - }) - - it('should hide multiple collapsed elements', done => { - fixtureEl.innerHTML = [ - '', - '
', - '
' - ].join('') - - const trigger = fixtureEl.querySelector('a') - const collapse1 = fixtureEl.querySelector('#collapse1') - const collapse2 = fixtureEl.querySelector('#collapse2') - - collapse2.addEventListener('hidden.bs.collapse', () => { - expect(trigger.getAttribute('aria-expanded')).toEqual('false') - expect(trigger.classList.contains('collapsed')).toEqual(true) - expect(collapse1.classList.contains('show')).toEqual(false) - expect(collapse1.classList.contains('show')).toEqual(false) - done() - }) - - trigger.click() - }) - - it('should remove "collapsed" class from target when collapse is shown', done => { - fixtureEl.innerHTML = [ - '', - '', - '
' - ].join('') - - const link1 = fixtureEl.querySelector('#link1') - const link2 = fixtureEl.querySelector('#link2') - const collapseTest1 = fixtureEl.querySelector('#test1') - - collapseTest1.addEventListener('hidden.bs.collapse', () => { - expect(link1.getAttribute('aria-expanded')).toEqual('false') - expect(link2.getAttribute('aria-expanded')).toEqual('false') - expect(link1.classList.contains('collapsed')).toEqual(true) - expect(link2.classList.contains('collapsed')).toEqual(true) - done() - }) - - link1.click() - }) - - it('should allow accordion to use children other than card', done => { - fixtureEl.innerHTML = [ - '
', - '
', - ' ', - '
', - '
', - '
', - ' ', - '
', - '
', - '
' - ].join('') - - const trigger = fixtureEl.querySelector('#linkTrigger') - const triggerTwo = fixtureEl.querySelector('#linkTriggerTwo') - const collapseOne = fixtureEl.querySelector('#collapseOne') - const collapseTwo = fixtureEl.querySelector('#collapseTwo') - - collapseOne.addEventListener('shown.bs.collapse', () => { - expect(collapseOne.classList.contains('show')).toEqual(true) - expect(collapseTwo.classList.contains('show')).toEqual(false) - - collapseTwo.addEventListener('shown.bs.collapse', () => { - expect(collapseOne.classList.contains('show')).toEqual(false) - expect(collapseTwo.classList.contains('show')).toEqual(true) - done() - }) - - triggerTwo.click() - }) - - trigger.click() - }) - - it('should not prevent event for input', done => { - fixtureEl.innerHTML = [ - '', - '
' - ].join('') - - const target = fixtureEl.querySelector('input') - const collapseEl = fixtureEl.querySelector('#collapsediv1') - - collapseEl.addEventListener('shown.bs.collapse', () => { - expect(collapseEl.classList.contains('show')).toEqual(true) - expect(target.checked).toEqual(true) - done() - }) - - target.click() - }) - - it('should allow accordion to contain nested elements', done => { - fixtureEl.innerHTML = [ - '
', - '
', - '
', - '
', - ' ', - '
', - '
', - '
', - '
', - '
', - ' ', - '
', - '
', - '
', - '
', - '
' - ].join('') - - const triggerEl = fixtureEl.querySelector('#linkTrigger') - const triggerTwoEl = fixtureEl.querySelector('#linkTriggerTwo') - const collapseOneEl = fixtureEl.querySelector('#collapseOne') - const collapseTwoEl = fixtureEl.querySelector('#collapseTwo') - - collapseOneEl.addEventListener('shown.bs.collapse', () => { - expect(collapseOneEl.classList.contains('show')).toEqual(true) - expect(triggerEl.classList.contains('collapsed')).toEqual(false) - expect(triggerEl.getAttribute('aria-expanded')).toEqual('true') - - expect(collapseTwoEl.classList.contains('show')).toEqual(false) - expect(triggerTwoEl.classList.contains('collapsed')).toEqual(true) - expect(triggerTwoEl.getAttribute('aria-expanded')).toEqual('false') - - collapseTwoEl.addEventListener('shown.bs.collapse', () => { - expect(collapseOneEl.classList.contains('show')).toEqual(false) - expect(triggerEl.classList.contains('collapsed')).toEqual(true) - expect(triggerEl.getAttribute('aria-expanded')).toEqual('false') - - expect(collapseTwoEl.classList.contains('show')).toEqual(true) - expect(triggerTwoEl.classList.contains('collapsed')).toEqual(false) - expect(triggerTwoEl.getAttribute('aria-expanded')).toEqual('true') - done() - }) - - triggerTwoEl.click() - }) - - triggerEl.click() - }) - - it('should allow accordion to target multiple elements', done => { - fixtureEl.innerHTML = [ - '
', - ' ', - ' ', - '
', - '
', - '
', - '
', - '
' - ].join('') - - const trigger = fixtureEl.querySelector('#linkTriggerOne') - const triggerTwo = fixtureEl.querySelector('#linkTriggerTwo') - const collapseOneOne = fixtureEl.querySelector('#collapseOneOne') - const collapseOneTwo = fixtureEl.querySelector('#collapseOneTwo') - const collapseTwoOne = fixtureEl.querySelector('#collapseTwoOne') - const collapseTwoTwo = fixtureEl.querySelector('#collapseTwoTwo') - const collapsedElements = { - one: false, - two: false - } - - function firstTest() { - expect(collapseOneOne.classList.contains('show')).toEqual(true) - expect(collapseOneTwo.classList.contains('show')).toEqual(true) - - expect(collapseTwoOne.classList.contains('show')).toEqual(false) - expect(collapseTwoTwo.classList.contains('show')).toEqual(false) - - triggerTwo.click() - } - - function secondTest() { - expect(collapseOneOne.classList.contains('show')).toEqual(false) - expect(collapseOneTwo.classList.contains('show')).toEqual(false) - - expect(collapseTwoOne.classList.contains('show')).toEqual(true) - expect(collapseTwoTwo.classList.contains('show')).toEqual(true) - done() - } - - collapseOneOne.addEventListener('shown.bs.collapse', () => { - if (collapsedElements.one) { - firstTest() - } else { - collapsedElements.one = true - } - }) - - collapseOneTwo.addEventListener('shown.bs.collapse', () => { - if (collapsedElements.one) { - firstTest() - } else { - collapsedElements.one = true - } - }) - - collapseTwoOne.addEventListener('shown.bs.collapse', () => { - if (collapsedElements.two) { - secondTest() - } else { - collapsedElements.two = true - } - }) - - collapseTwoTwo.addEventListener('shown.bs.collapse', () => { - if (collapsedElements.two) { - secondTest() - } else { - collapsedElements.two = true - } - }) - - trigger.click() - }) - - it('should collapse accordion children but not nested accordion children', done => { - fixtureEl.innerHTML = [ - '
', - '
', - ' ', - '
', - '
', - '
', - ' ', - '
', - '
', - '
', - '
', - '
', - '
', - ' ', - '
', - '
', - '
' - ].join('') - - const trigger = fixtureEl.querySelector('#linkTrigger') - const triggerTwo = fixtureEl.querySelector('#linkTriggerTwo') - const nestedTrigger = fixtureEl.querySelector('#nestedLinkTrigger') - const collapseOne = fixtureEl.querySelector('#collapseOne') - const collapseTwo = fixtureEl.querySelector('#collapseTwo') - const nestedCollapseOne = fixtureEl.querySelector('#nestedCollapseOne') - - function handlerCollapseOne() { - expect(collapseOne.classList.contains('show')).toEqual(true) - expect(collapseTwo.classList.contains('show')).toEqual(false) - expect(nestedCollapseOne.classList.contains('show')).toEqual(false) - - nestedCollapseOne.addEventListener('shown.bs.collapse', handlerNestedCollapseOne) - nestedTrigger.click() - collapseOne.removeEventListener('shown.bs.collapse', handlerCollapseOne) - } - - function handlerNestedCollapseOne() { - expect(collapseOne.classList.contains('show')).toEqual(true) - expect(collapseTwo.classList.contains('show')).toEqual(false) - expect(nestedCollapseOne.classList.contains('show')).toEqual(true) - - collapseTwo.addEventListener('shown.bs.collapse', () => { - expect(collapseOne.classList.contains('show')).toEqual(false) - expect(collapseTwo.classList.contains('show')).toEqual(true) - expect(nestedCollapseOne.classList.contains('show')).toEqual(true) - done() - }) - - triggerTwo.click() - nestedCollapseOne.removeEventListener('shown.bs.collapse', handlerNestedCollapseOne) - } - - collapseOne.addEventListener('shown.bs.collapse', handlerCollapseOne) - trigger.click() - }) - - it('should add "collapsed" class and set aria-expanded to triggers only when all the targeted collapse are hidden', done => { - fixtureEl.innerHTML = [ - '', - '', - '', - '
', - '
' - ].join('') - - const trigger1 = fixtureEl.querySelector('#trigger1') - const trigger2 = fixtureEl.querySelector('#trigger2') - const trigger3 = fixtureEl.querySelector('#trigger3') - const target1 = fixtureEl.querySelector('#test1') - const target2 = fixtureEl.querySelector('#test2') - - const target2Shown = () => { - expect(trigger1.classList.contains('collapsed')).toEqual(false) - expect(trigger1.getAttribute('aria-expanded')).toEqual('true') - - expect(trigger2.classList.contains('collapsed')).toEqual(false) - expect(trigger2.getAttribute('aria-expanded')).toEqual('true') - - expect(trigger3.classList.contains('collapsed')).toEqual(false) - expect(trigger3.getAttribute('aria-expanded')).toEqual('true') - - target2.addEventListener('hidden.bs.collapse', () => { - expect(trigger1.classList.contains('collapsed')).toEqual(false) - expect(trigger1.getAttribute('aria-expanded')).toEqual('true') - - expect(trigger2.classList.contains('collapsed')).toEqual(true) - expect(trigger2.getAttribute('aria-expanded')).toEqual('false') - - expect(trigger3.classList.contains('collapsed')).toEqual(false) - expect(trigger3.getAttribute('aria-expanded')).toEqual('true') - - target1.addEventListener('hidden.bs.collapse', () => { - expect(trigger1.classList.contains('collapsed')).toEqual(true) - expect(trigger1.getAttribute('aria-expanded')).toEqual('false') - - expect(trigger2.classList.contains('collapsed')).toEqual(true) - expect(trigger2.getAttribute('aria-expanded')).toEqual('false') - - expect(trigger3.classList.contains('collapsed')).toEqual(true) - expect(trigger3.getAttribute('aria-expanded')).toEqual('false') - done() - }) - - trigger1.click() - }) - - trigger2.click() - } - - target2.addEventListener('shown.bs.collapse', target2Shown) - trigger3.click() - }) - }) - - describe('jQueryInterface', () => { - it('should create a collapse', () => { - fixtureEl.innerHTML = '
' - - const div = fixtureEl.querySelector('div') - - jQueryMock.fn.collapse = Collapse.jQueryInterface - jQueryMock.elements = [div] - - jQueryMock.fn.collapse.call(jQueryMock) - - expect(Collapse.getInstance(div)).toBeDefined() - }) - - it('should not re create a collapse', () => { - fixtureEl.innerHTML = '
' - - const div = fixtureEl.querySelector('div') - const collapse = new Collapse(div) - - jQueryMock.fn.collapse = Collapse.jQueryInterface - jQueryMock.elements = [div] - - jQueryMock.fn.collapse.call(jQueryMock) - - expect(Collapse.getInstance(div)).toEqual(collapse) - }) - - it('should throw error on undefined method', () => { - fixtureEl.innerHTML = '
' - - const div = fixtureEl.querySelector('div') - const action = 'undefinedMethod' - - jQueryMock.fn.collapse = Collapse.jQueryInterface - jQueryMock.elements = [div] - - try { - jQueryMock.fn.collapse.call(jQueryMock, action) - } catch (error) { - expect(error.message).toEqual(`No method named "${action}"`) - } - }) - }) - - describe('getInstance', () => { - it('should return collapse instance', () => { - fixtureEl.innerHTML = '
' - - const div = fixtureEl.querySelector('div') - const collapse = new Collapse(div) - - expect(Collapse.getInstance(div)).toEqual(collapse) - }) - - it('should return null when there is no collapse instance', () => { - fixtureEl.innerHTML = '
' - - const div = fixtureEl.querySelector('div') - - expect(Collapse.getInstance(div)).toEqual(null) - }) - }) -}) diff --git a/js/src/dom/data.spec.js b/js/src/dom/data.spec.js deleted file mode 100644 index 46018dd5c..000000000 --- a/js/src/dom/data.spec.js +++ /dev/null @@ -1,131 +0,0 @@ -import Data from './data' - -/** Test helpers */ -import { getFixture, clearFixture } from '../../tests/helpers/fixture' - -describe('Data', () => { - let fixtureEl - - beforeAll(() => { - fixtureEl = getFixture() - }) - - afterEach(() => { - clearFixture() - }) - - describe('setData', () => { - it('should set data in an element by adding a key attribute', () => { - fixtureEl.innerHTML = '
' - - const div = fixtureEl.querySelector('div') - const data = { - test: 'bsData' - } - - Data.setData(div, 'test', data) - expect(div.key).toBeDefined() - }) - - it('should change data if something is already stored', () => { - fixtureEl.innerHTML = '
' - - const div = fixtureEl.querySelector('div') - const data = { - test: 'bsData' - } - - Data.setData(div, 'test', data) - - data.test = 'bsData2' - Data.setData(div, 'test', data) - - expect(div.key).toBeDefined() - }) - }) - - describe('getData', () => { - it('should return stored data', () => { - fixtureEl.innerHTML = '
' - - const div = fixtureEl.querySelector('div') - const data = { - test: 'bsData' - } - - Data.setData(div, 'test', data) - expect(Data.getData(div, 'test')).toEqual(data) - }) - - it('should return null on undefined element', () => { - expect(Data.getData(null)).toEqual(null) - expect(Data.getData(undefined)).toEqual(null) - }) - - it('should return null when an element have nothing stored', () => { - fixtureEl.innerHTML = '
' - - const div = fixtureEl.querySelector('div') - - expect(Data.getData(div, 'test')).toEqual(null) - }) - - it('should return null when an element have nothing stored with the provided key', () => { - fixtureEl.innerHTML = '
' - - const div = fixtureEl.querySelector('div') - const data = { - test: 'bsData' - } - - Data.setData(div, 'test', data) - - expect(Data.getData(div, 'test2')).toEqual(null) - }) - }) - - describe('removeData', () => { - it('should do nothing when an element have nothing stored', () => { - fixtureEl.innerHTML = '
' - - const div = fixtureEl.querySelector('div') - - Data.removeData(div, 'test') - expect().nothing() - }) - - it('should should do nothing if it\'s not a valid key provided', () => { - fixtureEl.innerHTML = '
' - - const div = fixtureEl.querySelector('div') - const data = { - test: 'bsData' - } - - Data.setData(div, 'test', data) - - expect(div.key).toBeDefined() - - Data.removeData(div, 'test2') - - expect(div.key).toBeDefined() - }) - - it('should remove data if something is stored', () => { - fixtureEl.innerHTML = '
' - - const div = fixtureEl.querySelector('div') - const data = { - test: 'bsData' - } - - Data.setData(div, 'test', data) - - expect(div.key).toBeDefined() - - Data.removeData(div, 'test') - - expect(div.key).toBeUndefined() - }) - }) -}) diff --git a/js/src/dom/event-handler.spec.js b/js/src/dom/event-handler.spec.js deleted file mode 100644 index dc5c5c30c..000000000 --- a/js/src/dom/event-handler.spec.js +++ /dev/null @@ -1,327 +0,0 @@ -import EventHandler from './event-handler' - -/** Test helpers */ -import { getFixture, clearFixture } from '../../tests/helpers/fixture' - -describe('EventHandler', () => { - let fixtureEl - - beforeAll(() => { - fixtureEl = getFixture() - }) - - afterEach(() => { - clearFixture() - }) - - describe('on', () => { - it('should not add event listener if the event is not a string', () => { - fixtureEl.innerHTML = '
' - - const div = fixtureEl.querySelector('div') - - EventHandler.on(div, null, () => {}) - EventHandler.on(null, 'click', () => {}) - - expect().nothing() - }) - - it('should add event listener', done => { - fixtureEl.innerHTML = '
' - - const div = fixtureEl.querySelector('div') - - EventHandler.on(div, 'click', () => { - expect().nothing() - done() - }) - - div.click() - }) - - it('should add namespaced event listener', done => { - fixtureEl.innerHTML = '
' - - const div = fixtureEl.querySelector('div') - - EventHandler.on(div, 'bs.namespace', () => { - expect().nothing() - done() - }) - - EventHandler.trigger(div, 'bs.namespace') - }) - - it('should add native namespaced event listener', done => { - fixtureEl.innerHTML = '
' - - const div = fixtureEl.querySelector('div') - - EventHandler.on(div, 'click.namespace', () => { - expect().nothing() - done() - }) - - EventHandler.trigger(div, 'click') - }) - - it('should handle event delegation', done => { - EventHandler.on(document, 'click', '.test', () => { - expect().nothing() - done() - }) - - fixtureEl.innerHTML = '
' - - const div = fixtureEl.querySelector('div') - - div.click() - }) - }) - - describe('one', () => { - it('should call listener just one', done => { - fixtureEl.innerHTML = '
' - - let called = 0 - const div = fixtureEl.querySelector('div') - const obj = { - oneListener() { - called++ - } - } - - EventHandler.one(div, 'bootstrap', obj.oneListener) - - EventHandler.trigger(div, 'bootstrap') - EventHandler.trigger(div, 'bootstrap') - - setTimeout(() => { - expect(called).toEqual(1) - done() - }, 20) - }) - }) - - describe('off', () => { - it('should not remove a listener', () => { - fixtureEl.innerHTML = '
' - const div = fixtureEl.querySelector('div') - - EventHandler.off(div, null, () => {}) - EventHandler.off(null, 'click', () => {}) - expect().nothing() - }) - - it('should remove a listener', done => { - fixtureEl.innerHTML = '
' - const div = fixtureEl.querySelector('div') - - let called = 0 - const handler = () => { - called++ - } - - EventHandler.on(div, 'foobar', handler) - EventHandler.trigger(div, 'foobar') - - EventHandler.off(div, 'foobar', handler) - EventHandler.trigger(div, 'foobar') - - setTimeout(() => { - expect(called).toEqual(1) - done() - }, 20) - }) - - it('should remove all the events', done => { - fixtureEl.innerHTML = '
' - const div = fixtureEl.querySelector('div') - - let called = 0 - - EventHandler.on(div, 'foobar', () => { - called++ - }) - EventHandler.on(div, 'foobar', () => { - called++ - }) - EventHandler.trigger(div, 'foobar') - - EventHandler.off(div, 'foobar') - EventHandler.trigger(div, 'foobar') - - setTimeout(() => { - expect(called).toEqual(2) - done() - }, 20) - }) - - it('should remove all the namespaced listeners if namespace is passed', done => { - fixtureEl.innerHTML = '
' - const div = fixtureEl.querySelector('div') - - let called = 0 - - EventHandler.on(div, 'foobar.namespace', () => { - called++ - }) - EventHandler.on(div, 'foofoo.namespace', () => { - called++ - }) - EventHandler.trigger(div, 'foobar.namespace') - EventHandler.trigger(div, 'foofoo.namespace') - - EventHandler.off(div, '.namespace') - EventHandler.trigger(div, 'foobar.namespace') - EventHandler.trigger(div, 'foofoo.namespace') - - setTimeout(() => { - expect(called).toEqual(2) - done() - }, 20) - }) - - it('should remove the namespaced listeners', done => { - fixtureEl.innerHTML = '
' - const div = fixtureEl.querySelector('div') - - let calledCallback1 = 0 - let calledCallback2 = 0 - - EventHandler.on(div, 'foobar.namespace', () => { - calledCallback1++ - }) - EventHandler.on(div, 'foofoo.namespace', () => { - calledCallback2++ - }) - - EventHandler.trigger(div, 'foobar.namespace') - EventHandler.off(div, 'foobar.namespace') - EventHandler.trigger(div, 'foobar.namespace') - - EventHandler.trigger(div, 'foofoo.namespace') - - setTimeout(() => { - expect(calledCallback1).toEqual(1) - expect(calledCallback2).toEqual(1) - done() - }, 20) - }) - - it('should remove the all the namespaced listeners for native events', done => { - fixtureEl.innerHTML = '
' - const div = fixtureEl.querySelector('div') - - let called = 0 - - EventHandler.on(div, 'click.namespace', () => { - called++ - }) - EventHandler.on(div, 'click.namespace2', () => { - called++ - }) - - EventHandler.trigger(div, 'click') - EventHandler.off(div, 'click') - EventHandler.trigger(div, 'click') - - setTimeout(() => { - expect(called).toEqual(2) - done() - }, 20) - }) - - it('should remove the specified namespaced listeners for native events', done => { - fixtureEl.innerHTML = '
' - const div = fixtureEl.querySelector('div') - - let called1 = 0 - let called2 = 0 - - EventHandler.on(div, 'click.namespace', () => { - called1++ - }) - EventHandler.on(div, 'click.namespace2', () => { - called2++ - }) - EventHandler.trigger(div, 'click') - - EventHandler.off(div, 'click.namespace') - EventHandler.trigger(div, 'click') - - setTimeout(() => { - expect(called1).toEqual(1) - expect(called2).toEqual(2) - done() - }, 20) - }) - - it('should remove a listener registered by .one', done => { - fixtureEl.innerHTML = '
' - - const div = fixtureEl.querySelector('div') - const handler = () => { - throw new Error('called') - } - - EventHandler.one(div, 'foobar', handler) - EventHandler.off(div, 'foobar', handler) - - EventHandler.trigger(div, 'foobar') - setTimeout(() => { - expect().nothing() - done() - }, 20) - }) - - it('should remove the correct delegated event listener', () => { - const element = document.createElement('div') - const subelement = document.createElement('span') - element.appendChild(subelement) - - const anchor = document.createElement('a') - element.appendChild(anchor) - - let i = 0 - const handler = () => { - i++ - } - - EventHandler.on(element, 'click', 'a', handler) - EventHandler.on(element, 'click', 'span', handler) - - fixtureEl.appendChild(element) - - EventHandler.trigger(anchor, 'click') - EventHandler.trigger(subelement, 'click') - - // first listeners called - expect(i === 2).toEqual(true) - - EventHandler.off(element, 'click', 'span', handler) - EventHandler.trigger(subelement, 'click') - - // removed listener not called - expect(i === 2).toEqual(true) - - EventHandler.trigger(anchor, 'click') - - // not removed listener called - expect(i === 3).toEqual(true) - - EventHandler.on(element, 'click', 'span', handler) - EventHandler.trigger(anchor, 'click') - EventHandler.trigger(subelement, 'click') - - // listener re-registered - expect(i === 5).toEqual(true) - - EventHandler.off(element, 'click', 'span') - EventHandler.trigger(subelement, 'click') - - // listener removed again - expect(i === 5).toEqual(true) - }) - }) -}) diff --git a/js/src/dom/manipulator.spec.js b/js/src/dom/manipulator.spec.js deleted file mode 100644 index e96c068d4..000000000 --- a/js/src/dom/manipulator.spec.js +++ /dev/null @@ -1,158 +0,0 @@ -import Manipulator from './manipulator' - -/** Test helpers */ -import { getFixture, clearFixture } from '../../tests/helpers/fixture' - -describe('Manipulator', () => { - let fixtureEl - - beforeAll(() => { - fixtureEl = getFixture() - }) - - afterEach(() => { - clearFixture() - }) - - describe('setDataAttribute', () => { - it('should set data attribute', () => { - fixtureEl.innerHTML = '
' - - const div = fixtureEl.querySelector('div') - - Manipulator.setDataAttribute(div, 'key', 'value') - expect(div.getAttribute('data-key')).toEqual('value') - }) - - it('should set data attribute in lower case', () => { - fixtureEl.innerHTML = '
' - - const div = fixtureEl.querySelector('div') - - Manipulator.setDataAttribute(div, 'tEsT', 'value') - expect(div.getAttribute('data-test')).toEqual('value') - }) - }) - - describe('removeDataAttribute', () => { - it('should remove data attribute', () => { - fixtureEl.innerHTML = '
' - - const div = fixtureEl.querySelector('div') - - Manipulator.removeDataAttribute(div, 'key') - expect(div.getAttribute('data-key')).toBeNull() - }) - - it('should remove data attribute in lower case', () => { - fixtureEl.innerHTML = '
' - - const div = fixtureEl.querySelector('div') - - Manipulator.removeDataAttribute(div, 'tEStKeY') - expect(div.getAttribute('data-testkey')).toBeNull() - }) - }) - - describe('getDataAttributes', () => { - it('should return empty object for null', () => { - expect(Manipulator.getDataAttributes(null), {}) - expect().nothing() - }) - - it('should get all data attributes', () => { - fixtureEl.innerHTML = '
' - - const div = fixtureEl.querySelector('div') - - expect(Manipulator.getDataAttributes(div)).toEqual({ - test: 'js', - test2: 'js2' - }) - }) - }) - - describe('getDataAttribute', () => { - it('should get data attribute', () => { - fixtureEl.innerHTML = '
' - - const div = fixtureEl.querySelector('div') - - expect(Manipulator.getDataAttribute(div, 'test')).toBeNull() - }) - - it('should get data attribute in lower case', () => { - fixtureEl.innerHTML = '
' - - const div = fixtureEl.querySelector('div') - - expect(Manipulator.getDataAttribute(div, 'tEsT')).toEqual('value') - }) - - it('should normalize data', () => { - fixtureEl.innerHTML = '
' - - const div = fixtureEl.querySelector('div') - - expect(Manipulator.getDataAttribute(div, 'test')).toEqual(false) - - div.setAttribute('data-test', 'true') - expect(Manipulator.getDataAttribute(div, 'test')).toEqual(true) - - div.setAttribute('data-test', '1') - expect(Manipulator.getDataAttribute(div, 'test')).toEqual(1) - }) - }) - - describe('offset', () => { - it('should return object with two properties top and left, both numbers', () => { - fixtureEl.innerHTML = '
' - - const div = fixtureEl.querySelector('div') - const offset = Manipulator.offset(div) - - expect(offset).toBeDefined() - expect(offset.top).toEqual(jasmine.any(Number)) - expect(offset.left).toEqual(jasmine.any(Number)) - }) - }) - - describe('position', () => { - it('should return object with two properties top and left, both numbers', () => { - fixtureEl.innerHTML = '
' - - const div = fixtureEl.querySelector('div') - const position = Manipulator.position(div) - - expect(position).toBeDefined() - expect(position.top).toEqual(jasmine.any(Number)) - expect(position.left).toEqual(jasmine.any(Number)) - }) - }) - - describe('toggleClass', () => { - it('should not error out if element is null or undefined', () => { - Manipulator.toggleClass(null, 'test') - Manipulator.toggleClass(undefined, 'test') - expect().nothing() - }) - - it('should add class if it is missing', () => { - fixtureEl.innerHTML = '
' - - const div = fixtureEl.querySelector('div') - - Manipulator.toggleClass(div, 'test') - expect(div.classList.contains('test')).toEqual(true) - }) - - it('should remove class if it is set', () => { - fixtureEl.innerHTML = '
' - - const div = fixtureEl.querySelector('div') - - Manipulator.toggleClass(div, 'test') - expect(div.classList.contains('test')).toEqual(false) - }) - }) -}) diff --git a/js/src/dom/selector-engine.spec.js b/js/src/dom/selector-engine.spec.js deleted file mode 100644 index 28ccdf40b..000000000 --- a/js/src/dom/selector-engine.spec.js +++ /dev/null @@ -1,115 +0,0 @@ -import SelectorEngine from './selector-engine' -import { makeArray } from '../util/index' - -/** Test helpers */ -import { getFixture, clearFixture } from '../../tests/helpers/fixture' - -describe('SelectorEngine', () => { - let fixtureEl - - beforeAll(() => { - fixtureEl = getFixture() - }) - - afterEach(() => { - clearFixture() - }) - - describe('matches', () => { - it('should return matched elements', () => { - fixtureEl.innerHTML = '
' - - expect(SelectorEngine.matches(fixtureEl, 'div')).toEqual(true) - }) - }) - - describe('find', () => { - it('should find elements', () => { - fixtureEl.innerHTML = '
' - - const div = fixtureEl.querySelector('div') - - expect(makeArray(SelectorEngine.find('div', fixtureEl))).toEqual([div]) - }) - - it('should find elements globaly', () => { - fixtureEl.innerHTML = '
' - - const div = fixtureEl.querySelector('#test') - - expect(makeArray(SelectorEngine.find('#test'))).toEqual([div]) - }) - - it('should handle :scope selectors', () => { - fixtureEl.innerHTML = `
` - - const listEl = fixtureEl.querySelector('ul') - const aActive = fixtureEl.querySelector('.active') - - expect(makeArray(SelectorEngine.find(':scope > li > .active', listEl))).toEqual([aActive]) - }) - }) - - describe('findOne', () => { - it('should return one element', () => { - fixtureEl.innerHTML = '
' - - const div = fixtureEl.querySelector('#test') - - expect(SelectorEngine.findOne('#test')).toEqual(div) - }) - }) - - describe('children', () => { - it('should find children', () => { - fixtureEl.innerHTML = `
    -
  • -
  • -
  • -
` - - const list = fixtureEl.querySelector('ul') - const liList = makeArray(fixtureEl.querySelectorAll('li')) - const result = makeArray(SelectorEngine.children(list, 'li')) - - expect(result).toEqual(liList) - }) - }) - - describe('parents', () => { - it('should return parents', () => { - expect(SelectorEngine.parents(fixtureEl, 'body').length).toEqual(1) - }) - }) - - describe('prev', () => { - it('should return previous element', () => { - fixtureEl.innerHTML = '
' - - const btn = fixtureEl.querySelector('.btn') - const divTest = fixtureEl.querySelector('.test') - - expect(SelectorEngine.prev(btn, '.test')).toEqual([divTest]) - }) - - it('should return previous element with an extra element between', () => { - fixtureEl.innerHTML = [ - '
', - '', - '' - ].join('') - - const btn = fixtureEl.querySelector('.btn') - const divTest = fixtureEl.querySelector('.test') - - expect(SelectorEngine.prev(btn, '.test')).toEqual([divTest]) - }) - }) -}) - diff --git a/js/src/dropdown.js b/js/src/dropdown.js new file mode 100644 index 000000000..06a271ef8 --- /dev/null +++ b/js/src/dropdown.js @@ -0,0 +1,545 @@ +/** + * -------------------------------------------------------------------------- + * Bootstrap (v4.3.1): dropdown.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + * -------------------------------------------------------------------------- + */ + +import { + getjQuery, + getElementFromSelector, + isElement, + makeArray, + noop, + typeCheckConfig +} from './util/index' +import Data from './dom/data' +import EventHandler from './dom/event-handler' +import Manipulator from './dom/manipulator' +import Popper from 'popper.js' +import SelectorEngine from './dom/selector-engine' + +/** + * ------------------------------------------------------------------------ + * Constants + * ------------------------------------------------------------------------ + */ + +const NAME = 'dropdown' +const VERSION = '4.3.1' +const DATA_KEY = 'bs.dropdown' +const EVENT_KEY = `.${DATA_KEY}` +const DATA_API_KEY = '.data-api' +const ESCAPE_KEYCODE = 27 // KeyboardEvent.which value for Escape (Esc) key +const SPACE_KEYCODE = 32 // KeyboardEvent.which value for space key +const TAB_KEYCODE = 9 // KeyboardEvent.which value for tab key +const ARROW_UP_KEYCODE = 38 // KeyboardEvent.which value for up arrow key +const ARROW_DOWN_KEYCODE = 40 // KeyboardEvent.which value for down arrow key +const RIGHT_MOUSE_BUTTON_WHICH = 3 // MouseEvent.which value for the right button (assuming a right-handed mouse) +const REGEXP_KEYDOWN = new RegExp(`${ARROW_UP_KEYCODE}|${ARROW_DOWN_KEYCODE}|${ESCAPE_KEYCODE}`) + +const Event = { + HIDE: `hide${EVENT_KEY}`, + HIDDEN: `hidden${EVENT_KEY}`, + SHOW: `show${EVENT_KEY}`, + SHOWN: `shown${EVENT_KEY}`, + CLICK: `click${EVENT_KEY}`, + CLICK_DATA_API: `click${EVENT_KEY}${DATA_API_KEY}`, + KEYDOWN_DATA_API: `keydown${EVENT_KEY}${DATA_API_KEY}`, + KEYUP_DATA_API: `keyup${EVENT_KEY}${DATA_API_KEY}` +} + +const ClassName = { + DISABLED: 'disabled', + SHOW: 'show', + DROPUP: 'dropup', + DROPRIGHT: 'dropright', + DROPLEFT: 'dropleft', + MENURIGHT: 'dropdown-menu-right', + POSITION_STATIC: 'position-static' +} + +const Selector = { + DATA_TOGGLE: '[data-toggle="dropdown"]', + FORM_CHILD: '.dropdown form', + MENU: '.dropdown-menu', + NAVBAR_NAV: '.navbar-nav', + VISIBLE_ITEMS: '.dropdown-menu .dropdown-item:not(.disabled):not(:disabled)' +} + +const AttachmentMap = { + TOP: 'top-start', + TOPEND: 'top-end', + BOTTOM: 'bottom-start', + BOTTOMEND: 'bottom-end', + RIGHT: 'right-start', + RIGHTEND: 'right-end', + LEFT: 'left-start', + LEFTEND: 'left-end' +} + +const Default = { + offset: 0, + flip: true, + boundary: 'scrollParent', + reference: 'toggle', + display: 'dynamic', + popperConfig: null +} + +const DefaultType = { + offset: '(number|string|function)', + flip: 'boolean', + boundary: '(string|element)', + reference: '(string|element)', + display: 'string', + popperConfig: '(null|object)' +} + +/** + * ------------------------------------------------------------------------ + * Class Definition + * ------------------------------------------------------------------------ + */ + +class Dropdown { + constructor(element, config) { + this._element = element + this._popper = null + this._config = this._getConfig(config) + this._menu = this._getMenuElement() + this._inNavbar = this._detectNavbar() + + this._addEventListeners() + Data.setData(element, DATA_KEY, this) + } + + // Getters + + static get VERSION() { + return VERSION + } + + static get Default() { + return Default + } + + static get DefaultType() { + return DefaultType + } + + // Public + + toggle() { + if (this._element.disabled || this._element.classList.contains(ClassName.DISABLED)) { + return + } + + const isActive = this._menu.classList.contains(ClassName.SHOW) + + Dropdown.clearMenus() + + if (isActive) { + return + } + + this.show() + } + + show() { + if (this._element.disabled || this._element.classList.contains(ClassName.DISABLED) || this._menu.classList.contains(ClassName.SHOW)) { + return + } + + const parent = Dropdown.getParentFromElement(this._element) + const relatedTarget = { + relatedTarget: this._element + } + + const showEvent = EventHandler.trigger(parent, Event.SHOW, relatedTarget) + + if (showEvent.defaultPrevented) { + return + } + + // Disable totally Popper.js for Dropdown in Navbar + if (!this._inNavbar) { + if (typeof Popper === 'undefined') { + throw new TypeError('Bootstrap\'s dropdowns require Popper.js (https://popper.js.org)') + } + + let referenceElement = this._element + + if (this._config.reference === 'parent') { + referenceElement = parent + } else if (isElement(this._config.reference)) { + referenceElement = this._config.reference + + // Check if it's jQuery element + if (typeof this._config.reference.jquery !== 'undefined') { + referenceElement = this._config.reference[0] + } + } + + // If boundary is not `scrollParent`, then set position to `static` + // to allow the menu to "escape" the scroll parent's boundaries + // https://github.com/twbs/bootstrap/issues/24251 + if (this._config.boundary !== 'scrollParent') { + parent.classList.add(ClassName.POSITION_STATIC) + } + + this._popper = new Popper(referenceElement, this._menu, this._getPopperConfig()) + } + + // 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 && + !makeArray(SelectorEngine.closest(parent, Selector.NAVBAR_NAV)).length) { + makeArray(document.body.children) + .forEach(elem => EventHandler.on(elem, 'mouseover', null, noop())) + } + + this._element.focus() + this._element.setAttribute('aria-expanded', true) + + Manipulator.toggleClass(this._menu, ClassName.SHOW) + Manipulator.toggleClass(parent, ClassName.SHOW) + EventHandler.trigger(parent, Event.SHOWN, relatedTarget) + } + + hide() { + if (this._element.disabled || this._element.classList.contains(ClassName.DISABLED) || !this._menu.classList.contains(ClassName.SHOW)) { + return + } + + const parent = Dropdown.getParentFromElement(this._element) + const relatedTarget = { + relatedTarget: this._element + } + + const hideEvent = EventHandler.trigger(parent, Event.HIDE, relatedTarget) + + if (hideEvent.defaultPrevented) { + return + } + + if (this._popper) { + this._popper.destroy() + } + + Manipulator.toggleClass(this._menu, ClassName.SHOW) + Manipulator.toggleClass(parent, ClassName.SHOW) + EventHandler.trigger(parent, Event.HIDDEN, relatedTarget) + } + + dispose() { + Data.removeData(this._element, DATA_KEY) + EventHandler.off(this._element, EVENT_KEY) + this._element = null + this._menu = null + if (this._popper) { + this._popper.destroy() + this._popper = null + } + } + + update() { + this._inNavbar = this._detectNavbar() + if (this._popper) { + this._popper.scheduleUpdate() + } + } + + // Private + + _addEventListeners() { + EventHandler.on(this._element, Event.CLICK, event => { + event.preventDefault() + event.stopPropagation() + this.toggle() + }) + } + + _getConfig(config) { + config = { + ...this.constructor.Default, + ...Manipulator.getDataAttributes(this._element), + ...config + } + + typeCheckConfig( + NAME, + config, + this.constructor.DefaultType + ) + + return config + } + + _getMenuElement() { + const parent = Dropdown.getParentFromElement(this._element) + + return SelectorEngine.findOne(Selector.MENU, parent) + } + + _getPlacement() { + const parentDropdown = this._element.parentNode + let placement = AttachmentMap.BOTTOM + + // Handle dropup + if (parentDropdown.classList.contains(ClassName.DROPUP)) { + placement = AttachmentMap.TOP + if (this._menu.classList.contains(ClassName.MENURIGHT)) { + placement = AttachmentMap.TOPEND + } + } else if (parentDropdown.classList.contains(ClassName.DROPRIGHT)) { + placement = AttachmentMap.RIGHT + } else if (parentDropdown.classList.contains(ClassName.DROPLEFT)) { + placement = AttachmentMap.LEFT + } else if (this._menu.classList.contains(ClassName.MENURIGHT)) { + placement = AttachmentMap.BOTTOMEND + } + + return placement + } + + _detectNavbar() { + return Boolean(SelectorEngine.closest(this._element, '.navbar')) + } + + _getOffset() { + const offset = {} + + if (typeof this._config.offset === 'function') { + offset.fn = data => { + data.offsets = { + ...data.offsets, + ...this._config.offset(data.offsets, this._element) || {} + } + + return data + } + } else { + offset.offset = this._config.offset + } + + return offset + } + + _getPopperConfig() { + const popperConfig = { + placement: this._getPlacement(), + modifiers: { + offset: this._getOffset(), + flip: { + enabled: this._config.flip + }, + preventOverflow: { + boundariesElement: this._config.boundary + } + } + } + + // Disable Popper.js if we have a static display + if (this._config.display === 'static') { + popperConfig.modifiers.applyStyle = { + enabled: false + } + } + + return { + ...popperConfig, + ...this._config.popperConfig + } + } + + // Static + + static dropdownInterface(element, config) { + let data = Data.getData(element, DATA_KEY) + const _config = typeof config === 'object' ? config : null + + if (!data) { + data = new Dropdown(element, _config) + } + + if (typeof config === 'string') { + if (typeof data[config] === 'undefined') { + throw new TypeError(`No method named "${config}"`) + } + + data[config]() + } + } + + static jQueryInterface(config) { + return this.each(function () { + Dropdown.dropdownInterface(this, config) + }) + } + + static clearMenus(event) { + if (event && (event.which === RIGHT_MOUSE_BUTTON_WHICH || + (event.type === 'keyup' && event.which !== TAB_KEYCODE))) { + return + } + + const toggles = makeArray(SelectorEngine.find(Selector.DATA_TOGGLE)) + for (let i = 0, len = toggles.length; i < len; i++) { + const parent = Dropdown.getParentFromElement(toggles[i]) + const context = Data.getData(toggles[i], DATA_KEY) + const relatedTarget = { + relatedTarget: toggles[i] + } + + if (event && event.type === 'click') { + relatedTarget.clickEvent = event + } + + if (!context) { + continue + } + + const dropdownMenu = context._menu + if (!parent.classList.contains(ClassName.SHOW)) { + continue + } + + if (event && ((event.type === 'click' && + /input|textarea/i.test(event.target.tagName)) || + (event.type === 'keyup' && event.which === TAB_KEYCODE)) && + parent.contains(event.target)) { + continue + } + + const hideEvent = EventHandler.trigger(parent, Event.HIDE, relatedTarget) + if (hideEvent.defaultPrevented) { + continue + } + + // If this is a touch-enabled device we remove the extra + // empty mouseover listeners we added for iOS support + if ('ontouchstart' in document.documentElement) { + makeArray(document.body.children) + .forEach(elem => EventHandler.off(elem, 'mouseover', null, noop())) + } + + toggles[i].setAttribute('aria-expanded', 'false') + + if (context._popper) { + context._popper.destroy() + } + + dropdownMenu.classList.remove(ClassName.SHOW) + parent.classList.remove(ClassName.SHOW) + EventHandler.trigger(parent, Event.HIDDEN, relatedTarget) + } + } + + static getParentFromElement(element) { + return getElementFromSelector(element) || element.parentNode + } + + static dataApiKeydownHandler(event) { + // If not input/textarea: + // - And not a key in REGEXP_KEYDOWN => not a dropdown command + // If input/textarea: + // - If space key => not a dropdown command + // - If key is other than escape + // - If key is not up or down => not a dropdown command + // - If trigger inside the menu => not a dropdown command + if (/input|textarea/i.test(event.target.tagName) ? + event.which === SPACE_KEYCODE || (event.which !== ESCAPE_KEYCODE && + ((event.which !== ARROW_DOWN_KEYCODE && event.which !== ARROW_UP_KEYCODE) || + SelectorEngine.closest(event.target, Selector.MENU))) : + !REGEXP_KEYDOWN.test(event.which)) { + return + } + + event.preventDefault() + event.stopPropagation() + + if (this.disabled || this.classList.contains(ClassName.DISABLED)) { + return + } + + const parent = Dropdown.getParentFromElement(this) + const isActive = parent.classList.contains(ClassName.SHOW) + + if (!isActive || (isActive && (event.which === ESCAPE_KEYCODE || event.which === SPACE_KEYCODE))) { + if (event.which === ESCAPE_KEYCODE) { + SelectorEngine.findOne(Selector.DATA_TOGGLE, parent).focus() + } + + Dropdown.clearMenus() + return + } + + const items = makeArray(SelectorEngine.find(Selector.VISIBLE_ITEMS, parent)) + + if (!items.length) { + return + } + + let index = items.indexOf(event.target) + + if (event.which === ARROW_UP_KEYCODE && index > 0) { // Up + index-- + } + + if (event.which === ARROW_DOWN_KEYCODE && index < items.length - 1) { // Down + index++ + } + + if (index < 0) { + index = 0 + } + + items[index].focus() + } + + static getInstance(element) { + return Data.getData(element, DATA_KEY) + } +} + +/** + * ------------------------------------------------------------------------ + * Data Api implementation + * ------------------------------------------------------------------------ + */ + +EventHandler.on(document, Event.KEYDOWN_DATA_API, Selector.DATA_TOGGLE, Dropdown.dataApiKeydownHandler) +EventHandler.on(document, Event.KEYDOWN_DATA_API, Selector.MENU, Dropdown.dataApiKeydownHandler) +EventHandler.on(document, Event.CLICK_DATA_API, Dropdown.clearMenus) +EventHandler.on(document, Event.KEYUP_DATA_API, Dropdown.clearMenus) +EventHandler.on(document, Event.CLICK_DATA_API, Selector.DATA_TOGGLE, function (event) { + event.preventDefault() + event.stopPropagation() + Dropdown.dropdownInterface(this, 'toggle') +}) +EventHandler + .on(document, Event.CLICK_DATA_API, Selector.FORM_CHILD, e => e.stopPropagation()) + +const $ = getjQuery() + +/** + * ------------------------------------------------------------------------ + * jQuery + * ------------------------------------------------------------------------ + * add .dropdown to jQuery only if jQuery is present + */ +/* istanbul ignore if */ +if ($) { + const JQUERY_NO_CONFLICT = $.fn[NAME] + $.fn[NAME] = Dropdown.jQueryInterface + $.fn[NAME].Constructor = Dropdown + $.fn[NAME].noConflict = () => { + $.fn[NAME] = JQUERY_NO_CONFLICT + return Dropdown.jQueryInterface + } +} + +export default Dropdown diff --git a/js/src/dropdown/dropdown.js b/js/src/dropdown/dropdown.js deleted file mode 100644 index b84035689..000000000 --- a/js/src/dropdown/dropdown.js +++ /dev/null @@ -1,545 +0,0 @@ -/** - * -------------------------------------------------------------------------- - * Bootstrap (v4.3.1): dropdown.js - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) - * -------------------------------------------------------------------------- - */ - -import { - getjQuery, - getElementFromSelector, - isElement, - makeArray, - noop, - typeCheckConfig -} from '../util/index' -import Data from '../dom/data' -import EventHandler from '../dom/event-handler' -import Manipulator from '../dom/manipulator' -import Popper from 'popper.js' -import SelectorEngine from '../dom/selector-engine' - -/** - * ------------------------------------------------------------------------ - * Constants - * ------------------------------------------------------------------------ - */ - -const NAME = 'dropdown' -const VERSION = '4.3.1' -const DATA_KEY = 'bs.dropdown' -const EVENT_KEY = `.${DATA_KEY}` -const DATA_API_KEY = '.data-api' -const ESCAPE_KEYCODE = 27 // KeyboardEvent.which value for Escape (Esc) key -const SPACE_KEYCODE = 32 // KeyboardEvent.which value for space key -const TAB_KEYCODE = 9 // KeyboardEvent.which value for tab key -const ARROW_UP_KEYCODE = 38 // KeyboardEvent.which value for up arrow key -const ARROW_DOWN_KEYCODE = 40 // KeyboardEvent.which value for down arrow key -const RIGHT_MOUSE_BUTTON_WHICH = 3 // MouseEvent.which value for the right button (assuming a right-handed mouse) -const REGEXP_KEYDOWN = new RegExp(`${ARROW_UP_KEYCODE}|${ARROW_DOWN_KEYCODE}|${ESCAPE_KEYCODE}`) - -const Event = { - HIDE: `hide${EVENT_KEY}`, - HIDDEN: `hidden${EVENT_KEY}`, - SHOW: `show${EVENT_KEY}`, - SHOWN: `shown${EVENT_KEY}`, - CLICK: `click${EVENT_KEY}`, - CLICK_DATA_API: `click${EVENT_KEY}${DATA_API_KEY}`, - KEYDOWN_DATA_API: `keydown${EVENT_KEY}${DATA_API_KEY}`, - KEYUP_DATA_API: `keyup${EVENT_KEY}${DATA_API_KEY}` -} - -const ClassName = { - DISABLED: 'disabled', - SHOW: 'show', - DROPUP: 'dropup', - DROPRIGHT: 'dropright', - DROPLEFT: 'dropleft', - MENURIGHT: 'dropdown-menu-right', - POSITION_STATIC: 'position-static' -} - -const Selector = { - DATA_TOGGLE: '[data-toggle="dropdown"]', - FORM_CHILD: '.dropdown form', - MENU: '.dropdown-menu', - NAVBAR_NAV: '.navbar-nav', - VISIBLE_ITEMS: '.dropdown-menu .dropdown-item:not(.disabled):not(:disabled)' -} - -const AttachmentMap = { - TOP: 'top-start', - TOPEND: 'top-end', - BOTTOM: 'bottom-start', - BOTTOMEND: 'bottom-end', - RIGHT: 'right-start', - RIGHTEND: 'right-end', - LEFT: 'left-start', - LEFTEND: 'left-end' -} - -const Default = { - offset: 0, - flip: true, - boundary: 'scrollParent', - reference: 'toggle', - display: 'dynamic', - popperConfig: null -} - -const DefaultType = { - offset: '(number|string|function)', - flip: 'boolean', - boundary: '(string|element)', - reference: '(string|element)', - display: 'string', - popperConfig: '(null|object)' -} - -/** - * ------------------------------------------------------------------------ - * Class Definition - * ------------------------------------------------------------------------ - */ - -class Dropdown { - constructor(element, config) { - this._element = element - this._popper = null - this._config = this._getConfig(config) - this._menu = this._getMenuElement() - this._inNavbar = this._detectNavbar() - - this._addEventListeners() - Data.setData(element, DATA_KEY, this) - } - - // Getters - - static get VERSION() { - return VERSION - } - - static get Default() { - return Default - } - - static get DefaultType() { - return DefaultType - } - - // Public - - toggle() { - if (this._element.disabled || this._element.classList.contains(ClassName.DISABLED)) { - return - } - - const isActive = this._menu.classList.contains(ClassName.SHOW) - - Dropdown.clearMenus() - - if (isActive) { - return - } - - this.show() - } - - show() { - if (this._element.disabled || this._element.classList.contains(ClassName.DISABLED) || this._menu.classList.contains(ClassName.SHOW)) { - return - } - - const parent = Dropdown.getParentFromElement(this._element) - const relatedTarget = { - relatedTarget: this._element - } - - const showEvent = EventHandler.trigger(parent, Event.SHOW, relatedTarget) - - if (showEvent.defaultPrevented) { - return - } - - // Disable totally Popper.js for Dropdown in Navbar - if (!this._inNavbar) { - if (typeof Popper === 'undefined') { - throw new TypeError('Bootstrap\'s dropdowns require Popper.js (https://popper.js.org)') - } - - let referenceElement = this._element - - if (this._config.reference === 'parent') { - referenceElement = parent - } else if (isElement(this._config.reference)) { - referenceElement = this._config.reference - - // Check if it's jQuery element - if (typeof this._config.reference.jquery !== 'undefined') { - referenceElement = this._config.reference[0] - } - } - - // If boundary is not `scrollParent`, then set position to `static` - // to allow the menu to "escape" the scroll parent's boundaries - // https://github.com/twbs/bootstrap/issues/24251 - if (this._config.boundary !== 'scrollParent') { - parent.classList.add(ClassName.POSITION_STATIC) - } - - this._popper = new Popper(referenceElement, this._menu, this._getPopperConfig()) - } - - // 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 && - !makeArray(SelectorEngine.closest(parent, Selector.NAVBAR_NAV)).length) { - makeArray(document.body.children) - .forEach(elem => EventHandler.on(elem, 'mouseover', null, noop())) - } - - this._element.focus() - this._element.setAttribute('aria-expanded', true) - - Manipulator.toggleClass(this._menu, ClassName.SHOW) - Manipulator.toggleClass(parent, ClassName.SHOW) - EventHandler.trigger(parent, Event.SHOWN, relatedTarget) - } - - hide() { - if (this._element.disabled || this._element.classList.contains(ClassName.DISABLED) || !this._menu.classList.contains(ClassName.SHOW)) { - return - } - - const parent = Dropdown.getParentFromElement(this._element) - const relatedTarget = { - relatedTarget: this._element - } - - const hideEvent = EventHandler.trigger(parent, Event.HIDE, relatedTarget) - - if (hideEvent.defaultPrevented) { - return - } - - if (this._popper) { - this._popper.destroy() - } - - Manipulator.toggleClass(this._menu, ClassName.SHOW) - Manipulator.toggleClass(parent, ClassName.SHOW) - EventHandler.trigger(parent, Event.HIDDEN, relatedTarget) - } - - dispose() { - Data.removeData(this._element, DATA_KEY) - EventHandler.off(this._element, EVENT_KEY) - this._element = null - this._menu = null - if (this._popper) { - this._popper.destroy() - this._popper = null - } - } - - update() { - this._inNavbar = this._detectNavbar() - if (this._popper) { - this._popper.scheduleUpdate() - } - } - - // Private - - _addEventListeners() { - EventHandler.on(this._element, Event.CLICK, event => { - event.preventDefault() - event.stopPropagation() - this.toggle() - }) - } - - _getConfig(config) { - config = { - ...this.constructor.Default, - ...Manipulator.getDataAttributes(this._element), - ...config - } - - typeCheckConfig( - NAME, - config, - this.constructor.DefaultType - ) - - return config - } - - _getMenuElement() { - const parent = Dropdown.getParentFromElement(this._element) - - return SelectorEngine.findOne(Selector.MENU, parent) - } - - _getPlacement() { - const parentDropdown = this._element.parentNode - let placement = AttachmentMap.BOTTOM - - // Handle dropup - if (parentDropdown.classList.contains(ClassName.DROPUP)) { - placement = AttachmentMap.TOP - if (this._menu.classList.contains(ClassName.MENURIGHT)) { - placement = AttachmentMap.TOPEND - } - } else if (parentDropdown.classList.contains(ClassName.DROPRIGHT)) { - placement = AttachmentMap.RIGHT - } else if (parentDropdown.classList.contains(ClassName.DROPLEFT)) { - placement = AttachmentMap.LEFT - } else if (this._menu.classList.contains(ClassName.MENURIGHT)) { - placement = AttachmentMap.BOTTOMEND - } - - return placement - } - - _detectNavbar() { - return Boolean(SelectorEngine.closest(this._element, '.navbar')) - } - - _getOffset() { - const offset = {} - - if (typeof this._config.offset === 'function') { - offset.fn = data => { - data.offsets = { - ...data.offsets, - ...this._config.offset(data.offsets, this._element) || {} - } - - return data - } - } else { - offset.offset = this._config.offset - } - - return offset - } - - _getPopperConfig() { - const popperConfig = { - placement: this._getPlacement(), - modifiers: { - offset: this._getOffset(), - flip: { - enabled: this._config.flip - }, - preventOverflow: { - boundariesElement: this._config.boundary - } - } - } - - // Disable Popper.js if we have a static display - if (this._config.display === 'static') { - popperConfig.modifiers.applyStyle = { - enabled: false - } - } - - return { - ...popperConfig, - ...this._config.popperConfig - } - } - - // Static - - static dropdownInterface(element, config) { - let data = Data.getData(element, DATA_KEY) - const _config = typeof config === 'object' ? config : null - - if (!data) { - data = new Dropdown(element, _config) - } - - if (typeof config === 'string') { - if (typeof data[config] === 'undefined') { - throw new TypeError(`No method named "${config}"`) - } - - data[config]() - } - } - - static jQueryInterface(config) { - return this.each(function () { - Dropdown.dropdownInterface(this, config) - }) - } - - static clearMenus(event) { - if (event && (event.which === RIGHT_MOUSE_BUTTON_WHICH || - (event.type === 'keyup' && event.which !== TAB_KEYCODE))) { - return - } - - const toggles = makeArray(SelectorEngine.find(Selector.DATA_TOGGLE)) - for (let i = 0, len = toggles.length; i < len; i++) { - const parent = Dropdown.getParentFromElement(toggles[i]) - const context = Data.getData(toggles[i], DATA_KEY) - const relatedTarget = { - relatedTarget: toggles[i] - } - - if (event && event.type === 'click') { - relatedTarget.clickEvent = event - } - - if (!context) { - continue - } - - const dropdownMenu = context._menu - if (!parent.classList.contains(ClassName.SHOW)) { - continue - } - - if (event && ((event.type === 'click' && - /input|textarea/i.test(event.target.tagName)) || - (event.type === 'keyup' && event.which === TAB_KEYCODE)) && - parent.contains(event.target)) { - continue - } - - const hideEvent = EventHandler.trigger(parent, Event.HIDE, relatedTarget) - if (hideEvent.defaultPrevented) { - continue - } - - // If this is a touch-enabled device we remove the extra - // empty mouseover listeners we added for iOS support - if ('ontouchstart' in document.documentElement) { - makeArray(document.body.children) - .forEach(elem => EventHandler.off(elem, 'mouseover', null, noop())) - } - - toggles[i].setAttribute('aria-expanded', 'false') - - if (context._popper) { - context._popper.destroy() - } - - dropdownMenu.classList.remove(ClassName.SHOW) - parent.classList.remove(ClassName.SHOW) - EventHandler.trigger(parent, Event.HIDDEN, relatedTarget) - } - } - - static getParentFromElement(element) { - return getElementFromSelector(element) || element.parentNode - } - - static dataApiKeydownHandler(event) { - // If not input/textarea: - // - And not a key in REGEXP_KEYDOWN => not a dropdown command - // If input/textarea: - // - If space key => not a dropdown command - // - If key is other than escape - // - If key is not up or down => not a dropdown command - // - If trigger inside the menu => not a dropdown command - if (/input|textarea/i.test(event.target.tagName) ? - event.which === SPACE_KEYCODE || (event.which !== ESCAPE_KEYCODE && - ((event.which !== ARROW_DOWN_KEYCODE && event.which !== ARROW_UP_KEYCODE) || - SelectorEngine.closest(event.target, Selector.MENU))) : - !REGEXP_KEYDOWN.test(event.which)) { - return - } - - event.preventDefault() - event.stopPropagation() - - if (this.disabled || this.classList.contains(ClassName.DISABLED)) { - return - } - - const parent = Dropdown.getParentFromElement(this) - const isActive = parent.classList.contains(ClassName.SHOW) - - if (!isActive || (isActive && (event.which === ESCAPE_KEYCODE || event.which === SPACE_KEYCODE))) { - if (event.which === ESCAPE_KEYCODE) { - SelectorEngine.findOne(Selector.DATA_TOGGLE, parent).focus() - } - - Dropdown.clearMenus() - return - } - - const items = makeArray(SelectorEngine.find(Selector.VISIBLE_ITEMS, parent)) - - if (!items.length) { - return - } - - let index = items.indexOf(event.target) - - if (event.which === ARROW_UP_KEYCODE && index > 0) { // Up - index-- - } - - if (event.which === ARROW_DOWN_KEYCODE && index < items.length - 1) { // Down - index++ - } - - if (index < 0) { - index = 0 - } - - items[index].focus() - } - - static getInstance(element) { - return Data.getData(element, DATA_KEY) - } -} - -/** - * ------------------------------------------------------------------------ - * Data Api implementation - * ------------------------------------------------------------------------ - */ - -EventHandler.on(document, Event.KEYDOWN_DATA_API, Selector.DATA_TOGGLE, Dropdown.dataApiKeydownHandler) -EventHandler.on(document, Event.KEYDOWN_DATA_API, Selector.MENU, Dropdown.dataApiKeydownHandler) -EventHandler.on(document, Event.CLICK_DATA_API, Dropdown.clearMenus) -EventHandler.on(document, Event.KEYUP_DATA_API, Dropdown.clearMenus) -EventHandler.on(document, Event.CLICK_DATA_API, Selector.DATA_TOGGLE, function (event) { - event.preventDefault() - event.stopPropagation() - Dropdown.dropdownInterface(this, 'toggle') -}) -EventHandler - .on(document, Event.CLICK_DATA_API, Selector.FORM_CHILD, e => e.stopPropagation()) - -const $ = getjQuery() - -/** - * ------------------------------------------------------------------------ - * jQuery - * ------------------------------------------------------------------------ - * add .dropdown to jQuery only if jQuery is present - */ -/* istanbul ignore if */ -if ($) { - const JQUERY_NO_CONFLICT = $.fn[NAME] - $.fn[NAME] = Dropdown.jQueryInterface - $.fn[NAME].Constructor = Dropdown - $.fn[NAME].noConflict = () => { - $.fn[NAME] = JQUERY_NO_CONFLICT - return Dropdown.jQueryInterface - } -} - -export default Dropdown diff --git a/js/src/dropdown/dropdown.spec.js b/js/src/dropdown/dropdown.spec.js deleted file mode 100644 index 46374453c..000000000 --- a/js/src/dropdown/dropdown.spec.js +++ /dev/null @@ -1,1564 +0,0 @@ -import Popper from 'popper.js' - -import Dropdown from './dropdown' -import EventHandler from '../dom/event-handler' - -/** Test helpers */ -import { getFixture, clearFixture, createEvent, jQueryMock } from '../../tests/helpers/fixture' - -describe('Dropdown', () => { - let fixtureEl - - beforeAll(() => { - fixtureEl = getFixture() - }) - - afterEach(() => { - clearFixture() - }) - - describe('VERSION', () => { - it('should return plugin version', () => { - expect(Dropdown.VERSION).toEqual(jasmine.any(String)) - }) - }) - - describe('Default', () => { - it('should return plugin default config', () => { - expect(Dropdown.Default).toEqual(jasmine.any(Object)) - }) - }) - - describe('DefaultType', () => { - it('should return plugin default type config', () => { - expect(Dropdown.DefaultType).toEqual(jasmine.any(Object)) - }) - }) - - describe('constructor', () => { - it('should create offset modifier correctly when offset option is a function', () => { - fixtureEl.innerHTML = [ - '' - ].join('') - - const getOffset = offsets => offsets - const btnDropdown = fixtureEl.querySelector('[data-toggle="dropdown"]') - const dropdown = new Dropdown(btnDropdown, { - offset: getOffset - }) - - const offset = dropdown._getOffset() - - expect(offset.offset).toBeUndefined() - expect(typeof offset.fn).toEqual('function') - }) - - it('should create offset modifier correctly when offset option is not a function', () => { - fixtureEl.innerHTML = [ - '' - ].join('') - - const myOffset = 7 - const btnDropdown = fixtureEl.querySelector('[data-toggle="dropdown"]') - const dropdown = new Dropdown(btnDropdown, { - offset: myOffset - }) - - const offset = dropdown._getOffset() - - expect(offset.offset).toEqual(myOffset) - expect(offset.fn).toBeUndefined() - }) - - it('should add a listener on trigger which do not have data-toggle="dropdown"', () => { - fixtureEl.innerHTML = [ - '' - ].join('') - - const btnDropdown = fixtureEl.querySelector('.btn') - const dropdown = new Dropdown(btnDropdown) - - spyOn(dropdown, 'toggle') - - btnDropdown.click() - - expect(dropdown.toggle).toHaveBeenCalled() - }) - - it('should allow to pass config to popper.js with `popperConfig`', () => { - fixtureEl.innerHTML = [ - '' - ].join('') - - const btnDropdown = fixtureEl.querySelector('[data-toggle="dropdown"]') - const dropdown = new Dropdown(btnDropdown, { - popperConfig: { - placement: 'left' - } - }) - - const popperConfig = dropdown._getPopperConfig() - - expect(popperConfig.placement).toEqual('left') - }) - }) - - describe('toggle', () => { - it('should toggle a dropdown', done => { - fixtureEl.innerHTML = [ - '' - ].join('') - - const btnDropdown = fixtureEl.querySelector('[data-toggle="dropdown"]') - const dropdownEl = fixtureEl.querySelector('.dropdown') - const dropdown = new Dropdown(btnDropdown) - - dropdownEl.addEventListener('shown.bs.dropdown', () => { - expect(dropdownEl.classList.contains('show')).toEqual(true) - expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true') - done() - }) - - dropdown.toggle() - }) - - it('should destroy old popper references on toggle', done => { - fixtureEl.innerHTML = [ - '', - '' - ].join('') - - const btnDropdown1 = fixtureEl.querySelector('.firstBtn') - const btnDropdown2 = fixtureEl.querySelector('.secondBtn') - const firstDropdownEl = fixtureEl.querySelector('.first') - const secondDropdownEl = fixtureEl.querySelector('.second') - const dropdown1 = new Dropdown(btnDropdown1) - const dropdown2 = new Dropdown(btnDropdown2) - - firstDropdownEl.addEventListener('shown.bs.dropdown', () => { - expect(firstDropdownEl.classList.contains('show')).toEqual(true) - spyOn(dropdown1._popper, 'destroy') - dropdown2.toggle() - }) - - secondDropdownEl.addEventListener('shown.bs.dropdown', () => { - expect(dropdown1._popper.destroy).toHaveBeenCalled() - done() - }) - - dropdown1.toggle() - }) - - it('should toggle a dropdown and add/remove event listener on mobile', done => { - fixtureEl.innerHTML = [ - '' - ].join('') - - const defaultValueOnTouchStart = document.documentElement.ontouchstart - const btnDropdown = fixtureEl.querySelector('[data-toggle="dropdown"]') - const dropdownEl = fixtureEl.querySelector('.dropdown') - const dropdown = new Dropdown(btnDropdown) - - document.documentElement.ontouchstart = () => {} - spyOn(EventHandler, 'on') - spyOn(EventHandler, 'off') - - dropdownEl.addEventListener('shown.bs.dropdown', () => { - expect(dropdownEl.classList.contains('show')).toEqual(true) - expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true') - expect(EventHandler.on).toHaveBeenCalled() - - dropdown.toggle() - }) - - dropdownEl.addEventListener('hidden.bs.dropdown', () => { - expect(dropdownEl.classList.contains('show')).toEqual(false) - expect(btnDropdown.getAttribute('aria-expanded')).toEqual('false') - expect(EventHandler.off).toHaveBeenCalled() - - document.documentElement.ontouchstart = defaultValueOnTouchStart - done() - }) - - dropdown.toggle() - }) - - it('should toggle a dropdown at the right', done => { - fixtureEl.innerHTML = [ - '' - ].join('') - - const btnDropdown = fixtureEl.querySelector('[data-toggle="dropdown"]') - const dropdownEl = fixtureEl.querySelector('.dropdown') - const dropdown = new Dropdown(btnDropdown) - - dropdownEl.addEventListener('shown.bs.dropdown', () => { - expect(dropdownEl.classList.contains('show')).toEqual(true) - expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true') - done() - }) - - dropdown.toggle() - }) - - it('should toggle a dropup', done => { - fixtureEl.innerHTML = [ - '
', - ' ', - ' ', - '
' - ].join('') - - const btnDropdown = fixtureEl.querySelector('[data-toggle="dropdown"]') - const dropupEl = fixtureEl.querySelector('.dropup') - const dropdown = new Dropdown(btnDropdown) - - dropupEl.addEventListener('shown.bs.dropdown', () => { - expect(dropupEl.classList.contains('show')).toEqual(true) - expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true') - done() - }) - - dropdown.toggle() - }) - - it('should toggle a dropup at the right', done => { - fixtureEl.innerHTML = [ - '
', - ' ', - ' ', - '
' - ].join('') - - const btnDropdown = fixtureEl.querySelector('[data-toggle="dropdown"]') - const dropupEl = fixtureEl.querySelector('.dropup') - const dropdown = new Dropdown(btnDropdown) - - dropupEl.addEventListener('shown.bs.dropdown', () => { - expect(dropupEl.classList.contains('show')).toEqual(true) - expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true') - done() - }) - - dropdown.toggle() - }) - - it('should toggle a dropright', done => { - fixtureEl.innerHTML = [ - '
', - ' ', - ' ', - '
' - ].join('') - - const btnDropdown = fixtureEl.querySelector('[data-toggle="dropdown"]') - const droprightEl = fixtureEl.querySelector('.dropright') - const dropdown = new Dropdown(btnDropdown) - - droprightEl.addEventListener('shown.bs.dropdown', () => { - expect(droprightEl.classList.contains('show')).toEqual(true) - expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true') - done() - }) - - dropdown.toggle() - }) - - it('should toggle a dropleft', done => { - fixtureEl.innerHTML = [ - '
', - ' ', - ' ', - '
' - ].join('') - - const btnDropdown = fixtureEl.querySelector('[data-toggle="dropdown"]') - const dropleftEl = fixtureEl.querySelector('.dropleft') - const dropdown = new Dropdown(btnDropdown) - - dropleftEl.addEventListener('shown.bs.dropdown', () => { - expect(dropleftEl.classList.contains('show')).toEqual(true) - expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true') - done() - }) - - dropdown.toggle() - }) - - it('should toggle a dropdown with parent reference', done => { - fixtureEl.innerHTML = [ - '' - ].join('') - - const btnDropdown = fixtureEl.querySelector('[data-toggle="dropdown"]') - const dropdownEl = fixtureEl.querySelector('.dropdown') - const dropdown = new Dropdown(btnDropdown, { - reference: 'parent' - }) - - dropdownEl.addEventListener('shown.bs.dropdown', () => { - expect(dropdownEl.classList.contains('show')).toEqual(true) - expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true') - done() - }) - - dropdown.toggle() - }) - - it('should toggle a dropdown with a dom node reference', done => { - fixtureEl.innerHTML = [ - '' - ].join('') - - const btnDropdown = fixtureEl.querySelector('[data-toggle="dropdown"]') - const dropdownEl = fixtureEl.querySelector('.dropdown') - const dropdown = new Dropdown(btnDropdown, { - reference: fixtureEl - }) - - dropdownEl.addEventListener('shown.bs.dropdown', () => { - expect(dropdownEl.classList.contains('show')).toEqual(true) - expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true') - done() - }) - - dropdown.toggle() - }) - - it('should toggle a dropdown with a jquery object reference', done => { - fixtureEl.innerHTML = [ - '' - ].join('') - - const btnDropdown = fixtureEl.querySelector('[data-toggle="dropdown"]') - const dropdownEl = fixtureEl.querySelector('.dropdown') - const dropdown = new Dropdown(btnDropdown, { - reference: { 0: fixtureEl, jquery: 'jQuery' } - }) - - dropdownEl.addEventListener('shown.bs.dropdown', () => { - expect(dropdownEl.classList.contains('show')).toEqual(true) - expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true') - done() - }) - - dropdown.toggle() - }) - - it('should not toggle a dropdown if the element is disabled', done => { - fixtureEl.innerHTML = [ - '' - ].join('') - - const btnDropdown = fixtureEl.querySelector('[data-toggle="dropdown"]') - const dropdownEl = fixtureEl.querySelector('.dropdown') - const dropdown = new Dropdown(btnDropdown) - - dropdownEl.addEventListener('shown.bs.dropdown', () => { - throw new Error('should not throw shown.bs.dropdown event') - }) - - dropdown.toggle() - - setTimeout(() => { - expect().nothing() - done() - }) - }) - - it('should not toggle a dropdown if the element contains .disabled', done => { - fixtureEl.innerHTML = [ - '' - ].join('') - - const btnDropdown = fixtureEl.querySelector('[data-toggle="dropdown"]') - const dropdownEl = fixtureEl.querySelector('.dropdown') - const dropdown = new Dropdown(btnDropdown) - - dropdownEl.addEventListener('shown.bs.dropdown', () => { - throw new Error('should not throw shown.bs.dropdown event') - }) - - dropdown.toggle() - - setTimeout(() => { - expect().nothing() - done() - }) - }) - - it('should not toggle a dropdown if the menu is shown', done => { - fixtureEl.innerHTML = [ - '' - ].join('') - - const btnDropdown = fixtureEl.querySelector('[data-toggle="dropdown"]') - const dropdownEl = fixtureEl.querySelector('.dropdown') - const dropdown = new Dropdown(btnDropdown) - - dropdownEl.addEventListener('shown.bs.dropdown', () => { - throw new Error('should not throw shown.bs.dropdown event') - }) - - dropdown.toggle() - - setTimeout(() => { - expect().nothing() - done() - }) - }) - - it('should not toggle a dropdown if show event is prevented', done => { - fixtureEl.innerHTML = [ - '' - ].join('') - - const btnDropdown = fixtureEl.querySelector('[data-toggle="dropdown"]') - const dropdownEl = fixtureEl.querySelector('.dropdown') - const dropdown = new Dropdown(btnDropdown) - - dropdownEl.addEventListener('show.bs.dropdown', e => { - e.preventDefault() - }) - - dropdownEl.addEventListener('shown.bs.dropdown', () => { - throw new Error('should not throw shown.bs.dropdown event') - }) - - dropdown.toggle() - - setTimeout(() => { - expect().nothing() - done() - }) - }) - }) - - describe('show', () => { - it('should show a dropdown', done => { - fixtureEl.innerHTML = [ - '' - ].join('') - - const btnDropdown = fixtureEl.querySelector('[data-toggle="dropdown"]') - const dropdownEl = fixtureEl.querySelector('.dropdown') - const dropdown = new Dropdown(btnDropdown) - - dropdownEl.addEventListener('shown.bs.dropdown', () => { - expect(dropdownEl.classList.contains('show')).toEqual(true) - done() - }) - - dropdown.show() - }) - - it('should not show a dropdown if the element is disabled', done => { - fixtureEl.innerHTML = [ - '' - ].join('') - - const btnDropdown = fixtureEl.querySelector('[data-toggle="dropdown"]') - const dropdownEl = fixtureEl.querySelector('.dropdown') - const dropdown = new Dropdown(btnDropdown) - - dropdownEl.addEventListener('shown.bs.dropdown', () => { - throw new Error('should not throw shown.bs.dropdown event') - }) - - dropdown.show() - - setTimeout(() => { - expect().nothing() - done() - }, 10) - }) - - it('should not show a dropdown if the element contains .disabled', done => { - fixtureEl.innerHTML = [ - '' - ].join('') - - const btnDropdown = fixtureEl.querySelector('[data-toggle="dropdown"]') - const dropdownEl = fixtureEl.querySelector('.dropdown') - const dropdown = new Dropdown(btnDropdown) - - dropdownEl.addEventListener('shown.bs.dropdown', () => { - throw new Error('should not throw shown.bs.dropdown event') - }) - - dropdown.show() - - setTimeout(() => { - expect().nothing() - done() - }, 10) - }) - - it('should not show a dropdown if the menu is shown', done => { - fixtureEl.innerHTML = [ - '' - ].join('') - - const btnDropdown = fixtureEl.querySelector('[data-toggle="dropdown"]') - const dropdownEl = fixtureEl.querySelector('.dropdown') - const dropdown = new Dropdown(btnDropdown) - - dropdownEl.addEventListener('shown.bs.dropdown', () => { - throw new Error('should not throw shown.bs.dropdown event') - }) - - dropdown.show() - - setTimeout(() => { - expect().nothing() - done() - }, 10) - }) - - it('should not show a dropdown if show event is prevented', done => { - fixtureEl.innerHTML = [ - '' - ].join('') - - const btnDropdown = fixtureEl.querySelector('[data-toggle="dropdown"]') - const dropdownEl = fixtureEl.querySelector('.dropdown') - const dropdown = new Dropdown(btnDropdown) - - dropdownEl.addEventListener('show.bs.dropdown', e => { - e.preventDefault() - }) - - dropdownEl.addEventListener('shown.bs.dropdown', () => { - throw new Error('should not throw shown.bs.dropdown event') - }) - - dropdown.show() - - setTimeout(() => { - expect().nothing() - done() - }, 10) - }) - }) - - describe('hide', () => { - it('should hide a dropdown', done => { - fixtureEl.innerHTML = [ - '' - ].join('') - - const btnDropdown = fixtureEl.querySelector('[data-toggle="dropdown"]') - const dropdownEl = fixtureEl.querySelector('.dropdown') - const dropdownMenu = fixtureEl.querySelector('.dropdown-menu') - const dropdown = new Dropdown(btnDropdown) - - dropdownEl.addEventListener('hidden.bs.dropdown', () => { - expect(dropdownMenu.classList.contains('show')).toEqual(false) - done() - }) - - dropdown.hide() - }) - - it('should hide a dropdown and destroy popper', done => { - fixtureEl.innerHTML = [ - '' - ].join('') - - const btnDropdown = fixtureEl.querySelector('[data-toggle="dropdown"]') - const dropdownEl = fixtureEl.querySelector('.dropdown') - const dropdown = new Dropdown(btnDropdown) - - dropdownEl.addEventListener('shown.bs.dropdown', () => { - spyOn(dropdown._popper, 'destroy') - dropdown.hide() - }) - - dropdownEl.addEventListener('hidden.bs.dropdown', () => { - expect(dropdown._popper.destroy).toHaveBeenCalled() - done() - }) - - dropdown.show() - }) - - it('should not hide a dropdown if the element is disabled', done => { - fixtureEl.innerHTML = [ - '' - ].join('') - - const btnDropdown = fixtureEl.querySelector('[data-toggle="dropdown"]') - const dropdownEl = fixtureEl.querySelector('.dropdown') - const dropdownMenu = fixtureEl.querySelector('.dropdown-menu') - const dropdown = new Dropdown(btnDropdown) - - dropdownEl.addEventListener('hidden.bs.dropdown', () => { - throw new Error('should not throw hidden.bs.dropdown event') - }) - - dropdown.hide() - - setTimeout(() => { - expect(dropdownMenu.classList.contains('show')).toEqual(true) - done() - }, 10) - }) - - it('should not hide a dropdown if the element contains .disabled', done => { - fixtureEl.innerHTML = [ - '' - ].join('') - - const btnDropdown = fixtureEl.querySelector('[data-toggle="dropdown"]') - const dropdownEl = fixtureEl.querySelector('.dropdown') - const dropdownMenu = fixtureEl.querySelector('.dropdown-menu') - const dropdown = new Dropdown(btnDropdown) - - dropdownEl.addEventListener('hidden.bs.dropdown', () => { - throw new Error('should not throw hidden.bs.dropdown event') - }) - - dropdown.hide() - - setTimeout(() => { - expect(dropdownMenu.classList.contains('show')).toEqual(true) - done() - }, 10) - }) - - it('should not hide a dropdown if the menu is not shown', done => { - fixtureEl.innerHTML = [ - '' - ].join('') - - const btnDropdown = fixtureEl.querySelector('[data-toggle="dropdown"]') - const dropdownEl = fixtureEl.querySelector('.dropdown') - const dropdown = new Dropdown(btnDropdown) - - dropdownEl.addEventListener('hidden.bs.dropdown', () => { - throw new Error('should not throw hidden.bs.dropdown event') - }) - - dropdown.hide() - - setTimeout(() => { - expect().nothing() - done() - }, 10) - }) - - it('should not hide a dropdown if hide event is prevented', done => { - fixtureEl.innerHTML = [ - '' - ].join('') - - const btnDropdown = fixtureEl.querySelector('[data-toggle="dropdown"]') - const dropdownEl = fixtureEl.querySelector('.dropdown') - const dropdownMenu = fixtureEl.querySelector('.dropdown-menu') - const dropdown = new Dropdown(btnDropdown) - - dropdownEl.addEventListener('hide.bs.dropdown', e => { - e.preventDefault() - }) - - dropdownEl.addEventListener('hidden.bs.dropdown', () => { - throw new Error('should not throw hidden.bs.dropdown event') - }) - - dropdown.hide() - - setTimeout(() => { - expect(dropdownMenu.classList.contains('show')).toEqual(true) - done() - }) - }) - }) - - describe('dispose', () => { - it('should dispose dropdown', () => { - fixtureEl.innerHTML = [ - '' - ].join('') - - const btnDropdown = fixtureEl.querySelector('[data-toggle="dropdown"]') - const dropdown = new Dropdown(btnDropdown) - - expect(dropdown._popper).toBeNull() - expect(dropdown._menu).toBeDefined() - expect(dropdown._element).toBeDefined() - - dropdown.dispose() - - expect(dropdown._menu).toBeNull() - expect(dropdown._element).toBeNull() - }) - - it('should dispose dropdown with popper.js', () => { - fixtureEl.innerHTML = [ - '' - ].join('') - - const btnDropdown = fixtureEl.querySelector('[data-toggle="dropdown"]') - const dropdown = new Dropdown(btnDropdown) - - dropdown.toggle() - - expect(dropdown._popper).toBeDefined() - expect(dropdown._menu).toBeDefined() - expect(dropdown._element).toBeDefined() - - spyOn(Popper.prototype, 'destroy') - - dropdown.dispose() - - expect(dropdown._popper).toBeNull() - expect(dropdown._menu).toBeNull() - expect(dropdown._element).toBeNull() - expect(Popper.prototype.destroy).toHaveBeenCalled() - }) - }) - - describe('update', () => { - it('should call popper.js and detect navbar on update', () => { - fixtureEl.innerHTML = [ - '' - ].join('') - - const btnDropdown = fixtureEl.querySelector('[data-toggle="dropdown"]') - const dropdown = new Dropdown(btnDropdown) - - dropdown.toggle() - - expect(dropdown._popper).toBeDefined() - - spyOn(dropdown._popper, 'scheduleUpdate') - spyOn(dropdown, '_detectNavbar') - - dropdown.update() - - expect(dropdown._popper.scheduleUpdate).toHaveBeenCalled() - expect(dropdown._detectNavbar).toHaveBeenCalled() - }) - - it('should just detect navbar on update', () => { - fixtureEl.innerHTML = [ - '' - ].join('') - - const btnDropdown = fixtureEl.querySelector('[data-toggle="dropdown"]') - const dropdown = new Dropdown(btnDropdown) - - spyOn(dropdown, '_detectNavbar') - - dropdown.update() - - expect(dropdown._popper).toBeNull() - expect(dropdown._detectNavbar).toHaveBeenCalled() - }) - }) - - describe('data-api', () => { - it('should not add class position-static to dropdown if boundary not set', done => { - fixtureEl.innerHTML = [ - '' - ].join('') - - const btnDropdown = fixtureEl.querySelector('[data-toggle="dropdown"]') - const dropdownEl = fixtureEl.querySelector('.dropdown') - - dropdownEl.addEventListener('shown.bs.dropdown', () => { - expect(dropdownEl.classList.contains('position-static')).toEqual(false) - done() - }) - - btnDropdown.click() - }) - - it('should add class position-static to dropdown if boundary not scrollParent', done => { - fixtureEl.innerHTML = [ - '' - ].join('') - - const btnDropdown = fixtureEl.querySelector('[data-toggle="dropdown"]') - const dropdownEl = fixtureEl.querySelector('.dropdown') - - dropdownEl.addEventListener('shown.bs.dropdown', () => { - expect(dropdownEl.classList.contains('position-static')).toEqual(true) - done() - }) - - btnDropdown.click() - }) - - it('should show and hide a dropdown', done => { - fixtureEl.innerHTML = [ - '' - ].join('') - - const btnDropdown = fixtureEl.querySelector('[data-toggle="dropdown"]') - const dropdownEl = fixtureEl.querySelector('.dropdown') - let showEventTriggered = false - let hideEventTriggered = false - - dropdownEl.addEventListener('show.bs.dropdown', () => { - showEventTriggered = true - }) - - dropdownEl.addEventListener('shown.bs.dropdown', e => { - expect(dropdownEl.classList.contains('show')).toEqual(true) - expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true') - expect(showEventTriggered).toEqual(true) - expect(e.relatedTarget).toEqual(btnDropdown) - document.body.click() - }) - - dropdownEl.addEventListener('hide.bs.dropdown', () => { - hideEventTriggered = true - }) - - dropdownEl.addEventListener('hidden.bs.dropdown', e => { - expect(dropdownEl.classList.contains('show')).toEqual(false) - expect(btnDropdown.getAttribute('aria-expanded')).toEqual('false') - expect(hideEventTriggered).toEqual(true) - expect(e.relatedTarget).toEqual(btnDropdown) - done() - }) - - btnDropdown.click() - }) - - it('should not use popper.js in navbar', done => { - fixtureEl.innerHTML = [ - '' - ].join('') - - const btnDropdown = fixtureEl.querySelector('[data-toggle="dropdown"]') - const dropdownEl = fixtureEl.querySelector('.dropdown') - const dropdownMenu = fixtureEl.querySelector('.dropdown-menu') - - dropdownEl.addEventListener('shown.bs.dropdown', () => { - expect(dropdownMenu.getAttribute('style')).toEqual(null, 'no inline style applied by popper.js') - done() - }) - - btnDropdown.click() - }) - - it('should not use popper.js if display set to static', done => { - fixtureEl.innerHTML = [ - '' - ].join('') - - const btnDropdown = fixtureEl.querySelector('[data-toggle="dropdown"]') - const dropdownEl = fixtureEl.querySelector('.dropdown') - const dropdownMenu = fixtureEl.querySelector('.dropdown-menu') - - dropdownEl.addEventListener('shown.bs.dropdown', () => { - // popper.js add this attribute when we use it - expect(dropdownMenu.getAttribute('x-placement')).toEqual(null) - done() - }) - - btnDropdown.click() - }) - - it('should remove "show" class if tabbing outside of menu', done => { - fixtureEl.innerHTML = [ - '' - ].join('') - - const btnDropdown = fixtureEl.querySelector('[data-toggle="dropdown"]') - const dropdownEl = fixtureEl.querySelector('.dropdown') - - dropdownEl.addEventListener('shown.bs.dropdown', () => { - expect(dropdownEl.classList.contains('show')).toEqual(true) - - const keyUp = createEvent('keyup') - - keyUp.which = 9 // Tab - document.dispatchEvent(keyUp) - }) - - dropdownEl.addEventListener('hidden.bs.dropdown', () => { - expect(dropdownEl.classList.contains('show')).toEqual(false) - done() - }) - - btnDropdown.click() - }) - - it('should remove "show" class if body is clicked, with multiple dropdowns', done => { - fixtureEl.innerHTML = [ - '', - '
', - ' ', - ' ', - ' ', - '
' - ].join('') - - const triggerDropdownList = fixtureEl.querySelectorAll('[data-toggle="dropdown"]') - - expect(triggerDropdownList.length).toEqual(2) - - const first = triggerDropdownList[0] - const last = triggerDropdownList[1] - const dropdownTestMenu = first.parentNode - const btnGroup = last.parentNode - - dropdownTestMenu.addEventListener('shown.bs.dropdown', () => { - expect(dropdownTestMenu.classList.contains('show')).toEqual(true) - expect(fixtureEl.querySelectorAll('.dropdown-menu.show').length).toEqual(1) - document.body.click() - }) - - dropdownTestMenu.addEventListener('hidden.bs.dropdown', () => { - expect(fixtureEl.querySelectorAll('.dropdown-menu.show').length).toEqual(0) - last.click() - }) - - btnGroup.addEventListener('shown.bs.dropdown', () => { - expect(btnGroup.classList.contains('show')).toEqual(true) - expect(fixtureEl.querySelectorAll('.dropdown-menu.show').length).toEqual(1) - document.body.click() - }) - - btnGroup.addEventListener('hidden.bs.dropdown', () => { - expect(fixtureEl.querySelectorAll('.dropdown-menu.show').length).toEqual(0) - done() - }) - - first.click() - }) - - it('should remove "show" class if body if tabbing outside of menu, with multiple dropdowns', done => { - fixtureEl.innerHTML = [ - '', - '
', - ' ', - ' ', - ' ', - '
' - ].join('') - - const triggerDropdownList = fixtureEl.querySelectorAll('[data-toggle="dropdown"]') - - expect(triggerDropdownList.length).toEqual(2) - - const first = triggerDropdownList[0] - const last = triggerDropdownList[1] - const dropdownTestMenu = first.parentNode - const btnGroup = last.parentNode - - dropdownTestMenu.addEventListener('shown.bs.dropdown', () => { - expect(dropdownTestMenu.classList.contains('show')).toEqual(true, '"show" class added on click') - expect(fixtureEl.querySelectorAll('.dropdown-menu.show').length).toEqual(1, 'only one dropdown is shown') - - const keyUp = createEvent('keyup') - keyUp.which = 9 // Tab - - document.dispatchEvent(keyUp) - }) - - dropdownTestMenu.addEventListener('hidden.bs.dropdown', () => { - expect(fixtureEl.querySelectorAll('.dropdown-menu.show').length).toEqual(0, '"show" class removed') - last.click() - }) - - btnGroup.addEventListener('shown.bs.dropdown', () => { - expect(btnGroup.classList.contains('show')).toEqual(true, '"show" class added on click') - expect(fixtureEl.querySelectorAll('.dropdown-menu.show').length).toEqual(1, 'only one dropdown is shown') - - const keyUp = createEvent('keyup') - keyUp.which = 9 // Tab - - document.dispatchEvent(keyUp) - }) - - btnGroup.addEventListener('hidden.bs.dropdown', () => { - expect(fixtureEl.querySelectorAll('.dropdown-menu.show').length).toEqual(0, '"show" class removed') - done() - }) - - first.click() - }) - - it('should fire hide and hidden event without a clickEvent if event type is not click', done => { - fixtureEl.innerHTML = [ - '' - ].join('') - - const triggerDropdown = fixtureEl.querySelector('[data-toggle="dropdown"]') - const dropdown = fixtureEl.querySelector('.dropdown') - - dropdown.addEventListener('hide.bs.dropdown', e => { - expect(e.clickEvent).toBeUndefined() - }) - - dropdown.addEventListener('hidden.bs.dropdown', e => { - expect(e.clickEvent).toBeUndefined() - done() - }) - - dropdown.addEventListener('shown.bs.dropdown', () => { - const keyDown = createEvent('keydown') - - keyDown.which = 27 - triggerDropdown.dispatchEvent(keyDown) - }) - - triggerDropdown.click() - }) - - it('should ignore keyboard events within s and ', - '
', - '
' - ].join('') - - const triggerDropdown = fixtureEl.querySelector('[data-toggle="dropdown"]') - const dropdown = fixtureEl.querySelector('.dropdown') - const input = fixtureEl.querySelector('input') - const textarea = fixtureEl.querySelector('textarea') - - dropdown.addEventListener('shown.bs.dropdown', () => { - input.focus() - const keyDown = createEvent('keydown') - - keyDown.which = 38 - input.dispatchEvent(keyDown) - - expect(document.activeElement).toEqual(input, 'input still focused') - - textarea.focus() - textarea.dispatchEvent(keyDown) - - expect(document.activeElement).toEqual(textarea, 'textarea still focused') - done() - }) - - triggerDropdown.click() - }) - - it('should skip disabled element when using keyboard navigation', done => { - fixtureEl.innerHTML = [ - '' - ].join('') - - const triggerDropdown = fixtureEl.querySelector('[data-toggle="dropdown"]') - const dropdown = fixtureEl.querySelector('.dropdown') - - dropdown.addEventListener('shown.bs.dropdown', () => { - const keyDown = createEvent('keydown') - keyDown.which = 40 - - triggerDropdown.dispatchEvent(keyDown) - triggerDropdown.dispatchEvent(keyDown) - - expect(document.activeElement.classList.contains('disabled')).toEqual(false, '.disabled not focused') - expect(document.activeElement.hasAttribute('disabled')).toEqual(false, ':disabled not focused') - done() - }) - - triggerDropdown.click() - }) - - it('should focus next/previous element when using keyboard navigation', done => { - fixtureEl.innerHTML = [ - '' - ].join('') - - const triggerDropdown = fixtureEl.querySelector('[data-toggle="dropdown"]') - const dropdown = fixtureEl.querySelector('.dropdown') - const item1 = fixtureEl.querySelector('#item1') - const item2 = fixtureEl.querySelector('#item2') - - dropdown.addEventListener('shown.bs.dropdown', () => { - const keyDown40 = createEvent('keydown') - keyDown40.which = 40 - - triggerDropdown.dispatchEvent(keyDown40) - expect(document.activeElement).toEqual(item1, 'item1 is focused') - - document.activeElement.dispatchEvent(keyDown40) - expect(document.activeElement).toEqual(item2, 'item2 is focused') - - const keyDown38 = createEvent('keydown') - keyDown38.which = 38 - - document.activeElement.dispatchEvent(keyDown38) - expect(document.activeElement).toEqual(item1, 'item1 is focused') - - done() - }) - - triggerDropdown.click() - }) - - it('should not close the dropdown if the user clicks on a text field', done => { - fixtureEl.innerHTML = [ - '' - ].join('') - - const triggerDropdown = fixtureEl.querySelector('[data-toggle="dropdown"]') - const dropdown = fixtureEl.querySelector('.dropdown') - const input = fixtureEl.querySelector('input') - - input.addEventListener('click', () => { - expect(dropdown.classList.contains('show')).toEqual(true, 'dropdown menu is shown') - done() - }) - - dropdown.addEventListener('shown.bs.dropdown', () => { - expect(dropdown.classList.contains('show')).toEqual(true, 'dropdown menu is shown') - input.dispatchEvent(createEvent('click')) - }) - - triggerDropdown.click() - }) - - it('should not close the dropdown if the user clicks on a textarea', done => { - fixtureEl.innerHTML = [ - '' - ].join('') - - const triggerDropdown = fixtureEl.querySelector('[data-toggle="dropdown"]') - const dropdown = fixtureEl.querySelector('.dropdown') - const textarea = fixtureEl.querySelector('textarea') - - textarea.addEventListener('click', () => { - expect(dropdown.classList.contains('show')).toEqual(true, 'dropdown menu is shown') - done() - }) - - dropdown.addEventListener('shown.bs.dropdown', () => { - expect(dropdown.classList.contains('show')).toEqual(true, 'dropdown menu is shown') - textarea.dispatchEvent(createEvent('click')) - }) - - triggerDropdown.click() - }) - - it('should ignore keyboard events for s and ', - '
', - '
' - ].join('') - - const triggerDropdown = fixtureEl.querySelector('[data-toggle="dropdown"]') - const dropdown = fixtureEl.querySelector('.dropdown') - const input = fixtureEl.querySelector('input') - const textarea = fixtureEl.querySelector('textarea') - - // Space key - const keyDownSpace = createEvent('keydown') - keyDownSpace.which = 32 - - // Key up - const keyDownUp = createEvent('keydown') - keyDownSpace.which = 38 - - // Key down - const keyDown = createEvent('keydown') - keyDownSpace.which = 40 - - // Key escape - const keyDownEscape = createEvent('keydown') - keyDownEscape.which = 27 - - dropdown.addEventListener('shown.bs.dropdown', () => { - // Space key - input.focus() - input.dispatchEvent(keyDownSpace) - - expect(document.activeElement).toEqual(input, 'input still focused') - - textarea.focus() - textarea.dispatchEvent(keyDownSpace) - - expect(document.activeElement).toEqual(textarea, 'textarea still focused') - - // Key up - input.focus() - input.dispatchEvent(keyDownUp) - - expect(document.activeElement).toEqual(input, 'input still focused') - - textarea.focus() - textarea.dispatchEvent(keyDownUp) - - expect(document.activeElement).toEqual(textarea, 'textarea still focused') - - // Key down - input.focus() - input.dispatchEvent(keyDown) - - expect(document.activeElement).toEqual(input, 'input still focused') - - textarea.focus() - textarea.dispatchEvent(keyDown) - - expect(document.activeElement).toEqual(textarea, 'textarea still focused') - - // Key escape - input.focus() - input.dispatchEvent(keyDownEscape) - - expect(dropdown.classList.contains('show')).toEqual(false, 'dropdown menu is not shown') - done() - }) - - triggerDropdown.click() - }) - - it('should not open dropdown if escape key was pressed on the toggle', done => { - fixtureEl.innerHTML = [ - '
', - ' ' - ] - - const triggerDropdown = fixtureEl.querySelector('[data-toggle="dropdown"]') - const dropdown = new Dropdown(triggerDropdown) - const button = fixtureEl.querySelector('button[data-toggle="dropdown"]') - - spyOn(dropdown, 'toggle') - - // Key escape - button.focus() - // Key escape - const keyDownEscape = createEvent('keydown') - keyDownEscape.which = 27 - button.dispatchEvent(keyDownEscape) - - setTimeout(() => { - expect(dropdown.toggle).not.toHaveBeenCalled() - expect(triggerDropdown.parentNode.classList.contains('show')).toEqual(false) - done() - }, 20) - }) - }) - - describe('jQueryInterface', () => { - it('should create a dropdown', () => { - fixtureEl.innerHTML = '
' - - const div = fixtureEl.querySelector('div') - - jQueryMock.fn.dropdown = Dropdown.jQueryInterface - jQueryMock.elements = [div] - - jQueryMock.fn.dropdown.call(jQueryMock) - - expect(Dropdown.getInstance(div)).toBeDefined() - }) - - it('should not re create a dropdown', () => { - fixtureEl.innerHTML = '
' - - const div = fixtureEl.querySelector('div') - const dropdown = new Dropdown(div) - - jQueryMock.fn.dropdown = Dropdown.jQueryInterface - jQueryMock.elements = [div] - - jQueryMock.fn.dropdown.call(jQueryMock) - - expect(Dropdown.getInstance(div)).toEqual(dropdown) - }) - - it('should throw error on undefined method', () => { - fixtureEl.innerHTML = '
' - - const div = fixtureEl.querySelector('div') - const action = 'undefinedMethod' - - jQueryMock.fn.dropdown = Dropdown.jQueryInterface - jQueryMock.elements = [div] - - try { - jQueryMock.fn.dropdown.call(jQueryMock, action) - } catch (error) { - expect(error.message).toEqual(`No method named "${action}"`) - } - }) - }) - - describe('getInstance', () => { - it('should return dropdown instance', () => { - fixtureEl.innerHTML = '
' - - const div = fixtureEl.querySelector('div') - const dropdown = new Dropdown(div) - - expect(Dropdown.getInstance(div)).toEqual(dropdown) - }) - - it('should return null when there is no dropdown instance', () => { - fixtureEl.innerHTML = '
' - - const div = fixtureEl.querySelector('div') - - expect(Dropdown.getInstance(div)).toEqual(null) - }) - }) -}) diff --git a/js/src/modal.js b/js/src/modal.js new file mode 100644 index 000000000..bee5e23f8 --- /dev/null +++ b/js/src/modal.js @@ -0,0 +1,604 @@ +/** + * -------------------------------------------------------------------------- + * Bootstrap (v4.3.1): modal.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + * -------------------------------------------------------------------------- + */ + +import { + getjQuery, + TRANSITION_END, + emulateTransitionEnd, + getElementFromSelector, + getTransitionDurationFromElement, + isVisible, + makeArray, + reflow, + typeCheckConfig +} from './util/index' +import Data from './dom/data' +import EventHandler from './dom/event-handler' +import Manipulator from './dom/manipulator' +import SelectorEngine from './dom/selector-engine' + +/** + * ------------------------------------------------------------------------ + * Constants + * ------------------------------------------------------------------------ + */ + +const NAME = 'modal' +const VERSION = '4.3.1' +const DATA_KEY = 'bs.modal' +const EVENT_KEY = `.${DATA_KEY}` +const DATA_API_KEY = '.data-api' +const ESCAPE_KEYCODE = 27 // KeyboardEvent.which value for Escape (Esc) key + +const Default = { + backdrop: true, + keyboard: true, + focus: true, + show: true +} + +const DefaultType = { + backdrop: '(boolean|string)', + keyboard: 'boolean', + focus: 'boolean', + show: 'boolean' +} + +const Event = { + HIDE: `hide${EVENT_KEY}`, + HIDDEN: `hidden${EVENT_KEY}`, + SHOW: `show${EVENT_KEY}`, + SHOWN: `shown${EVENT_KEY}`, + FOCUSIN: `focusin${EVENT_KEY}`, + RESIZE: `resize${EVENT_KEY}`, + CLICK_DISMISS: `click.dismiss${EVENT_KEY}`, + KEYDOWN_DISMISS: `keydown.dismiss${EVENT_KEY}`, + MOUSEUP_DISMISS: `mouseup.dismiss${EVENT_KEY}`, + MOUSEDOWN_DISMISS: `mousedown.dismiss${EVENT_KEY}`, + CLICK_DATA_API: `click${EVENT_KEY}${DATA_API_KEY}` +} + +const ClassName = { + SCROLLABLE: 'modal-dialog-scrollable', + SCROLLBAR_MEASURER: 'modal-scrollbar-measure', + BACKDROP: 'modal-backdrop', + OPEN: 'modal-open', + FADE: 'fade', + SHOW: 'show' +} + +const Selector = { + DIALOG: '.modal-dialog', + MODAL_BODY: '.modal-body', + DATA_TOGGLE: '[data-toggle="modal"]', + DATA_DISMISS: '[data-dismiss="modal"]', + FIXED_CONTENT: '.fixed-top, .fixed-bottom, .is-fixed, .sticky-top', + STICKY_CONTENT: '.sticky-top' +} + +/** + * ------------------------------------------------------------------------ + * Class Definition + * ------------------------------------------------------------------------ + */ + +class Modal { + constructor(element, config) { + this._config = this._getConfig(config) + this._element = element + this._dialog = SelectorEngine.findOne(Selector.DIALOG, element) + this._backdrop = null + this._isShown = false + this._isBodyOverflowing = false + this._ignoreBackdropClick = false + this._isTransitioning = false + this._scrollbarWidth = 0 + Data.setData(element, DATA_KEY, this) + } + + // Getters + + static get VERSION() { + return VERSION + } + + static get Default() { + return Default + } + + // Public + + toggle(relatedTarget) { + return this._isShown ? this.hide() : this.show(relatedTarget) + } + + show(relatedTarget) { + if (this._isShown || this._isTransitioning) { + return + } + + if (this._element.classList.contains(ClassName.FADE)) { + this._isTransitioning = true + } + + const showEvent = EventHandler.trigger(this._element, Event.SHOW, { + relatedTarget + }) + + if (this._isShown || showEvent.defaultPrevented) { + return + } + + this._isShown = true + + this._checkScrollbar() + this._setScrollbar() + + this._adjustDialog() + + this._setEscapeEvent() + this._setResizeEvent() + + EventHandler.on(this._element, + Event.CLICK_DISMISS, + Selector.DATA_DISMISS, + event => this.hide(event) + ) + + EventHandler.on(this._dialog, Event.MOUSEDOWN_DISMISS, () => { + EventHandler.one(this._element, Event.MOUSEUP_DISMISS, event => { + if (event.target === this._element) { + this._ignoreBackdropClick = true + } + }) + }) + + this._showBackdrop(() => this._showElement(relatedTarget)) + } + + hide(event) { + if (event) { + event.preventDefault() + } + + if (!this._isShown || this._isTransitioning) { + return + } + + const hideEvent = EventHandler.trigger(this._element, Event.HIDE) + + if (hideEvent.defaultPrevented) { + return + } + + this._isShown = false + const transition = this._element.classList.contains(ClassName.FADE) + + if (transition) { + this._isTransitioning = true + } + + this._setEscapeEvent() + this._setResizeEvent() + + EventHandler.off(document, Event.FOCUSIN) + + this._element.classList.remove(ClassName.SHOW) + + EventHandler.off(this._element, Event.CLICK_DISMISS) + EventHandler.off(this._dialog, Event.MOUSEDOWN_DISMISS) + + if (transition) { + const transitionDuration = getTransitionDurationFromElement(this._element) + + EventHandler.one(this._element, TRANSITION_END, event => this._hideModal(event)) + emulateTransitionEnd(this._element, transitionDuration) + } else { + this._hideModal() + } + } + + dispose() { + [window, this._element, this._dialog] + .forEach(htmlElement => EventHandler.off(htmlElement, EVENT_KEY)) + + /** + * `document` has 2 events `Event.FOCUSIN` and `Event.CLICK_DATA_API` + * Do not move `document` in `htmlElements` array + * It will remove `Event.CLICK_DATA_API` event that should remain + */ + EventHandler.off(document, Event.FOCUSIN) + + Data.removeData(this._element, DATA_KEY) + + this._config = null + this._element = null + this._dialog = null + this._backdrop = null + this._isShown = null + this._isBodyOverflowing = null + this._ignoreBackdropClick = null + this._isTransitioning = null + this._scrollbarWidth = null + } + + handleUpdate() { + this._adjustDialog() + } + + // Private + + _getConfig(config) { + config = { + ...Default, + ...config + } + typeCheckConfig(NAME, config, DefaultType) + return config + } + + _showElement(relatedTarget) { + const transition = this._element.classList.contains(ClassName.FADE) + const modalBody = SelectorEngine.findOne(Selector.MODAL_BODY, this._dialog) + + if (!this._element.parentNode || + this._element.parentNode.nodeType !== Node.ELEMENT_NODE) { + // Don't move modal's DOM position + document.body.appendChild(this._element) + } + + this._element.style.display = 'block' + this._element.removeAttribute('aria-hidden') + this._element.setAttribute('aria-modal', true) + + if (this._dialog.classList.contains(ClassName.SCROLLABLE) && modalBody) { + modalBody.scrollTop = 0 + } else { + this._element.scrollTop = 0 + } + + if (transition) { + reflow(this._element) + } + + this._element.classList.add(ClassName.SHOW) + + if (this._config.focus) { + this._enforceFocus() + } + + const transitionComplete = () => { + if (this._config.focus) { + this._element.focus() + } + + this._isTransitioning = false + EventHandler.trigger(this._element, Event.SHOWN, { + relatedTarget + }) + } + + if (transition) { + const transitionDuration = getTransitionDurationFromElement(this._dialog) + + EventHandler.one(this._dialog, TRANSITION_END, transitionComplete) + emulateTransitionEnd(this._dialog, transitionDuration) + } else { + transitionComplete() + } + } + + _enforceFocus() { + EventHandler.off(document, Event.FOCUSIN) // guard against infinite focus loop + EventHandler.on(document, Event.FOCUSIN, event => { + if (document !== event.target && + this._element !== event.target && + !this._element.contains(event.target)) { + this._element.focus() + } + }) + } + + _setEscapeEvent() { + if (this._isShown && this._config.keyboard) { + EventHandler.on(this._element, Event.KEYDOWN_DISMISS, event => { + if (event.which === ESCAPE_KEYCODE) { + event.preventDefault() + this.hide() + } + }) + } else { + EventHandler.off(this._element, Event.KEYDOWN_DISMISS) + } + } + + _setResizeEvent() { + if (this._isShown) { + EventHandler.on(window, Event.RESIZE, () => this._adjustDialog()) + } else { + EventHandler.off(window, Event.RESIZE) + } + } + + _hideModal() { + this._element.style.display = 'none' + this._element.setAttribute('aria-hidden', true) + this._element.removeAttribute('aria-modal') + this._isTransitioning = false + this._showBackdrop(() => { + document.body.classList.remove(ClassName.OPEN) + this._resetAdjustments() + this._resetScrollbar() + EventHandler.trigger(this._element, Event.HIDDEN) + }) + } + + _removeBackdrop() { + this._backdrop.parentNode.removeChild(this._backdrop) + this._backdrop = null + } + + _showBackdrop(callback) { + const animate = this._element.classList.contains(ClassName.FADE) ? + ClassName.FADE : + '' + + if (this._isShown && this._config.backdrop) { + this._backdrop = document.createElement('div') + this._backdrop.className = ClassName.BACKDROP + + if (animate) { + this._backdrop.classList.add(animate) + } + + document.body.appendChild(this._backdrop) + + EventHandler.on(this._element, Event.CLICK_DISMISS, event => { + if (this._ignoreBackdropClick) { + this._ignoreBackdropClick = false + return + } + + if (event.target !== event.currentTarget) { + return + } + + if (this._config.backdrop === 'static') { + this._element.focus() + } else { + this.hide() + } + }) + + if (animate) { + reflow(this._backdrop) + } + + this._backdrop.classList.add(ClassName.SHOW) + + if (!animate) { + callback() + return + } + + const backdropTransitionDuration = getTransitionDurationFromElement(this._backdrop) + + EventHandler.one(this._backdrop, TRANSITION_END, callback) + emulateTransitionEnd(this._backdrop, backdropTransitionDuration) + } else if (!this._isShown && this._backdrop) { + this._backdrop.classList.remove(ClassName.SHOW) + + const callbackRemove = () => { + this._removeBackdrop() + callback() + } + + if (this._element.classList.contains(ClassName.FADE)) { + const backdropTransitionDuration = getTransitionDurationFromElement(this._backdrop) + EventHandler.one(this._backdrop, TRANSITION_END, callbackRemove) + emulateTransitionEnd(this._backdrop, backdropTransitionDuration) + } else { + callbackRemove() + } + } else { + callback() + } + } + + // ---------------------------------------------------------------------- + // the following methods are used to handle overflowing modals + // ---------------------------------------------------------------------- + + _adjustDialog() { + const isModalOverflowing = + this._element.scrollHeight > document.documentElement.clientHeight + + if (!this._isBodyOverflowing && isModalOverflowing) { + this._element.style.paddingLeft = `${this._scrollbarWidth}px` + } + + if (this._isBodyOverflowing && !isModalOverflowing) { + this._element.style.paddingRight = `${this._scrollbarWidth}px` + } + } + + _resetAdjustments() { + this._element.style.paddingLeft = '' + this._element.style.paddingRight = '' + } + + _checkScrollbar() { + const rect = document.body.getBoundingClientRect() + this._isBodyOverflowing = rect.left + rect.right < window.innerWidth + this._scrollbarWidth = this._getScrollbarWidth() + } + + _setScrollbar() { + if (this._isBodyOverflowing) { + // Note: DOMNode.style.paddingRight returns the actual value or '' if not set + // while $(DOMNode).css('padding-right') returns the calculated value or 0 if not set + + // Adjust fixed content padding + makeArray(SelectorEngine.find(Selector.FIXED_CONTENT)) + .forEach(element => { + const actualPadding = element.style.paddingRight + const calculatedPadding = window.getComputedStyle(element)['padding-right'] + Manipulator.setDataAttribute(element, 'padding-right', actualPadding) + element.style.paddingRight = `${parseFloat(calculatedPadding) + this._scrollbarWidth}px` + }) + + // Adjust sticky content margin + makeArray(SelectorEngine.find(Selector.STICKY_CONTENT)) + .forEach(element => { + const actualMargin = element.style.marginRight + const calculatedMargin = window.getComputedStyle(element)['margin-right'] + Manipulator.setDataAttribute(element, 'margin-right', actualMargin) + element.style.marginRight = `${parseFloat(calculatedMargin) - this._scrollbarWidth}px` + }) + + // Adjust body padding + const actualPadding = document.body.style.paddingRight + const calculatedPadding = window.getComputedStyle(document.body)['padding-right'] + + Manipulator.setDataAttribute(document.body, 'padding-right', actualPadding) + document.body.style.paddingRight = `${parseFloat(calculatedPadding) + this._scrollbarWidth}px` + } + + document.body.classList.add(ClassName.OPEN) + } + + _resetScrollbar() { + // Restore fixed content padding + makeArray(SelectorEngine.find(Selector.FIXED_CONTENT)) + .forEach(element => { + const padding = Manipulator.getDataAttribute(element, 'padding-right') + if (typeof padding !== 'undefined') { + Manipulator.removeDataAttribute(element, 'padding-right') + element.style.paddingRight = padding + } + }) + + // Restore sticky content and navbar-toggler margin + makeArray(SelectorEngine.find(`${Selector.STICKY_CONTENT}`)) + .forEach(element => { + const margin = Manipulator.getDataAttribute(element, 'margin-right') + if (typeof margin !== 'undefined') { + Manipulator.removeDataAttribute(element, 'margin-right') + element.style.marginRight = margin + } + }) + + // Restore body padding + const padding = Manipulator.getDataAttribute(document.body, 'padding-right') + if (typeof padding === 'undefined') { + document.body.style.paddingRight = '' + } else { + Manipulator.removeDataAttribute(document.body, 'padding-right') + document.body.style.paddingRight = padding + } + } + + _getScrollbarWidth() { // thx d.walsh + const scrollDiv = document.createElement('div') + scrollDiv.className = ClassName.SCROLLBAR_MEASURER + document.body.appendChild(scrollDiv) + const scrollbarWidth = scrollDiv.getBoundingClientRect().width - scrollDiv.clientWidth + document.body.removeChild(scrollDiv) + return scrollbarWidth + } + + // Static + + static jQueryInterface(config, relatedTarget) { + return this.each(function () { + let data = Data.getData(this, DATA_KEY) + const _config = { + ...Default, + ...Manipulator.getDataAttributes(this), + ...typeof config === 'object' && config ? config : {} + } + + if (!data) { + data = new Modal(this, _config) + } + + if (typeof config === 'string') { + if (typeof data[config] === 'undefined') { + throw new TypeError(`No method named "${config}"`) + } + + data[config](relatedTarget) + } else if (_config.show) { + data.show(relatedTarget) + } + }) + } + + static getInstance(element) { + return Data.getData(element, DATA_KEY) + } +} + +/** + * ------------------------------------------------------------------------ + * Data Api implementation + * ------------------------------------------------------------------------ + */ + +EventHandler.on(document, Event.CLICK_DATA_API, Selector.DATA_TOGGLE, function (event) { + const target = getElementFromSelector(this) + + if (this.tagName === 'A' || this.tagName === 'AREA') { + event.preventDefault() + } + + EventHandler.one(target, Event.SHOW, showEvent => { + if (showEvent.defaultPrevented) { + // only register focus restorer if modal will actually get shown + return + } + + EventHandler.one(target, Event.HIDDEN, () => { + if (isVisible(this)) { + this.focus() + } + }) + }) + + let data = Data.getData(target, DATA_KEY) + if (!data) { + const config = { + ...Manipulator.getDataAttributes(target), + ...Manipulator.getDataAttributes(this) + } + + data = new Modal(target, config) + } + + data.show(this) +}) + +const $ = getjQuery() + +/** + * ------------------------------------------------------------------------ + * jQuery + * ------------------------------------------------------------------------ + * add .modal to jQuery only if jQuery is present + */ +/* istanbul ignore if */ +if ($) { + const JQUERY_NO_CONFLICT = $.fn[NAME] + $.fn[NAME] = Modal.jQueryInterface + $.fn[NAME].Constructor = Modal + $.fn[NAME].noConflict = () => { + $.fn[NAME] = JQUERY_NO_CONFLICT + return Modal.jQueryInterface + } +} + +export default Modal diff --git a/js/src/modal/modal.js b/js/src/modal/modal.js deleted file mode 100644 index 4864cad9d..000000000 --- a/js/src/modal/modal.js +++ /dev/null @@ -1,604 +0,0 @@ -/** - * -------------------------------------------------------------------------- - * Bootstrap (v4.3.1): modal.js - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) - * -------------------------------------------------------------------------- - */ - -import { - getjQuery, - TRANSITION_END, - emulateTransitionEnd, - getElementFromSelector, - getTransitionDurationFromElement, - isVisible, - makeArray, - reflow, - typeCheckConfig -} from '../util/index' -import Data from '../dom/data' -import EventHandler from '../dom/event-handler' -import Manipulator from '../dom/manipulator' -import SelectorEngine from '../dom/selector-engine' - -/** - * ------------------------------------------------------------------------ - * Constants - * ------------------------------------------------------------------------ - */ - -const NAME = 'modal' -const VERSION = '4.3.1' -const DATA_KEY = 'bs.modal' -const EVENT_KEY = `.${DATA_KEY}` -const DATA_API_KEY = '.data-api' -const ESCAPE_KEYCODE = 27 // KeyboardEvent.which value for Escape (Esc) key - -const Default = { - backdrop: true, - keyboard: true, - focus: true, - show: true -} - -const DefaultType = { - backdrop: '(boolean|string)', - keyboard: 'boolean', - focus: 'boolean', - show: 'boolean' -} - -const Event = { - HIDE: `hide${EVENT_KEY}`, - HIDDEN: `hidden${EVENT_KEY}`, - SHOW: `show${EVENT_KEY}`, - SHOWN: `shown${EVENT_KEY}`, - FOCUSIN: `focusin${EVENT_KEY}`, - RESIZE: `resize${EVENT_KEY}`, - CLICK_DISMISS: `click.dismiss${EVENT_KEY}`, - KEYDOWN_DISMISS: `keydown.dismiss${EVENT_KEY}`, - MOUSEUP_DISMISS: `mouseup.dismiss${EVENT_KEY}`, - MOUSEDOWN_DISMISS: `mousedown.dismiss${EVENT_KEY}`, - CLICK_DATA_API: `click${EVENT_KEY}${DATA_API_KEY}` -} - -const ClassName = { - SCROLLABLE: 'modal-dialog-scrollable', - SCROLLBAR_MEASURER: 'modal-scrollbar-measure', - BACKDROP: 'modal-backdrop', - OPEN: 'modal-open', - FADE: 'fade', - SHOW: 'show' -} - -const Selector = { - DIALOG: '.modal-dialog', - MODAL_BODY: '.modal-body', - DATA_TOGGLE: '[data-toggle="modal"]', - DATA_DISMISS: '[data-dismiss="modal"]', - FIXED_CONTENT: '.fixed-top, .fixed-bottom, .is-fixed, .sticky-top', - STICKY_CONTENT: '.sticky-top' -} - -/** - * ------------------------------------------------------------------------ - * Class Definition - * ------------------------------------------------------------------------ - */ - -class Modal { - constructor(element, config) { - this._config = this._getConfig(config) - this._element = element - this._dialog = SelectorEngine.findOne(Selector.DIALOG, element) - this._backdrop = null - this._isShown = false - this._isBodyOverflowing = false - this._ignoreBackdropClick = false - this._isTransitioning = false - this._scrollbarWidth = 0 - Data.setData(element, DATA_KEY, this) - } - - // Getters - - static get VERSION() { - return VERSION - } - - static get Default() { - return Default - } - - // Public - - toggle(relatedTarget) { - return this._isShown ? this.hide() : this.show(relatedTarget) - } - - show(relatedTarget) { - if (this._isShown || this._isTransitioning) { - return - } - - if (this._element.classList.contains(ClassName.FADE)) { - this._isTransitioning = true - } - - const showEvent = EventHandler.trigger(this._element, Event.SHOW, { - relatedTarget - }) - - if (this._isShown || showEvent.defaultPrevented) { - return - } - - this._isShown = true - - this._checkScrollbar() - this._setScrollbar() - - this._adjustDialog() - - this._setEscapeEvent() - this._setResizeEvent() - - EventHandler.on(this._element, - Event.CLICK_DISMISS, - Selector.DATA_DISMISS, - event => this.hide(event) - ) - - EventHandler.on(this._dialog, Event.MOUSEDOWN_DISMISS, () => { - EventHandler.one(this._element, Event.MOUSEUP_DISMISS, event => { - if (event.target === this._element) { - this._ignoreBackdropClick = true - } - }) - }) - - this._showBackdrop(() => this._showElement(relatedTarget)) - } - - hide(event) { - if (event) { - event.preventDefault() - } - - if (!this._isShown || this._isTransitioning) { - return - } - - const hideEvent = EventHandler.trigger(this._element, Event.HIDE) - - if (hideEvent.defaultPrevented) { - return - } - - this._isShown = false - const transition = this._element.classList.contains(ClassName.FADE) - - if (transition) { - this._isTransitioning = true - } - - this._setEscapeEvent() - this._setResizeEvent() - - EventHandler.off(document, Event.FOCUSIN) - - this._element.classList.remove(ClassName.SHOW) - - EventHandler.off(this._element, Event.CLICK_DISMISS) - EventHandler.off(this._dialog, Event.MOUSEDOWN_DISMISS) - - if (transition) { - const transitionDuration = getTransitionDurationFromElement(this._element) - - EventHandler.one(this._element, TRANSITION_END, event => this._hideModal(event)) - emulateTransitionEnd(this._element, transitionDuration) - } else { - this._hideModal() - } - } - - dispose() { - [window, this._element, this._dialog] - .forEach(htmlElement => EventHandler.off(htmlElement, EVENT_KEY)) - - /** - * `document` has 2 events `Event.FOCUSIN` and `Event.CLICK_DATA_API` - * Do not move `document` in `htmlElements` array - * It will remove `Event.CLICK_DATA_API` event that should remain - */ - EventHandler.off(document, Event.FOCUSIN) - - Data.removeData(this._element, DATA_KEY) - - this._config = null - this._element = null - this._dialog = null - this._backdrop = null - this._isShown = null - this._isBodyOverflowing = null - this._ignoreBackdropClick = null - this._isTransitioning = null - this._scrollbarWidth = null - } - - handleUpdate() { - this._adjustDialog() - } - - // Private - - _getConfig(config) { - config = { - ...Default, - ...config - } - typeCheckConfig(NAME, config, DefaultType) - return config - } - - _showElement(relatedTarget) { - const transition = this._element.classList.contains(ClassName.FADE) - const modalBody = SelectorEngine.findOne(Selector.MODAL_BODY, this._dialog) - - if (!this._element.parentNode || - this._element.parentNode.nodeType !== Node.ELEMENT_NODE) { - // Don't move modal's DOM position - document.body.appendChild(this._element) - } - - this._element.style.display = 'block' - this._element.removeAttribute('aria-hidden') - this._element.setAttribute('aria-modal', true) - - if (this._dialog.classList.contains(ClassName.SCROLLABLE) && modalBody) { - modalBody.scrollTop = 0 - } else { - this._element.scrollTop = 0 - } - - if (transition) { - reflow(this._element) - } - - this._element.classList.add(ClassName.SHOW) - - if (this._config.focus) { - this._enforceFocus() - } - - const transitionComplete = () => { - if (this._config.focus) { - this._element.focus() - } - - this._isTransitioning = false - EventHandler.trigger(this._element, Event.SHOWN, { - relatedTarget - }) - } - - if (transition) { - const transitionDuration = getTransitionDurationFromElement(this._dialog) - - EventHandler.one(this._dialog, TRANSITION_END, transitionComplete) - emulateTransitionEnd(this._dialog, transitionDuration) - } else { - transitionComplete() - } - } - - _enforceFocus() { - EventHandler.off(document, Event.FOCUSIN) // guard against infinite focus loop - EventHandler.on(document, Event.FOCUSIN, event => { - if (document !== event.target && - this._element !== event.target && - !this._element.contains(event.target)) { - this._element.focus() - } - }) - } - - _setEscapeEvent() { - if (this._isShown && this._config.keyboard) { - EventHandler.on(this._element, Event.KEYDOWN_DISMISS, event => { - if (event.which === ESCAPE_KEYCODE) { - event.preventDefault() - this.hide() - } - }) - } else { - EventHandler.off(this._element, Event.KEYDOWN_DISMISS) - } - } - - _setResizeEvent() { - if (this._isShown) { - EventHandler.on(window, Event.RESIZE, () => this._adjustDialog()) - } else { - EventHandler.off(window, Event.RESIZE) - } - } - - _hideModal() { - this._element.style.display = 'none' - this._element.setAttribute('aria-hidden', true) - this._element.removeAttribute('aria-modal') - this._isTransitioning = false - this._showBackdrop(() => { - document.body.classList.remove(ClassName.OPEN) - this._resetAdjustments() - this._resetScrollbar() - EventHandler.trigger(this._element, Event.HIDDEN) - }) - } - - _removeBackdrop() { - this._backdrop.parentNode.removeChild(this._backdrop) - this._backdrop = null - } - - _showBackdrop(callback) { - const animate = this._element.classList.contains(ClassName.FADE) ? - ClassName.FADE : - '' - - if (this._isShown && this._config.backdrop) { - this._backdrop = document.createElement('div') - this._backdrop.className = ClassName.BACKDROP - - if (animate) { - this._backdrop.classList.add(animate) - } - - document.body.appendChild(this._backdrop) - - EventHandler.on(this._element, Event.CLICK_DISMISS, event => { - if (this._ignoreBackdropClick) { - this._ignoreBackdropClick = false - return - } - - if (event.target !== event.currentTarget) { - return - } - - if (this._config.backdrop === 'static') { - this._element.focus() - } else { - this.hide() - } - }) - - if (animate) { - reflow(this._backdrop) - } - - this._backdrop.classList.add(ClassName.SHOW) - - if (!animate) { - callback() - return - } - - const backdropTransitionDuration = getTransitionDurationFromElement(this._backdrop) - - EventHandler.one(this._backdrop, TRANSITION_END, callback) - emulateTransitionEnd(this._backdrop, backdropTransitionDuration) - } else if (!this._isShown && this._backdrop) { - this._backdrop.classList.remove(ClassName.SHOW) - - const callbackRemove = () => { - this._removeBackdrop() - callback() - } - - if (this._element.classList.contains(ClassName.FADE)) { - const backdropTransitionDuration = getTransitionDurationFromElement(this._backdrop) - EventHandler.one(this._backdrop, TRANSITION_END, callbackRemove) - emulateTransitionEnd(this._backdrop, backdropTransitionDuration) - } else { - callbackRemove() - } - } else { - callback() - } - } - - // ---------------------------------------------------------------------- - // the following methods are used to handle overflowing modals - // ---------------------------------------------------------------------- - - _adjustDialog() { - const isModalOverflowing = - this._element.scrollHeight > document.documentElement.clientHeight - - if (!this._isBodyOverflowing && isModalOverflowing) { - this._element.style.paddingLeft = `${this._scrollbarWidth}px` - } - - if (this._isBodyOverflowing && !isModalOverflowing) { - this._element.style.paddingRight = `${this._scrollbarWidth}px` - } - } - - _resetAdjustments() { - this._element.style.paddingLeft = '' - this._element.style.paddingRight = '' - } - - _checkScrollbar() { - const rect = document.body.getBoundingClientRect() - this._isBodyOverflowing = rect.left + rect.right < window.innerWidth - this._scrollbarWidth = this._getScrollbarWidth() - } - - _setScrollbar() { - if (this._isBodyOverflowing) { - // Note: DOMNode.style.paddingRight returns the actual value or '' if not set - // while $(DOMNode).css('padding-right') returns the calculated value or 0 if not set - - // Adjust fixed content padding - makeArray(SelectorEngine.find(Selector.FIXED_CONTENT)) - .forEach(element => { - const actualPadding = element.style.paddingRight - const calculatedPadding = window.getComputedStyle(element)['padding-right'] - Manipulator.setDataAttribute(element, 'padding-right', actualPadding) - element.style.paddingRight = `${parseFloat(calculatedPadding) + this._scrollbarWidth}px` - }) - - // Adjust sticky content margin - makeArray(SelectorEngine.find(Selector.STICKY_CONTENT)) - .forEach(element => { - const actualMargin = element.style.marginRight - const calculatedMargin = window.getComputedStyle(element)['margin-right'] - Manipulator.setDataAttribute(element, 'margin-right', actualMargin) - element.style.marginRight = `${parseFloat(calculatedMargin) - this._scrollbarWidth}px` - }) - - // Adjust body padding - const actualPadding = document.body.style.paddingRight - const calculatedPadding = window.getComputedStyle(document.body)['padding-right'] - - Manipulator.setDataAttribute(document.body, 'padding-right', actualPadding) - document.body.style.paddingRight = `${parseFloat(calculatedPadding) + this._scrollbarWidth}px` - } - - document.body.classList.add(ClassName.OPEN) - } - - _resetScrollbar() { - // Restore fixed content padding - makeArray(SelectorEngine.find(Selector.FIXED_CONTENT)) - .forEach(element => { - const padding = Manipulator.getDataAttribute(element, 'padding-right') - if (typeof padding !== 'undefined') { - Manipulator.removeDataAttribute(element, 'padding-right') - element.style.paddingRight = padding - } - }) - - // Restore sticky content and navbar-toggler margin - makeArray(SelectorEngine.find(`${Selector.STICKY_CONTENT}`)) - .forEach(element => { - const margin = Manipulator.getDataAttribute(element, 'margin-right') - if (typeof margin !== 'undefined') { - Manipulator.removeDataAttribute(element, 'margin-right') - element.style.marginRight = margin - } - }) - - // Restore body padding - const padding = Manipulator.getDataAttribute(document.body, 'padding-right') - if (typeof padding === 'undefined') { - document.body.style.paddingRight = '' - } else { - Manipulator.removeDataAttribute(document.body, 'padding-right') - document.body.style.paddingRight = padding - } - } - - _getScrollbarWidth() { // thx d.walsh - const scrollDiv = document.createElement('div') - scrollDiv.className = ClassName.SCROLLBAR_MEASURER - document.body.appendChild(scrollDiv) - const scrollbarWidth = scrollDiv.getBoundingClientRect().width - scrollDiv.clientWidth - document.body.removeChild(scrollDiv) - return scrollbarWidth - } - - // Static - - static jQueryInterface(config, relatedTarget) { - return this.each(function () { - let data = Data.getData(this, DATA_KEY) - const _config = { - ...Default, - ...Manipulator.getDataAttributes(this), - ...typeof config === 'object' && config ? config : {} - } - - if (!data) { - data = new Modal(this, _config) - } - - if (typeof config === 'string') { - if (typeof data[config] === 'undefined') { - throw new TypeError(`No method named "${config}"`) - } - - data[config](relatedTarget) - } else if (_config.show) { - data.show(relatedTarget) - } - }) - } - - static getInstance(element) { - return Data.getData(element, DATA_KEY) - } -} - -/** - * ------------------------------------------------------------------------ - * Data Api implementation - * ------------------------------------------------------------------------ - */ - -EventHandler.on(document, Event.CLICK_DATA_API, Selector.DATA_TOGGLE, function (event) { - const target = getElementFromSelector(this) - - if (this.tagName === 'A' || this.tagName === 'AREA') { - event.preventDefault() - } - - EventHandler.one(target, Event.SHOW, showEvent => { - if (showEvent.defaultPrevented) { - // only register focus restorer if modal will actually get shown - return - } - - EventHandler.one(target, Event.HIDDEN, () => { - if (isVisible(this)) { - this.focus() - } - }) - }) - - let data = Data.getData(target, DATA_KEY) - if (!data) { - const config = { - ...Manipulator.getDataAttributes(target), - ...Manipulator.getDataAttributes(this) - } - - data = new Modal(target, config) - } - - data.show(this) -}) - -const $ = getjQuery() - -/** - * ------------------------------------------------------------------------ - * jQuery - * ------------------------------------------------------------------------ - * add .modal to jQuery only if jQuery is present - */ -/* istanbul ignore if */ -if ($) { - const JQUERY_NO_CONFLICT = $.fn[NAME] - $.fn[NAME] = Modal.jQueryInterface - $.fn[NAME].Constructor = Modal - $.fn[NAME].noConflict = () => { - $.fn[NAME] = JQUERY_NO_CONFLICT - return Modal.jQueryInterface - } -} - -export default Modal diff --git a/js/src/modal/modal.spec.js b/js/src/modal/modal.spec.js deleted file mode 100644 index 292f61f8b..000000000 --- a/js/src/modal/modal.spec.js +++ /dev/null @@ -1,987 +0,0 @@ -import Modal from './modal' -import EventHandler from '../dom/event-handler' -import { makeArray } from '../util/index' - -/** Test helpers */ -import { getFixture, clearFixture, createEvent, jQueryMock } from '../../tests/helpers/fixture' - -describe('Modal', () => { - let fixtureEl - let style - - beforeAll(() => { - fixtureEl = getFixture() - - // Enable the scrollbar measurer - const css = '.modal-scrollbar-measure { position: absolute; top: -9999px; width: 50px; height: 50px; overflow: scroll; }' - - style = document.createElement('style') - style.type = 'text/css' - style.appendChild(document.createTextNode(css)) - - document.head.appendChild(style) - - // Simulate scrollbars - document.documentElement.style.paddingRight = '16px' - }) - - afterEach(() => { - clearFixture() - - document.body.classList.remove('modal-open') - document.body.removeAttribute('style') - document.body.removeAttribute('data-padding-right') - const backdropList = makeArray(document.querySelectorAll('.modal-backdrop')) - - backdropList.forEach(backdrop => { - document.body.removeChild(backdrop) - }) - - document.body.style.paddingRight = '0px' - }) - - afterAll(() => { - document.head.removeChild(style) - document.documentElement.style.paddingRight = '0px' - }) - - describe('VERSION', () => { - it('should return plugin version', () => { - expect(Modal.VERSION).toEqual(jasmine.any(String)) - }) - }) - - describe('Default', () => { - it('should return plugin default config', () => { - expect(Modal.Default).toEqual(jasmine.any(Object)) - }) - }) - - describe('toggle', () => { - it('should toggle a modal', done => { - fixtureEl.innerHTML = '