From 08679ac0b5f34e1a1f1766be460e51bc1aa8d82a Mon Sep 17 00:00:00 2001 From: Johann-S Date: Sat, 16 Mar 2019 16:10:23 +0200 Subject: [PATCH] Add back support for IE 11 --- build/build-plugins.js | 16 +-- js/src/button.js | 10 +- js/src/dom/eventHandler.js | 6 +- js/src/dom/polyfill.js | 207 ++++++++++++++++++++----------- js/src/dom/selectorEngine.js | 7 +- js/src/util/index.js | 5 +- js/tests/browsers.js | 7 ++ js/tests/karma.conf.js | 9 +- js/tests/unit/modal.js | 25 ++-- js/tests/unit/tests-polyfills.js | 28 +++++ 10 files changed, 214 insertions(+), 106 deletions(-) create mode 100644 js/tests/unit/tests-polyfills.js diff --git a/build/build-plugins.js b/build/build-plugins.js index 8569e979f1..8a2873341e 100644 --- a/build/build-plugins.js +++ b/build/build-plugins.js @@ -31,7 +31,6 @@ const bsPlugins = { Data: path.resolve(__dirname, '../js/src/dom/data.js'), EventHandler: path.resolve(__dirname, '../js/src/dom/eventHandler.js'), 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/selectorEngine.js'), Alert: path.resolve(__dirname, '../js/src/alert.js'), Button: path.resolve(__dirname, '../js/src/button.js'), @@ -69,7 +68,8 @@ function getConfigByPluginKey(pluginKey) { if ( pluginKey === 'Data' || pluginKey === 'Manipulator' || - pluginKey === 'Polyfill' || + pluginKey === 'EventHandler' || + pluginKey === 'SelectorEngine' || pluginKey === 'Util' || pluginKey === 'Sanitizer' ) { @@ -79,17 +79,6 @@ function getConfigByPluginKey(pluginKey) { } } - if (pluginKey === 'EventHandler' || pluginKey === 'SelectorEngine') { - return { - external: [ - bsPlugins.Polyfill - ], - globals: { - [bsPlugins.Polyfill]: 'Polyfill' - } - } - } - if (pluginKey === 'Alert' || pluginKey === 'Tab') { return defaultPluginConfig } @@ -161,7 +150,6 @@ function build(plugin) { 'Data', 'EventHandler', 'Manipulator', - 'Polyfill', 'SelectorEngine' ] diff --git a/js/src/button.js b/js/src/button.js index 6453137e4e..78b0fea8cd 100644 --- a/js/src/button.js +++ b/js/src/button.js @@ -166,12 +166,18 @@ EventHandler.on(document, Event.CLICK_DATA_API, Selector.DATA_TOGGLE_CARROT, eve EventHandler.on(document, Event.FOCUS_DATA_API, Selector.DATA_TOGGLE_CARROT, event => { const button = SelectorEngine.closest(event.target, Selector.BUTTON) - button.classList.add(ClassName.FOCUS) + + 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) - button.classList.remove(ClassName.FOCUS) + + if (button) { + button.classList.remove(ClassName.FOCUS) + } }) /** diff --git a/js/src/dom/eventHandler.js b/js/src/dom/eventHandler.js index 1774650533..65c671facd 100644 --- a/js/src/dom/eventHandler.js +++ b/js/src/dom/eventHandler.js @@ -6,7 +6,7 @@ */ import { jQuery as $ } from '../util/index' -import Polyfill from './polyfill' +import { createCustomEvent, defaultPreventedPreservedOnDispatch } from './polyfill' /** * ------------------------------------------------------------------------ @@ -305,7 +305,7 @@ const EventHandler = { evt = document.createEvent('HTMLEvents') evt.initEvent(typeEvent, bubbles, true) } else { - evt = new CustomEvent(event, { + evt = createCustomEvent(event, { bubbles, cancelable: true }) @@ -326,7 +326,7 @@ const EventHandler = { if (defaultPrevented) { evt.preventDefault() - if (!Polyfill.defaultPreventedPreservedOnDispatch) { + if (!defaultPreventedPreservedOnDispatch) { Object.defineProperty(evt, 'defaultPrevented', { get: () => true }) diff --git a/js/src/dom/polyfill.js b/js/src/dom/polyfill.js index f6cd23bdb9..fd857cb38c 100644 --- a/js/src/dom/polyfill.js +++ b/js/src/dom/polyfill.js @@ -1,3 +1,5 @@ +/* istanbul ignore file */ + /** * -------------------------------------------------------------------------- * Bootstrap (v4.3.1): dom/polyfill.js @@ -7,83 +9,144 @@ import { getUID } from '../util/index' -/* istanbul ignore next */ -const Polyfill = (() => { - // MSEdge resets defaultPrevented flag upon dispatchEvent call if at least one listener is attached - const defaultPreventedPreservedOnDispatch = (() => { - const e = new CustomEvent('Bootstrap', { - cancelable: true - }) +let { matches, closest } = Element.prototype +let find = Element.prototype.querySelectorAll +let findOne = Element.prototype.querySelector +let createCustomEvent = (eventName, params) => { + const cEvent = new CustomEvent(eventName, params) - const element = document.createElement('div') - element.addEventListener('Bootstrap', () => null) + return cEvent +} - e.preventDefault() - element.dispatchEvent(e) - return e.defaultPrevented - })() +if (typeof window.CustomEvent !== 'function') { + createCustomEvent = (eventName, params) => { + params = params || { bubbles: false, cancelable: false, detail: null } - let find = Element.prototype.querySelectorAll - let findOne = Element.prototype.querySelector + const evt = document.createEvent('CustomEvent') - const scopeSelectorRegex = /:scope\b/ - const supportScopeQuery = (() => { - const element = document.createElement('div') - - try { - element.querySelectorAll(':scope *') - } catch (error) { - return false - } - - return true - })() - - if (!supportScopeQuery) { - find = function (selector) { - if (!scopeSelectorRegex.test(selector)) { - return this.querySelectorAll(selector) - } - - const hasId = Boolean(this.id) - - if (!hasId) { - this.id = getUID('scope') - } - - let nodeList = null - try { - selector = selector.replace(scopeSelectorRegex, `#${this.id}`) - nodeList = this.querySelectorAll(selector) - } finally { - if (!hasId) { - this.removeAttribute('id') - } - } - - return nodeList - } - - findOne = function (selector) { - if (!scopeSelectorRegex.test(selector)) { - return this.querySelector(selector) - } - - const matches = find.call(this, selector) - - if (typeof matches[0] !== 'undefined') { - return matches[0] - } - - return null - } + evt.initCustomEvent(eventName, params.bubbles, params.cancelable, params.detail) + return evt } +} - return { - defaultPreventedPreservedOnDispatch, - find, - findOne - } +const workingDefaultPrevented = (() => { + const e = document.createEvent('CustomEvent') + + e.initEvent('Bootstrap', true, true) + e.preventDefault() + return e.defaultPrevented })() -export default Polyfill +if (!workingDefaultPrevented) { + const origPreventDefault = Event.prototype.preventDefault + + Event.prototype.preventDefault = function () { + if (!this.cancelable) { + return + } + + origPreventDefault.call(this) + Object.defineProperty(this, 'defaultPrevented', { + get() { + return true + }, + configurable: true + }) + } +} + +// MSEdge resets defaultPrevented flag upon dispatchEvent call if at least one listener is attached +const defaultPreventedPreservedOnDispatch = (() => { + const e = createCustomEvent('Bootstrap', { + cancelable: true + }) + + const element = document.createElement('div') + element.addEventListener('Bootstrap', () => null) + + e.preventDefault() + element.dispatchEvent(e) + return e.defaultPrevented +})() + +if (!matches) { + matches = Element.prototype.msMatchesSelector || Element.prototype.webkitMatchesSelector +} + +if (!closest) { + closest = function (selector) { + let element = this + + do { + if (matches.call(element, selector)) { + return element + } + + element = element.parentElement || element.parentNode + } while (element !== null && element.nodeType === 1) + + return null + } +} + +const scopeSelectorRegex = /:scope\b/ +const supportScopeQuery = (() => { + const element = document.createElement('div') + + try { + element.querySelectorAll(':scope *') + } catch (error) { + return false + } + + return true +})() + +if (!supportScopeQuery) { + find = function (selector) { + if (!scopeSelectorRegex.test(selector)) { + return this.querySelectorAll(selector) + } + + const hasId = Boolean(this.id) + + if (!hasId) { + this.id = getUID('scope') + } + + let nodeList = null + try { + selector = selector.replace(scopeSelectorRegex, `#${this.id}`) + nodeList = this.querySelectorAll(selector) + } finally { + if (!hasId) { + this.removeAttribute('id') + } + } + + return nodeList + } + + findOne = function (selector) { + if (!scopeSelectorRegex.test(selector)) { + return this.querySelector(selector) + } + + const matches = find.call(this, selector) + + if (typeof matches[0] !== 'undefined') { + return matches[0] + } + + return null + } +} + +export { + createCustomEvent, + find, + findOne, + matches, + closest, + defaultPreventedPreservedOnDispatch +} diff --git a/js/src/dom/selectorEngine.js b/js/src/dom/selectorEngine.js index a54b18e581..fad3a43b54 100644 --- a/js/src/dom/selectorEngine.js +++ b/js/src/dom/selectorEngine.js @@ -5,7 +5,7 @@ * -------------------------------------------------------------------------- */ -import Polyfill from './polyfill' +import { find as findFn, findOne, matches, closest } from './polyfill' import { makeArray } from '../util/index' /** @@ -14,12 +14,11 @@ import { makeArray } from '../util/index' * ------------------------------------------------------------------------ */ -const { find: findFn, findOne } = Polyfill const NODE_TEXT = 3 const SelectorEngine = { matches(element, selector) { - return element.matches(selector) + return matches.call(element, selector) }, find(selector, element = document.documentElement) { @@ -72,7 +71,7 @@ const SelectorEngine = { return null } - return element.closest(selector) + return closest.call(element, selector) }, prev(element, selector) { diff --git a/js/src/util/index.js b/js/src/util/index.js index aea369558a..5788c8749e 100644 --- a/js/src/util/index.js +++ b/js/src/util/index.js @@ -71,7 +71,10 @@ const getTransitionDurationFromElement = element => { } const triggerTransitionEnd = element => { - element.dispatchEvent(new Event(TRANSITION_END)) + const evt = document.createEvent('HTMLEvents') + + evt.initEvent(TRANSITION_END, true, true) + element.dispatchEvent(evt) } const isElement = obj => (obj[0] || obj).nodeType diff --git a/js/tests/browsers.js b/js/tests/browsers.js index 859f9505cc..a0d43da864 100644 --- a/js/tests/browsers.js +++ b/js/tests/browsers.js @@ -30,6 +30,13 @@ const browsers = { browser: 'Edge', browser_version: 'latest' }, + ie11Win10: { + base: 'BrowserStack', + os: 'Windows', + os_version: '10', + browser: 'IE', + browser_version: '11.0' + }, chromeWin10: { base: 'BrowserStack', os: 'Windows', diff --git a/js/tests/karma.conf.js b/js/tests/karma.conf.js index 122d95753b..16f07cbf1a 100644 --- a/js/tests/karma.conf.js +++ b/js/tests/karma.conf.js @@ -79,8 +79,9 @@ if (bundle) { conf.detectBrowsers = detectBrowsers files = files.concat([ jqueryFile, + 'js/tests/unit/tests-polyfills.js', 'dist/js/bootstrap.js', - 'js/tests/unit/*.js' + 'js/tests/unit/!(tests-polyfills).js' ]) } else if (browserStack) { conf.hostname = ip.address() @@ -97,6 +98,7 @@ if (bundle) { reporters.push('BrowserStack') files = files.concat([ jqueryFile, + 'js/tests/unit/tests-polyfills.js', 'js/coverage/dist/util/util.js', 'js/coverage/dist/util/sanitizer.js', 'js/coverage/dist/dom/polyfill.js', @@ -107,7 +109,7 @@ if (bundle) { 'js/coverage/dist/dom/!(polyfill).js', 'js/coverage/dist/tooltip.js', 'js/coverage/dist/!(util|index|tooltip).js', // include all of our js/dist files except util.js, index.js and tooltip.js - 'js/tests/unit/*.js', + 'js/tests/unit/!(tests-polyfills).js', 'js/tests/unit/dom/*.js', 'js/tests/unit/util/*.js' ]) @@ -121,6 +123,7 @@ if (bundle) { ) files = files.concat([ jqueryFile, + 'js/tests/unit/tests-polyfills.js', 'js/coverage/dist/util/util.js', 'js/coverage/dist/util/sanitizer.js', 'js/coverage/dist/dom/polyfill.js', @@ -131,7 +134,7 @@ if (bundle) { 'js/coverage/dist/dom/!(polyfill).js', 'js/coverage/dist/tooltip.js', 'js/coverage/dist/!(util|index|tooltip).js', // include all of our js/dist files except util.js, index.js and tooltip.js - 'js/tests/unit/*.js', + 'js/tests/unit/!(tests-polyfills).js', 'js/tests/unit/dom/*.js', 'js/tests/unit/util/*.js' ]) diff --git a/js/tests/unit/modal.js b/js/tests/unit/modal.js index 87d778b868..82b37f2367 100644 --- a/js/tests/unit/modal.js +++ b/js/tests/unit/modal.js @@ -731,7 +731,14 @@ $(function () { }) QUnit.test('should enforce focus', function (assert) { - assert.expect(2) + var isIE11 = Boolean(window.MSInputMethodContext) && Boolean(document.documentMode) + + if (isIE11) { + assert.expect(1) + } else { + assert.expect(2) + } + var done = assert.async() var $modal = $([ @@ -759,14 +766,18 @@ $(function () { done() } - document.addEventListener('focusin', focusInListener) + if (isIE11) { + done() + } else { + document.addEventListener('focusin', focusInListener) - var focusInEvent = new Event('focusin') - Object.defineProperty(focusInEvent, 'target', { - value: $('#qunit-fixture')[0] - }) + var focusInEvent = new Event('focusin') + Object.defineProperty(focusInEvent, 'target', { + value: $('#qunit-fixture')[0] + }) - document.dispatchEvent(focusInEvent) + document.dispatchEvent(focusInEvent) + } }) .bootstrapModal('show') }) diff --git a/js/tests/unit/tests-polyfills.js b/js/tests/unit/tests-polyfills.js new file mode 100644 index 0000000000..4f2583e0db --- /dev/null +++ b/js/tests/unit/tests-polyfills.js @@ -0,0 +1,28 @@ +// Polyfills for our unit tests +(function () { + 'use strict' + + // Event constructor shim + if (!window.Event || typeof window.Event !== 'function') { + var origEvent = window.Event + window.Event = function (inType, params) { + params = params || {} + var e = document.createEvent('Event') + e.initEvent(inType, Boolean(params.bubbles), Boolean(params.cancelable)) + return e + } + + window.Event.prototype = origEvent.prototype + } + + if (typeof window.CustomEvent !== 'function') { + window.CustomEvent = function (event, params) { + params = params || { bubbles: false, cancelable: false, detail: null } + var evt = document.createEvent('CustomEvent') + evt.initCustomEvent(event, params.bubbles, params.cancelable, params.detail) + return evt + } + + CustomEvent.prototype = window.Event.prototype + } +})()