diff --git a/js/src/alert.js b/js/src/alert.js index 14b4b5204baa..a6a597a452c6 100644 --- a/js/src/alert.js +++ b/js/src/alert.js @@ -1,3 +1,6 @@ +import Data from './dom/data' +import EventHandler from './dom/eventHandler' +import SelectorEngine from './dom/selectorEngine' import Util from './util' @@ -8,7 +11,7 @@ import Util from './util' * -------------------------------------------------------------------------- */ -const Alert = (($) => { +const Alert = (() => { /** @@ -70,7 +73,7 @@ const Alert = (($) => { const rootElement = this._getRootElement(element) const customEvent = this._triggerCloseEvent(rootElement) - if (customEvent.isDefaultPrevented()) { + if (customEvent.defaultPrevented) { return } @@ -78,7 +81,7 @@ const Alert = (($) => { } dispose() { - $.removeData(this._element, DATA_KEY) + Data.removeData(this._element, DATA_KEY) this._element = null } @@ -90,42 +93,37 @@ const Alert = (($) => { let parent = false if (selector) { - parent = $(selector)[0] + parent = SelectorEngine.find(selector)[0] } if (!parent) { - parent = $(element).closest(`.${ClassName.ALERT}`)[0] + parent = SelectorEngine.closest(element, `.${ClassName.ALERT}`) } return parent } _triggerCloseEvent(element) { - const closeEvent = $.Event(Event.CLOSE) - - $(element).trigger(closeEvent) - return closeEvent + return EventHandler.trigger(element, Event.CLOSE) } _removeElement(element) { - $(element).removeClass(ClassName.SHOW) + element.classList.remove(ClassName.SHOW) if (!Util.supportsTransitionEnd() || - !$(element).hasClass(ClassName.FADE)) { + !element.classList.contains(ClassName.FADE)) { this._destroyElement(element) return } - $(element) - .one(Util.TRANSITION_END, (event) => this._destroyElement(element, event)) + EventHandler + .one(element, Util.TRANSITION_END, (event) => this._destroyElement(element, event)) Util.emulateTransitionEnd(element, TRANSITION_DURATION) } _destroyElement(element) { - $(element) - .detach() - .trigger(Event.CLOSED) - .remove() + EventHandler.trigger(element, Event.CLOSED) + element.parentNode.removeChild(element) } @@ -133,12 +131,11 @@ const Alert = (($) => { static _jQueryInterface(config) { return this.each(function () { - const $element = $(this) - let data = $element.data(DATA_KEY) + let data = Data.getData(this, DATA_KEY) if (!data) { data = new Alert(this) - $element.data(DATA_KEY, data) + Data.setData(this, DATA_KEY, data) } if (config === 'close') { @@ -165,29 +162,28 @@ const Alert = (($) => { * Data Api implementation * ------------------------------------------------------------------------ */ - - $(document).on( - Event.CLICK_DATA_API, - Selector.DISMISS, - Alert._handleDismiss(new Alert()) - ) + EventHandler.on(document, Event.CLICK_DATA_API, Selector.DISMISS, Alert._handleDismiss(new Alert())) /** * ------------------------------------------------------------------------ * jQuery * ------------------------------------------------------------------------ + * add .alert to jQuery only if jQuery is present */ - $.fn[NAME] = Alert._jQueryInterface - $.fn[NAME].Constructor = Alert - $.fn[NAME].noConflict = function () { - $.fn[NAME] = JQUERY_NO_CONFLICT - return Alert._jQueryInterface + if (typeof window.$ !== 'undefined' || typeof window.jQuery !== 'undefined') { + const $ = window.$ || window.jQuery + $.fn[NAME] = Alert._jQueryInterface + $.fn[NAME].Constructor = Alert + $.fn[NAME].noConflict = function () { + $.fn[NAME] = JQUERY_NO_CONFLICT + return Alert._jQueryInterface + } } return Alert -})(jQuery) +})() export default Alert diff --git a/js/src/dom/data.js b/js/src/dom/data.js new file mode 100644 index 000000000000..bbe807aac1e1 --- /dev/null +++ b/js/src/dom/data.js @@ -0,0 +1,51 @@ +/** + * -------------------------------------------------------------------------- + * Bootstrap (v4.0.0-beta): dom/data.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + * -------------------------------------------------------------------------- + */ + +const mapData = (() => { + const storeData = {} + return { + set(element, key, data) { + let id + if (element.key === undefined) { + element.key = { + key, + id + } + } + + storeData[id] = data + }, + get(element, key) { + if (element.key === undefined || element.key !== key) { + return null + } + const keyProperties = element.key + return storeData[keyProperties.id] + }, + delete(element, key) { + if (element.key === undefined || element.key !== key) { + return + } + const keyProperties = element.key + delete storeData[keyProperties.id] + } + } +})() + +const Data = { + setData(instance, key, data) { + mapData.set(instance, key, data) + }, + getData(instance, key) { + mapData.get(instance, key) + }, + removeData(instance, key) { + mapData.delete(instance, key) + } +} + +export default Data diff --git a/js/src/dom/event.js b/js/src/dom/event.js deleted file mode 100644 index f1217f9ba727..000000000000 --- a/js/src/dom/event.js +++ /dev/null @@ -1,61 +0,0 @@ -/** - * -------------------------------------------------------------------------- - * Bootstrap (v4.0.0-beta): dom/event.js - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) - * -------------------------------------------------------------------------- - */ - -const TransitionEndEvent = { - WebkitTransition : 'webkitTransitionEnd', - MozTransition : 'transitionend', - OTransition : 'oTransitionEnd otransitionend', - transition : 'transitionend' -} - -const Event = { - on(element, event, handler) { - if (typeof event !== 'string') { - return - } - element.addEventListener(event, handler, false) - }, - - one(element, event, handler) { - const complete = () => { - /* eslint func-style: off */ - handler() - element.removeEventListener(event, complete, false) - } - Event.on(element, event, complete) - }, - - trigger(element, event) { - if (typeof event !== 'string') { - return - } - - const eventToDispatch = new CustomEvent(event, { - bubbles: true, - cancelable: true - }) - element.dispatchEvent(eventToDispatch) - }, - - getBrowserTransitionEnd() { - if (window.QUnit) { - return false - } - - const el = document.createElement('bootstrap') - for (const name in TransitionEndEvent) { - if (el.style[name] !== undefined) { - return { - end: TransitionEndEvent[name] - } - } - } - return false - } -} - -export default Event diff --git a/js/src/dom/eventHandler.js b/js/src/dom/eventHandler.js new file mode 100644 index 000000000000..448833318b5b --- /dev/null +++ b/js/src/dom/eventHandler.js @@ -0,0 +1,216 @@ +/** + * -------------------------------------------------------------------------- + * Bootstrap (v4.0.0-beta): dom/eventHandler.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + * -------------------------------------------------------------------------- + */ + +const TransitionEndEvent = { + WebkitTransition : 'webkitTransitionEnd', + MozTransition : 'transitionend', + OTransition : 'oTransitionEnd otransitionend', + transition : 'transitionend' +} + +// defaultPrevented is broken in IE. +// https://connect.microsoft.com/IE/feedback/details/790389/event-defaultprevented-returns-false-after-preventdefault-was-called +const workingDefaultPrevented = (() => { + const e = document.createEvent('CustomEvent') + e.initEvent('Bootstrap', true, true) + e.preventDefault() + return e.defaultPrevented +})() + +// CustomEvent polyfill for IE (see: https://mzl.la/2v76Zvn) +if (typeof window.CustomEvent !== 'function') { + window.CustomEvent = (event, params) => { + params = params || { + bubbles: false, + cancelable: false, + detail: undefined + } + const evt = document.createEvent('CustomEvent') + evt.initCustomEvent(event, params.bubbles, params.cancelable, params.detail) + if (!workingDefaultPrevented) { + const origPreventDefault = Event.prototype.preventDefault + evt.preventDefault = () => { + if (!evt.cancelable) { + return + } + + origPreventDefault.call(evt) + Object.defineProperty(evt, 'defaultPrevented', { + get() { + return true + }, + configurable: true + }) + } + } + return evt + } + + window.CustomEvent.prototype = window.Event.prototype +} + +// Event constructor shim +if (!window.Event || typeof window.Event !== 'function') { + const origEvent = window.Event + window.Event = (inType, params) => { + params = params || {} + const e = document.createEvent('Event') + e.initEvent(inType, Boolean(params.bubbles), Boolean(params.cancelable)) + return e + } + window.Event.prototype = origEvent.prototype +} + +const namespaceRegex = /[^.]*(?=\..*)\.|.*/ +const stripNameRegex = /\..*/ + +// Events storage +const eventRegistry = {} +let uidEvent = 1 + +function getUidEvent(element, uid) { + return element.uidEvent = uid && `${uid}::${uidEvent++}` || element.uidEvent || uidEvent++ +} + +function getEvent(element) { + const uid = getUidEvent(element) + return eventRegistry[uid] = eventRegistry[uid] || {} +} + +const nativeEvents = +`click,dblclick,mouseup,mousedown,contextmenu, +'mousewheel,DOMMouseScroll, +'mouseover,mouseout,mousemove,selectstart,selectend, +'keydown,keypress,keyup, +'orientationchange, +'touchstart,touchmove,touchend,touchcancel, +'gesturestart,gesturechange,gestureend, +'focus,blur,change,reset,select,submit, +'load,unload,beforeunload,resize,move,DOMContentLoaded,readystatechange, +'error,abort,scroll`.split(',') + +function bootstrapHandler(element, fn) { + return function (event) { + return fn.apply(element, [event]) + } +} + +function bootstrapDelegationHandler(selector, fn) { + return function (event) { + const domElements = document.querySelectorAll(selector) + for (let target = event.target; target && target !== this; target = target.parentNode) { + for (let i = domElements.length; i--;) { + if (domElements[i] === target) { + return fn.apply(target, [event]) + } + } + } + // To please ESLint + return null + } +} + +const EventHandler = { + on(element, originalTypeEvent, handler, delegationFn) { + if (typeof originalTypeEvent !== 'string' + || (typeof element === 'undefined' || element === null)) { + return + } + + const delegation = typeof handler === 'string' + // allow to get the native events from namespaced events ('click.bs.button' --> 'click') + let typeEvent = originalTypeEvent.replace(stripNameRegex, '') + const isNative = nativeEvents.indexOf(typeEvent) > -1 + if (!isNative) { + typeEvent = originalTypeEvent + } + const events = getEvent(element) + const handlers = events[typeEvent] || (events[typeEvent] = {}) + const uid = getUidEvent(handler, originalTypeEvent.replace(namespaceRegex, '')) + if (handlers[uid]) { + return + } + + const fn = !delegation ? bootstrapHandler(element, handler) : bootstrapDelegationHandler(handler, delegationFn) + handlers[uid] = fn + handler.uidEvent = uid + element.addEventListener(typeEvent, fn, false) + }, + + one(element, event, handler) { + function complete(e) { + const typeEvent = event.replace(stripNameRegex, '') + const events = getEvent(element) + if (!events || !events[typeEvent]) { + return + } + handler.apply(element, [e]) + EventHandler.off(element, event, complete) + } + EventHandler.on(element, event, complete) + }, + + off(element, originalTypeEvent, handler) { + if (typeof originalTypeEvent !== 'string' + || (typeof element === 'undefined' || element === null)) { + return + } + + const typeEvent = originalTypeEvent.replace(stripNameRegex, '') + const events = getEvent(element) + if (!events || !events[typeEvent]) { + return + } + + const uidEvent = handler.uidEvent + const fn = events[typeEvent][uidEvent] + element.removeEventListener(typeEvent, fn, false) + delete events[typeEvent][uidEvent] + }, + + trigger(element, event) { + if (typeof event !== 'string' + || (typeof element === 'undefined' || element === null)) { + return null + } + const typeEvent = event.replace(stripNameRegex, '') + const isNative = nativeEvents.indexOf(typeEvent) > -1 + let returnedEvent = null + if (isNative) { + const evt = document.createEvent('HTMLEvents') + evt.initEvent(typeEvent, true, true) + element.dispatchEvent(evt) + returnedEvent = evt + } else { + const eventToDispatch = new CustomEvent(event, { + bubbles: true, + cancelable: true + }) + element.dispatchEvent(eventToDispatch) + returnedEvent = eventToDispatch + } + return returnedEvent + }, + + getBrowserTransitionEnd() { + if (window.QUnit) { + return false + } + + const el = document.createElement('bootstrap') + for (const name in TransitionEndEvent) { + if (el.style[name] !== undefined) { + return { + end: TransitionEndEvent[name] + } + } + } + return false + } +} + +export default EventHandler diff --git a/js/src/dom/selectorEngine.js b/js/src/dom/selectorEngine.js new file mode 100644 index 000000000000..1b33bf62d6d8 --- /dev/null +++ b/js/src/dom/selectorEngine.js @@ -0,0 +1,66 @@ +/** + * -------------------------------------------------------------------------- + * Bootstrap (v4.0.0-beta): dom/selectorEngine.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + * -------------------------------------------------------------------------- + */ + +// matches polyfill (see: https://mzl.la/2ikXneG) +let fnMatches = null +if (!Element.prototype.matches) { + fnMatches = + Element.prototype.msMatchesSelector || + Element.prototype.webkitMatchesSelector +} else { + fnMatches = Element.prototype.matches +} + +// closest polyfill (see: https://mzl.la/2vXggaI) +let fnClosest = null +if (!Element.prototype.closest) { + fnClosest = (element, selector) => { + let ancestor = element + if (!document.documentElement.contains(element)) { + return null + } + + do { + if (fnMatches.call(ancestor, selector)) { + return ancestor + } + + ancestor = ancestor.parentElement + } while (ancestor !== null) + + return null + } +} else { + fnClosest = (element, selector) => { + return element.closest(selector) + } +} + +const SelectorEngine = { + matches(element, selector) { + return fnMatches.call(element, selector) + }, + + find(selector) { + if (typeof selector !== 'string') { + return null + } + + let selectorType = 'querySelectorAll' + if (selector.indexOf('#') === 0) { + selectorType = 'getElementById' + selector = selector.substr(1, selector.length) + } + return document[selectorType](selector) + }, + + closest(element, selector) { + return fnClosest(element, selector) + } +} + +export default SelectorEngine diff --git a/js/src/util.js b/js/src/util.js index 68f4c3028b50..6dfb179708a4 100644 --- a/js/src/util.js +++ b/js/src/util.js @@ -1,4 +1,4 @@ -import Event from './dom/event' +import EventHandler from './dom/eventHandler' /** * -------------------------------------------------------------------------- @@ -9,7 +9,7 @@ import Event from './dom/event' const Util = (() => { - const transition = Event.getBrowserTransitionEnd() + const transition = EventHandler.getBrowserTransitionEnd() const MAX_UID = 1000000 @@ -60,7 +60,7 @@ const Util = (() => { }, triggerTransitionEnd(element) { - Event.trigger(element, Util.TRANSITION_END) + EventHandler.trigger(element, Util.TRANSITION_END) }, supportsTransitionEnd() { diff --git a/js/tests/.eslintrc.json b/js/tests/.eslintrc.json index a05a3a3900fa..8d1f338dcdda 100644 --- a/js/tests/.eslintrc.json +++ b/js/tests/.eslintrc.json @@ -4,6 +4,7 @@ "qunit": true }, "globals": { + "EventHandler": false, "Util": false }, "parserOptions": { diff --git a/js/tests/index.html b/js/tests/index.html index b9daafbc6352..90c15ee1a840 100644 --- a/js/tests/index.html +++ b/js/tests/index.html @@ -96,7 +96,9 @@ - + + + diff --git a/js/tests/unit/alert.js b/js/tests/unit/alert.js index e078082c3c87..5ab9b37411d9 100644 --- a/js/tests/unit/alert.js +++ b/js/tests/unit/alert.js @@ -41,8 +41,8 @@ $(function () { var $alert = $(alertHTML).bootstrapAlert().appendTo($('#qunit-fixture')) - $alert.find('.close').trigger('click') - + var closeBtn = $alert.find('.close')[0] + EventHandler.trigger(closeBtn, 'click') assert.strictEqual($alert.hasClass('show'), false, 'remove .show class on .close click') }) @@ -56,7 +56,8 @@ $(function () { assert.notEqual($('#qunit-fixture').find('.alert').length, 0, 'element added to dom') - $alert.find('.close').trigger('click') + var closeBtn = $alert.find('.close')[0] + EventHandler.trigger(closeBtn, 'click') assert.strictEqual($('#qunit-fixture').find('.alert').length, 0, 'element removed from dom') }) @@ -64,16 +65,19 @@ $(function () { QUnit.test('should not fire closed when close is prevented', function (assert) { assert.expect(1) var done = assert.async() - $('
') - .on('close.bs.alert', function (e) { - e.preventDefault() - assert.ok(true, 'close event fired') - done() - }) - .on('closed.bs.alert', function () { - assert.ok(false, 'closed event fired') - }) - .bootstrapAlert('close') + var $alert = $('
') + $alert.appendTo('#qunit-fixture') + + EventHandler.on($alert[0], 'close.bs.alert', function (e) { + e.preventDefault() + assert.ok(true, 'close event fired') + done() + }) + EventHandler.on($alert[0], 'closed.bs.alert', function () { + assert.ok(false, 'closed event fired') + }) + + $alert.bootstrapAlert('close') }) }) diff --git a/js/tests/visual/alert.html b/js/tests/visual/alert.html index 22320a94ab9d..4d4ff73b006c 100644 --- a/js/tests/visual/alert.html +++ b/js/tests/visual/alert.html @@ -45,7 +45,9 @@

Alert Bootstrap Visual Test

- + + + diff --git a/js/tests/visual/button.html b/js/tests/visual/button.html index 5ad9a50ba304..910966847c17 100644 --- a/js/tests/visual/button.html +++ b/js/tests/visual/button.html @@ -45,7 +45,7 @@

Button Bootstrap Visual Test

- + diff --git a/js/tests/visual/carousel.html b/js/tests/visual/carousel.html index 1c72122f8815..d4bb5d3a3bde 100644 --- a/js/tests/visual/carousel.html +++ b/js/tests/visual/carousel.html @@ -41,7 +41,7 @@

Carousel Bootstrap Visual Test

- + diff --git a/js/tests/visual/collapse.html b/js/tests/visual/collapse.html index f9c800173003..a505f736a741 100644 --- a/js/tests/visual/collapse.html +++ b/js/tests/visual/collapse.html @@ -58,7 +58,7 @@
- + diff --git a/js/tests/visual/dropdown.html b/js/tests/visual/dropdown.html index 1d6474a5c3f4..f696de747957 100644 --- a/js/tests/visual/dropdown.html +++ b/js/tests/visual/dropdown.html @@ -119,7 +119,7 @@

Dropdown Bootstrap Visual Test

- + diff --git a/js/tests/visual/modal.html b/js/tests/visual/modal.html index 6e50e6b75823..b36ab9e8b332 100644 --- a/js/tests/visual/modal.html +++ b/js/tests/visual/modal.html @@ -171,7 +171,7 @@ - + diff --git a/js/tests/visual/popover.html b/js/tests/visual/popover.html index 6c2df5645902..ee517f1e007b 100644 --- a/js/tests/visual/popover.html +++ b/js/tests/visual/popover.html @@ -34,7 +34,7 @@

Popover Bootstrap Visual Test

- + diff --git a/js/tests/visual/scrollspy.html b/js/tests/visual/scrollspy.html index 7a7a1ce60229..43b08029fb5d 100644 --- a/js/tests/visual/scrollspy.html +++ b/js/tests/visual/scrollspy.html @@ -88,7 +88,7 @@

Final section

- + diff --git a/js/tests/visual/tab.html b/js/tests/visual/tab.html index 857a81391524..100fa88e4fe7 100644 --- a/js/tests/visual/tab.html +++ b/js/tests/visual/tab.html @@ -226,7 +226,7 @@

Tabs with list-group (with fade)

- + diff --git a/package.json b/package.json index 03053982f20d..b0421f41840f 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,7 @@ "js-lint": "eslint js/ && eslint --config js/tests/.eslintrc.json --env node build/ Gruntfile.js", "js-lint-docs": "eslint --config js/tests/.eslintrc.json assets/js/", "js-compile": "npm-run-all --parallel js-compile-*", - "js-compile-bundle": "shx cat js/src/dom/event.js js/src/util.js js/src/alert.js js/src/button.js js/src/carousel.js js/src/collapse.js js/src/dropdown.js js/src/modal.js js/src/scrollspy.js js/src/tab.js js/src/tooltip.js js/src/popover.js | shx sed \"s/^(import|export).*//\" | babel --filename js/src/bootstrap.js | node build/stamp.js > dist/js/bootstrap.js", + "js-compile-bundle": "shx cat js/src/dom/eventHandler.js js/src/dom/selectorEngine.js js/src/dom/data.js js/src/util.js js/src/alert.js js/src/button.js js/src/carousel.js js/src/collapse.js js/src/dropdown.js js/src/modal.js js/src/scrollspy.js js/src/tab.js js/src/tooltip.js js/src/popover.js | shx sed \"s/^(import|export).*//\" | babel --filename js/src/bootstrap.js | node build/stamp.js > dist/js/bootstrap.js", "js-compile-plugins": "babel js/src/ --out-dir js/dist/ --source-maps", "js-minify": "uglifyjs --config-file build/uglifyjs.config.json --output dist/js/bootstrap.min.js dist/js/bootstrap.js", "js-minify-docs": "uglifyjs --config-file build/uglifyjs.config.json --output assets/js/docs.min.js assets/js/vendor/anchor.min.js assets/js/vendor/clipboard.min.js assets/js/vendor/holder.min.js assets/js/src/application.js assets/js/src/pwa.js",