From 2985fb6ae92c16a838d4d707e7b69d3e16d57e0c Mon Sep 17 00:00:00 2001 From: Johann-S Date: Mon, 21 Aug 2017 09:11:37 +0200 Subject: [PATCH 1/7] Near the end of removing jQuery from Alert --- js/src/alert.js | 36 ++++++++--------- js/src/dom/data.js | 51 ++++++++++++++++++++++++ js/src/dom/{event.js => eventHandler.js} | 15 +++---- js/src/dom/selectorEngine.js | 42 +++++++++++++++++++ js/src/util.js | 6 +-- js/tests/index.html | 4 +- js/tests/visual/alert.html | 4 +- js/tests/visual/button.html | 2 +- js/tests/visual/carousel.html | 2 +- js/tests/visual/collapse.html | 2 +- js/tests/visual/dropdown.html | 2 +- js/tests/visual/modal.html | 2 +- js/tests/visual/popover.html | 2 +- js/tests/visual/scrollspy.html | 2 +- js/tests/visual/tab.html | 2 +- package.json | 2 +- 16 files changed, 136 insertions(+), 40 deletions(-) create mode 100644 js/src/dom/data.js rename js/src/dom/{event.js => eventHandler.js} (79%) create mode 100644 js/src/dom/selectorEngine.js diff --git a/js/src/alert.js b/js/src/alert.js index 14b4b5204baa..4808d2c3d74f 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' @@ -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,38 @@ const Alert = (($) => { let parent = false if (selector) { - parent = $(selector)[0] + const tmpSelected = SelectorEngine.find(selector) + parent = tmpSelected[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 +132,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') { 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/eventHandler.js similarity index 79% rename from js/src/dom/event.js rename to js/src/dom/eventHandler.js index f1217f9ba727..3fe0c31a46d5 100644 --- a/js/src/dom/event.js +++ b/js/src/dom/eventHandler.js @@ -1,6 +1,6 @@ /** * -------------------------------------------------------------------------- - * Bootstrap (v4.0.0-beta): dom/event.js + * Bootstrap (v4.0.0-beta): dom/eventHandler.js * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) * -------------------------------------------------------------------------- */ @@ -12,9 +12,9 @@ const TransitionEndEvent = { transition : 'transitionend' } -const Event = { +const EventHandler = { on(element, event, handler) { - if (typeof event !== 'string') { + if (typeof event !== 'string' || typeof element === 'undefined') { return } element.addEventListener(event, handler, false) @@ -26,12 +26,12 @@ const Event = { handler() element.removeEventListener(event, complete, false) } - Event.on(element, event, complete) + EventHandler.on(element, event, complete) }, trigger(element, event) { - if (typeof event !== 'string') { - return + if (typeof event !== 'string' || typeof element === 'undefined') { + return null } const eventToDispatch = new CustomEvent(event, { @@ -39,6 +39,7 @@ const Event = { cancelable: true }) element.dispatchEvent(eventToDispatch) + return eventToDispatch }, getBrowserTransitionEnd() { @@ -58,4 +59,4 @@ const Event = { } } -export default Event +export default EventHandler diff --git a/js/src/dom/selectorEngine.js b/js/src/dom/selectorEngine.js new file mode 100644 index 000000000000..f6bcf6da265b --- /dev/null +++ b/js/src/dom/selectorEngine.js @@ -0,0 +1,42 @@ +/** + * -------------------------------------------------------------------------- + * Bootstrap (v4.0.0-beta): dom/selectorEngine.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + * -------------------------------------------------------------------------- + */ + +const SelectorEngine = { + matches: Element.prototype.msMatchesSelector || Element.prototype.webkitMatchesSelector, + + 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) { + let ancestor = element + if (!document.documentElement.contains(element)) { + return null + } + + do { + if (SelectorEngine.matches.call(ancestor, selector)) { + return ancestor + } + + ancestor = ancestor.parentElement + } while (ancestor !== null) + + return null + } +} + +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/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/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", From 2a5a34dd01a749d71d9e112d4521f88bb8f06952 Mon Sep 17 00:00:00 2001 From: Johann-S Date: Mon, 21 Aug 2017 15:18:41 +0200 Subject: [PATCH 2/7] fix unit test --- js/tests/.eslintrc.json | 1 + js/tests/unit/alert.js | 23 +++++++++++++---------- 2 files changed, 14 insertions(+), 10 deletions(-) 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/unit/alert.js b/js/tests/unit/alert.js index e078082c3c87..7574bd5a8a1b 100644 --- a/js/tests/unit/alert.js +++ b/js/tests/unit/alert.js @@ -64,16 +64,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') }) }) From 96e025c3d452a1c5150c842392ca45547f58db46 Mon Sep 17 00:00:00 2001 From: Johann-S Date: Mon, 21 Aug 2017 16:45:29 +0200 Subject: [PATCH 3/7] better polyfill for closest and matches functions --- js/src/dom/selectorEngine.js | 54 ++++++++++++++++++++++++++---------- 1 file changed, 39 insertions(+), 15 deletions(-) diff --git a/js/src/dom/selectorEngine.js b/js/src/dom/selectorEngine.js index f6bcf6da265b..1b33bf62d6d8 100644 --- a/js/src/dom/selectorEngine.js +++ b/js/src/dom/selectorEngine.js @@ -5,8 +5,45 @@ * -------------------------------------------------------------------------- */ +// 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.prototype.msMatchesSelector || Element.prototype.webkitMatchesSelector, + matches(element, selector) { + return fnMatches.call(element, selector) + }, find(selector) { if (typeof selector !== 'string') { @@ -22,20 +59,7 @@ const SelectorEngine = { }, closest(element, selector) { - let ancestor = element - if (!document.documentElement.contains(element)) { - return null - } - - do { - if (SelectorEngine.matches.call(ancestor, selector)) { - return ancestor - } - - ancestor = ancestor.parentElement - } while (ancestor !== null) - - return null + return fnClosest(element, selector) } } From 9b65b239e9315bf6e0b02d35fefc11612663e639 Mon Sep 17 00:00:00 2001 From: Johann-S Date: Mon, 21 Aug 2017 17:43:04 +0200 Subject: [PATCH 4/7] Add CustomEvent polyfill and a working preventDefault for IE --- js/src/alert.js | 2 +- js/src/dom/eventHandler.js | 58 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 1 deletion(-) diff --git a/js/src/alert.js b/js/src/alert.js index 4808d2c3d74f..2233cbf74659 100644 --- a/js/src/alert.js +++ b/js/src/alert.js @@ -73,7 +73,7 @@ const Alert = (($) => { const rootElement = this._getRootElement(element) const customEvent = this._triggerCloseEvent(rootElement) - if (customEvent.defaultPrevented) { + if (customEvent.isDefaultPrevented()) { return } diff --git a/js/src/dom/eventHandler.js b/js/src/dom/eventHandler.js index 3fe0c31a46d5..9096edaf6bff 100644 --- a/js/src/dom/eventHandler.js +++ b/js/src/dom/eventHandler.js @@ -12,6 +12,59 @@ const TransitionEndEvent = { 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 EventHandler = { on(element, event, handler) { if (typeof event !== 'string' || typeof element === 'undefined') { @@ -38,6 +91,11 @@ const EventHandler = { bubbles: true, cancelable: true }) + + // Add a function 'isDefaultPrevented' + eventToDispatch.isDefaultPrevented = () => { + return eventToDispatch.defaultPrevented + } element.dispatchEvent(eventToDispatch) return eventToDispatch }, From 438f06b06ca061e029ec628a844e032560487e3b Mon Sep 17 00:00:00 2001 From: Johann-S Date: Tue, 22 Aug 2017 18:47:54 +0200 Subject: [PATCH 5/7] Add namespaced events --- js/src/alert.js | 2 +- js/src/dom/eventHandler.js | 121 +++++++++++++++++++++++++++++++------ 2 files changed, 103 insertions(+), 20 deletions(-) diff --git a/js/src/alert.js b/js/src/alert.js index 2233cbf74659..4808d2c3d74f 100644 --- a/js/src/alert.js +++ b/js/src/alert.js @@ -73,7 +73,7 @@ const Alert = (($) => { const rootElement = this._getRootElement(element) const customEvent = this._triggerCloseEvent(rootElement) - if (customEvent.isDefaultPrevented()) { + if (customEvent.defaultPrevented) { return } diff --git a/js/src/dom/eventHandler.js b/js/src/dom/eventHandler.js index 9096edaf6bff..1e28a75ba8e7 100644 --- a/js/src/dom/eventHandler.js +++ b/js/src/dom/eventHandler.js @@ -65,39 +65,122 @@ if (!window.Event || typeof window.Event !== 'function') { 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]) + } +} + const EventHandler = { - on(element, event, handler) { - if (typeof event !== 'string' || typeof element === 'undefined') { + on(element, originalTypeEvent, handler) { + if (typeof originalTypeEvent !== 'string' + || (typeof element === 'undefined' || element === null)) { + return + } + + // 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, '')) + // TODO : Handle multi events on one element + if (handlers[uid]) { return } - element.addEventListener(event, handler, false) + + const fn = bootstrapHandler(element, handler) + handlers[uid] = fn + handler.uidEvent = uid + element.addEventListener(typeEvent, fn, false) }, one(element, event, handler) { - const complete = () => { - /* eslint func-style: off */ - handler() - element.removeEventListener(event, complete, false) + function complete(e) { + const typeEvent = event.replace(stripNameRegex, '') + const events = getEvent(element) + if (!events || !events[typeEvent]) { + return + } + const uidEvent = handler.uidEvent + const fn = events[typeEvent][uidEvent] + fn.apply(element, [e]) + EventHandler.off(element, event, handler) } EventHandler.on(element, event, complete) }, - trigger(element, event) { - if (typeof event !== 'string' || typeof element === 'undefined') { - return null + off(element, originalTypeEvent, handler) { + if (typeof originalTypeEvent !== 'string' + || (typeof element === 'undefined' || element === null)) { + return } - const eventToDispatch = new CustomEvent(event, { - bubbles: true, - cancelable: true - }) + 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] + }, - // Add a function 'isDefaultPrevented' - eventToDispatch.isDefaultPrevented = () => { - return eventToDispatch.defaultPrevented + 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 } - element.dispatchEvent(eventToDispatch) - return eventToDispatch + return returnedEvent }, getBrowserTransitionEnd() { From 48dbae676c7300658189ddb8941239f0390dabf3 Mon Sep 17 00:00:00 2001 From: Johann-S Date: Wed, 23 Aug 2017 12:01:38 +0200 Subject: [PATCH 6/7] Add event delegation + fix EventHandler.one --- js/src/alert.js | 7 +------ js/src/dom/eventHandler.js | 25 ++++++++++++++++++------- 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/js/src/alert.js b/js/src/alert.js index 4808d2c3d74f..f14ae2598354 100644 --- a/js/src/alert.js +++ b/js/src/alert.js @@ -163,12 +163,7 @@ 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())) /** diff --git a/js/src/dom/eventHandler.js b/js/src/dom/eventHandler.js index 1e28a75ba8e7..fa5fe89e2b6d 100644 --- a/js/src/dom/eventHandler.js +++ b/js/src/dom/eventHandler.js @@ -99,13 +99,27 @@ function bootstrapHandler(element, fn) { } } +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]) + } + } + } + } +} + const EventHandler = { - on(element, originalTypeEvent, handler) { + 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 @@ -115,12 +129,11 @@ const EventHandler = { const events = getEvent(element) const handlers = events[typeEvent] || (events[typeEvent] = {}) const uid = getUidEvent(handler, originalTypeEvent.replace(namespaceRegex, '')) - // TODO : Handle multi events on one element if (handlers[uid]) { return } - const fn = bootstrapHandler(element, handler) + const fn = !delegation ? bootstrapHandler(element, handler) : bootstrapDelegationHandler(handler, delegationFn) handlers[uid] = fn handler.uidEvent = uid element.addEventListener(typeEvent, fn, false) @@ -133,10 +146,8 @@ const EventHandler = { if (!events || !events[typeEvent]) { return } - const uidEvent = handler.uidEvent - const fn = events[typeEvent][uidEvent] - fn.apply(element, [e]) - EventHandler.off(element, event, handler) + handler.apply(element, [e]) + EventHandler.off(element, event, complete) } EventHandler.on(element, event, complete) }, From f862b8eba97471771493e252e7a32e9e83fcccd2 Mon Sep 17 00:00:00 2001 From: Johann-S Date: Wed, 23 Aug 2017 12:03:50 +0200 Subject: [PATCH 7/7] Remove jQuery from alert.js and add .alert only if jQuery is available --- js/src/alert.js | 21 ++++++++++++--------- js/src/dom/eventHandler.js | 4 +++- js/tests/unit/alert.js | 7 ++++--- 3 files changed, 19 insertions(+), 13 deletions(-) diff --git a/js/src/alert.js b/js/src/alert.js index f14ae2598354..a6a597a452c6 100644 --- a/js/src/alert.js +++ b/js/src/alert.js @@ -11,7 +11,7 @@ import Util from './util' * -------------------------------------------------------------------------- */ -const Alert = (($) => { +const Alert = (() => { /** @@ -93,8 +93,7 @@ const Alert = (($) => { let parent = false if (selector) { - const tmpSelected = SelectorEngine.find(selector) - parent = tmpSelected[0] + parent = SelectorEngine.find(selector)[0] } if (!parent) { @@ -170,17 +169,21 @@ const 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/eventHandler.js b/js/src/dom/eventHandler.js index fa5fe89e2b6d..448833318b5b 100644 --- a/js/src/dom/eventHandler.js +++ b/js/src/dom/eventHandler.js @@ -65,7 +65,7 @@ if (!window.Event || typeof window.Event !== 'function') { window.Event.prototype = origEvent.prototype } -const namespaceRegex = /[^\.]*(?=\..*)\.|.*/ +const namespaceRegex = /[^.]*(?=\..*)\.|.*/ const stripNameRegex = /\..*/ // Events storage @@ -109,6 +109,8 @@ function bootstrapDelegationHandler(selector, fn) { } } } + // To please ESLint + return null } } diff --git a/js/tests/unit/alert.js b/js/tests/unit/alert.js index 7574bd5a8a1b..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') })