From 08679ac0b5f34e1a1f1766be460e51bc1aa8d82a Mon Sep 17 00:00:00 2001
From: Johann-S <johann.servoire@gmail.com>
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           | 179 +++++++++++++++++++++----------
 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, 200 insertions(+), 92 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')
+    evt.initCustomEvent(eventName, params.bubbles, params.cancelable, params.detail)
+    return evt
+  }
+}
 
-    try {
-      element.querySelectorAll(':scope *')
-    } catch (error) {
-      return false
+const workingDefaultPrevented = (() => {
+  const e = document.createEvent('CustomEvent')
+
+  e.initEvent('Bootstrap', true, true)
+  e.preventDefault()
+  return e.defaultPrevented
+})()
+
+if (!workingDefaultPrevented) {
+  const origPreventDefault = Event.prototype.preventDefault
+
+  Event.prototype.preventDefault = function () {
+    if (!this.cancelable) {
+      return
     }
 
-    return true
-  })()
+    origPreventDefault.call(this)
+    Object.defineProperty(this, 'defaultPrevented', {
+      get() {
+        return true
+      },
+      configurable: true
+    })
+  }
+}
 
-  if (!supportScopeQuery) {
-    find = function (selector) {
-      if (!scopeSelectorRegex.test(selector)) {
-        return this.querySelectorAll(selector)
-      }
+// MSEdge resets defaultPrevented flag upon dispatchEvent call if at least one listener is attached
+const defaultPreventedPreservedOnDispatch = (() => {
+  const e = createCustomEvent('Bootstrap', {
+    cancelable: true
+  })
 
-      const hasId = Boolean(this.id)
+  const element = document.createElement('div')
+  element.addEventListener('Bootstrap', () => null)
 
-      if (!hasId) {
-        this.id = getUID('scope')
-      }
+  e.preventDefault()
+  element.dispatchEvent(e)
+  return e.defaultPrevented
+})()
+
+if (!matches) {
+  matches = Element.prototype.msMatchesSelector || Element.prototype.webkitMatchesSelector
+}
 
-      let nodeList = null
-      try {
-        selector = selector.replace(scopeSelectorRegex, `#${this.id}`)
-        nodeList = this.querySelectorAll(selector)
-      } finally {
-        if (!hasId) {
-          this.removeAttribute('id')
-        }
+if (!closest) {
+  closest = function (selector) {
+    let element = this
+
+    do {
+      if (matches.call(element, selector)) {
+        return element
       }
 
-      return nodeList
+      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)
     }
 
-    findOne = function (selector) {
-      if (!scopeSelectorRegex.test(selector)) {
-        return this.querySelector(selector)
-      }
+    const hasId = Boolean(this.id)
 
-      const matches = find.call(this, selector)
+    if (!hasId) {
+      this.id = getUID('scope')
+    }
 
-      if (typeof matches[0] !== 'undefined') {
-        return matches[0]
+    let nodeList = null
+    try {
+      selector = selector.replace(scopeSelectorRegex, `#${this.id}`)
+      nodeList = this.querySelectorAll(selector)
+    } finally {
+      if (!hasId) {
+        this.removeAttribute('id')
       }
-
-      return null
     }
-  }
 
-  return {
-    defaultPreventedPreservedOnDispatch,
-    find,
-    findOne
+    return nodeList
   }
-})()
 
-export default Polyfill
+  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
+  }
+})()
-- 
GitLab