From 2e80974f42bf33334611ed339a94994b8ffff20f Mon Sep 17 00:00:00 2001
From: Mark Carver <mark.carver@me.com>
Date: Sun, 14 Jan 2018 04:15:31 -0600
Subject: [PATCH] Issue #2831237 by markcarver, fearlsgroove, tedbow: Bootstrap
 modal does not work well with jQuery UI dialog

Signed-off-by: Mark Carver <mark.carver@me.com>
---
 bootstrap.info.yml                                 |  10 +-
 bootstrap.libraries.yml                            |  29 +-
 js/attributes.js                                   | 234 ++++++--
 js/drupal.bootstrap.js                             | 423 ++++++++++----
 js/misc/dialog.ajax.js                             | 293 ++--------
 js/misc/dialog.js                                  | 104 ----
 js/modal.jquery.ui.bridge.js                       | 467 ++++++++++++++++
 js/modal.js                                        | 615 +++++++++++++++------
 js/theme-settings.js                               |   7 +-
 js/theme.js                                        |  24 +-
 src/Plugin/Alter/LibraryInfo.php                   |  12 +-
 .../Setting/JavaScript/Modals/ModalEnabled.php     |   1 -
 .../JavaScript/Modals/ModalJqueryUIBridge.php      |  55 ++
 13 files changed, 1557 insertions(+), 717 deletions(-)
 delete mode 100644 js/misc/dialog.js
 create mode 100644 js/modal.jquery.ui.bridge.js
 create mode 100644 src/Plugin/Setting/JavaScript/Modals/ModalJqueryUIBridge.php

diff --git a/bootstrap.info.yml b/bootstrap.info.yml
index 718dd5b..a977d18 100644
--- a/bootstrap.info.yml
+++ b/bootstrap.info.yml
@@ -27,6 +27,8 @@ libraries-extend:
     - bootstrap/drupal.ajax
   core/drupal.autocomplete:
     - bootstrap/drupal.autocomplete
+  core/drupal.dialog.ajax:
+    - bootstrap/drupal.dialog.ajax
   core/drupal.form:
     - bootstrap/drupal.form
   core/drupal.progress:
@@ -70,11 +72,3 @@ libraries-override:
     css:
       theme:
         css/node.preview.css: false
-
-# The following are dynamic library overrides based on certain critera,
-# usually a theme setting.
-#
-# @see \Drupal\bootstrap\Plugin\Alter\LibraryInfo::alter()
-#
-#  core/drupal.dialog: bootstrap/drupal.dialog
-#  core/drupal.dialog.ajax: bootstrap/drupal.dialog.ajax
diff --git a/bootstrap.libraries.yml b/bootstrap.libraries.yml
index b98efe0..716ac3b 100644
--- a/bootstrap.libraries.yml
+++ b/bootstrap.libraries.yml
@@ -48,6 +48,18 @@ modal:
   dependencies:
     - bootstrap/theme
 
+modal.jquery.ui.bridge:
+  js:
+    js/modal.jquery.ui.bridge.js: {}
+  dependencies:
+    - bootstrap/modal
+    - core/jquery.ui
+    - core/jquery.ui.widget
+    - core/jquery.ui.draggable
+    - core/jquery.ui.mouse
+    - core/jquery.ui.position
+    - core/jquery.ui.resizable
+
 popover:
   js:
     js/popover.js: {}
@@ -69,6 +81,10 @@ drupal.autocomplete:
   js:
     js/misc/autocomplete.js: {}
 
+drupal.dialog.ajax:
+  js:
+    js/misc/dialog.ajax.js: {}
+
 drupal.form:
   js:
     js/misc/form.js: {}
@@ -105,19 +121,6 @@ drupal.batch:
     - core/drupal.progress
     - core/jquery.once
 
-drupal.dialog:
-  js:
-    js/misc/dialog.js: {}
-  dependencies:
-    - bootstrap/modal
-
-drupal.dialog.ajax:
-  js:
-    js/misc/dialog.ajax.js: {}
-  dependencies:
-    - core/drupal.ajax
-    - bootstrap/drupal.dialog
-
 drupal.filter:
   version: VERSION
   js:
diff --git a/js/attributes.js b/js/attributes.js
index dd71c5d..52259e0 100644
--- a/js/attributes.js
+++ b/js/attributes.js
@@ -1,21 +1,24 @@
 (function ($, _) {
 
   /**
-   * Class to help modify attributes.
+   * @class Attributes
    *
-   * @param {object} object
-   *   An object to initialize attributes with.
+   * Modifies attributes.
    *
-   * @constructor
+   * @param {Object|Attributes} attributes
+   *   An object to initialize attributes with.
    */
-  var Attributes = function (object) {
-    this.data = object && _.isObject(object) && _.clone(object) || {};
+  var Attributes = function (attributes) {
+    this.data = {};
+    this.data['class'] = [];
+    this.merge(attributes);
   };
 
   /**
    * Renders the attributes object as a string to inject into an HTML element.
    *
-   * @returns {string}
+   * @return {String}
+   *   A rendered string suitable for inclusion in HTML markup.
    */
   Attributes.prototype.toString = function () {
     var output = '';
@@ -23,9 +26,10 @@
     var checkPlain = function (str) {
       return str && str.toString().replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/</g, '&lt;').replace(/>/g, '&gt;') || '';
     };
-    for (name in this.data) {
-      if (!this.data.hasOwnProperty(name)) continue;
-      value = this.data[name];
+    var data = this.getData();
+    for (name in data) {
+      if (!data.hasOwnProperty(name)) continue;
+      value = data[name];
       if (_.isFunction(value)) value = value();
       if (_.isObject(value)) value = _.values(value);
       if (_.isArray(value)) value = value.join(' ');
@@ -34,6 +38,27 @@
     return output;
   };
 
+  /**
+   * Renders the Attributes object as a plain object.
+   *
+   * @return {Object}
+   *   A plain object suitable for inclusion in DOM elements.
+   */
+  Attributes.prototype.toPlainObject = function () {
+    var object = {};
+    var name, value;
+    var data = this.getData();
+    for (name in data) {
+      if (!data.hasOwnProperty(name)) continue;
+      value = data[name];
+      if (_.isFunction(value)) value = value();
+      if (_.isObject(value)) value = _.values(value);
+      if (_.isArray(value)) value = value.join(' ');
+      object[name] = value;
+    }
+    return object;
+  };
+
   /**
    * Add class(es) to the array.
    *
@@ -45,9 +70,8 @@
    * @chainable
    */
   Attributes.prototype.addClass = function (value) {
-    var classes = this.getClasses();
-    value = [].concat(classes, value);
-    this.set('class', _.uniq(value));
+    var args = Array.prototype.slice.call(arguments);
+    this.data['class'] = this.sanitizeClasses(this.data['class'].concat(args));
     return this;
   };
 
@@ -83,10 +107,10 @@
   /**
    * Retrieves a cloned copy of the internal attributes data object.
    *
-   * @returns {Object}
+   * @return {Object}
    */
   Attributes.prototype.getData = function () {
-    return _.clone(this.data);
+    return _.extend({}, this.data);
   };
 
   /**
@@ -96,32 +120,37 @@
    *   The classes array.
    */
   Attributes.prototype.getClasses = function () {
-    var classes = [].concat(this.get('class', []));
-    return _.uniq(classes);
+    return this.get('class', []);
   };
 
   /**
    * Indicates whether a class is present in the array.
    *
-   * @param {string|Array} name
+   * @param {string|Array} className
    *   The class(es) to search for.
    *
    * @return {boolean}
    *   TRUE or FALSE
    */
-  Attributes.prototype.hasClass = function (name) {
-    name = [].concat(name);
+  Attributes.prototype.hasClass = function (className) {
+    className = this.sanitizeClasses(Array.prototype.slice.call(arguments));
     var classes = this.getClasses();
-    var found = false;
-    _.each(name, function (value) { if (_.indexOf(classes, value) !== -1) found = true; });
-    return found;
+    for (var i = 0, l = className.length; i < l; i++) {
+      // If one of the classes fails, immediately return false.
+      if (_.indexOf(classes, className[i]) === -1) {
+        return false;
+      }
+    }
+    return true;
   };
 
   /**
    * Merges multiple values into the array.
    *
-   * @param {object} values
-   *   An associative key/value array.
+   * @param {Attributes|Node|jQuery|Object} object
+   *   An Attributes object with existing data, a Node DOM element, a jQuery
+   *   instance or a plain object where the key is the attribute name and the
+   *   value is the attribute value.
    * @param {boolean} [recursive]
    *   Flag determining whether or not to recursively merge key/value pairs.
    *
@@ -129,14 +158,54 @@
    *
    * @chainable
    */
-  Attributes.prototype.merge = function (values, recursive) {
-    values = values instanceof Attributes ? values.getData() : values;
-    if (recursive === void(0) || recursive) {
-      this.data = $.extend(true, {}, this.data, values);
+  Attributes.prototype.merge = function (object, recursive) {
+    // Immediately return if there is nothing to merge.
+    if (!object) {
+      return this;
+    }
+
+    // Get attributes from a jQuery element.
+    if (object instanceof $) {
+      object = object[0];
+    }
+
+    // Get attributes from a DOM element.
+    if (object instanceof Node) {
+      object = Array.prototype.slice.call(object.attributes).reduce(function (attributes, attribute) {
+        attributes[attribute.name] = attribute.value;
+        return attributes;
+      }, {});
+    }
+    // Get attributes from an Attributes instance.
+    else if (object instanceof Attributes) {
+      object = object.getData();
+    }
+    // Otherwise, clone the object.
+    else {
+      object = _.extend({}, object);
+    }
+
+    // By this point, there should be a valid plain object.
+    if (!$.isPlainObject(object)) {
+      setTimeout(function () {
+        throw new Error('Passed object is not supported: ' + object);
+      });
+      return this;
+    }
+
+    // Handle classes separately.
+    if (object && object['class'] !== void 0) {
+      this.addClass(object['class']);
+      delete object['class'];
+    }
+
+    if (recursive === void 0 || recursive) {
+      this.data = $.extend(true, {}, this.data, object);
     }
     else {
-      $.extend(this.data, values);
+      this.data = $.extend({}, this.data, object);
     }
+
     return this;
   };
 
@@ -158,15 +227,16 @@
   /**
    * Removes a class from the attributes array.
    *
-   * @param {string|Array} value
+   * @param {...string|Array} className
    *   An individual class or an array of classes to remove.
    *
    * @return {Attributes}
    *
    * @chainable
    */
-  Attributes.prototype.removeClass = function (value) {
-    this.set('class', _.without(this.getClasses(), [].concat(value)));
+  Attributes.prototype.removeClass = function (className) {
+    var remove = this.sanitizeClasses(Array.prototype.slice.apply(arguments));
+    this.data['class'] = _.without(this.getClasses(), remove);
     return this;
   };
 
@@ -184,7 +254,7 @@
    */
   Attributes.prototype.replaceClass = function (oldValue, newValue) {
     var classes = this.getClasses();
-    var i = _.indexOf(oldValue, classes);
+    var i = _.indexOf(this.sanitizeClasses(oldValue), classes);
     if (i >= 0) {
       classes[i] = newValue;
       this.set('class', classes);
@@ -192,6 +262,41 @@
     return this;
   };
 
+  /**
+   * Ensures classes are flattened into a single is an array and sanitized.
+   *
+   * @param {...String|Array} classes
+   *   The class or classes to sanitize.
+   *
+   * @return {Array}
+   *   A sanitized array of classes.
+   */
+  Attributes.prototype.sanitizeClasses = function (classes) {
+    return _.chain(Array.prototype.slice.call(arguments))
+      // Flatten in case there's a mix of strings and arrays.
+      .flatten()
+
+      // Split classes that may have been added with a space as a separator.
+      .map(function (string) {
+        return string.split(' ');
+      })
+
+      // Flatten again since it was just split into arrays.
+      .flatten()
+
+      // Filter out empty items.
+      .filter()
+
+      // Clean the class to ensure it's a valid class name.
+      .map(Attributes.cleanClass)
+
+      // Ensure classes are unique.
+      .uniq()
+
+      // Retrieve the final value.
+      .value();
+  };
+
   /**
    * Sets an attribute on the array.
    *
@@ -205,24 +310,69 @@
    * @chainable
    */
   Attributes.prototype.set = function (name, value) {
-    this.data[name] = value;
-    return this;
+    var obj = $.isPlainObject(name) ? name : {};
+    if (typeof name === 'string') {
+      obj[name] = value;
+    }
+    return this.merge(obj);
+  };
+
+  /**
+   * Prepares a string for use as a CSS identifier (element, class, or ID name).
+   *
+   * Note: this is essentially a direct copy from
+   * \Drupal\Component\Utility\Html::cleanCssIdentifier
+   *
+   * @param {string} identifier
+   *   The identifier to clean.
+   * @param {Object} [filter]
+   *   An object of string replacements to use on the identifier.
+   *
+   * @return {string}
+   *   The cleaned identifier.
+   */
+  Attributes.cleanClass = function (identifier, filter) {
+    filter = filter || {
+      ' ': '-',
+      '_': '-',
+      '/': '-',
+      '[': '-',
+      ']': ''
+    };
+
+    identifier = identifier.toLowerCase();
+
+    if (filter['__'] === void 0) {
+      identifier = identifier.replace('__', '#DOUBLE_UNDERSCORE#', identifier);
+    }
+
+    identifier = identifier.replace(Object.keys(filter), Object.values(filter), identifier);
+
+    if (filter['__'] === void 0) {
+      identifier = identifier.replace('#DOUBLE_UNDERSCORE#', '__', identifier);
+    }
+
+    identifier = identifier.replace(/[^\u002D\u0030-\u0039\u0041-\u005A\u005F\u0061-\u007A\u00A1-\uFFFF]/u, '', identifier);
+    identifier = identifier.replace(['/^[0-9]/', '/^(-[0-9])|^(--)/'], ['_', '__'], identifier);
+
+    return identifier;
   };
 
   /**
    * Creates an Attributes instance.
    *
-   * @param {object|Attributes} object
+   * @param {object|Attributes} [attributes]
    *   An object to initialize attributes with.
    *
-   * @returns {Attributes}
-   *
-   * @global
+   * @return {Attributes}
+   *   An Attributes instance.
    *
    * @constructor
    */
-  window.Attributes = function (object) {
-    return object instanceof Attributes ? object : new Attributes(object);
+  Attributes.create = function (attributes) {
+    return new Attributes(attributes);
   };
 
+  window.Attributes = Attributes;
+
 })(window.jQuery, window._);
diff --git a/js/drupal.bootstrap.js b/js/drupal.bootstrap.js
index 332fcdd..07af6a3 100644
--- a/js/drupal.bootstrap.js
+++ b/js/drupal.bootstrap.js
@@ -8,10 +8,11 @@
  *
  * @namespace
  */
-(function ($, Drupal, drupalSettings) {
+(function (_, $, Drupal, drupalSettings) {
   'use strict';
 
-  Drupal.bootstrap = {
+  var Bootstrap = {
+    processedOnce: {},
     settings: drupalSettings.bootstrap || {}
   };
 
@@ -28,82 +29,325 @@
    *
    * @ingroup sanitization
    */
-  Drupal.bootstrap.checkPlain = function (str) {
+  Bootstrap.checkPlain = function (str) {
     return str && Drupal.checkPlain(str) || '';
   };
 
   /**
-   * Extends a Bootstrap plugin constructor.
+   * Creates a jQuery plugin.
    *
-   * @param {string} id
-   *   A Bootstrap plugin identifier located in $.fn.
-   * @param {function} [callback]
-   *   A callback to extend the plugin constructor.
+   * @param {String} id
+   *   A jQuery plugin identifier located in $.fn.
+   * @param {Function} plugin
+   *   A constructor function used to initialize the for the jQuery plugin.
+   * @param {Boolean} [noConflict]
+   *   Flag indicating whether or not to create a ".noConflict()" helper method
+   *   for the plugin.
+   */
+  Bootstrap.createPlugin = function (id, plugin, noConflict) {
+    // Immediately return if plugin doesn't exist.
+    if ($.fn[id] !== void 0) {
+      return this.fatal('Specified jQuery plugin identifier already exists: @id. Use Drupal.bootstrap.replacePlugin() instead.', {'@id': id});
+    }
+
+    // Immediately return if plugin isn't a function.
+    if (typeof plugin !== 'function') {
+      return this.fatal('You must provide a constructor function to create a jQuery plugin "@id": @plugin', {'@id': id, '@plugin':  plugin});
+    }
+
+    // Add a ".noConflict()" helper method.
+    this.pluginNoConflict(id, plugin, noConflict);
+
+    $.fn[id] = plugin;
+  };
+
+  /**
+   * Diff object properties.
+   *
+   * @param {...Object} objects
+   *   Two or more objects. The first object will be used to return properties
+   *   values.
    *
-   * @return {function|boolean}
-   *   The Bootstrap plugin or FALSE if the plugin does not exist.
+   * @return {Object}
+   *   Returns the properties of the first passed object that are not present
+   *   in all other passed objects.
    */
-  Drupal.bootstrap.extendPlugin = function (id, callback) {
-    // Immediately return if the plugin does not exist.
-    if (!$.fn[id] || !$.fn[id].Constructor) return false;
-
-    // Extend the plugin if a callback was provided.
-    if ($.isFunction(callback)) {
-      var ret = callback.apply($.fn[id].Constructor, [this.settings]);
-      if ($.isPlainObject(ret)) {
-        $.extend(true, $.fn[id].Constructor, ret);
-      }
+  Bootstrap.diffObjects = function (objects) {
+    var args = Array.prototype.slice.call(arguments);
+    return _.pick(args[0], _.difference.apply(_, _.map(args, function (obj) {
+      return Object.keys(obj);
+    })));
+  };
+
+  /**
+   * Map of supported events by regular expression.
+   *
+   * @type {Object<Event|MouseEvent|KeyboardEvent|TouchEvent,RegExp>}
+   */
+  Bootstrap.eventMap = {
+    Event: /^(?:load|unload|abort|error|select|change|submit|reset|focus|blur|resize|scroll)$/,
+    MouseEvent: /^(?:click|dblclick|mouse(?:down|enter|leave|up|over|move|out))$/,
+    KeyboardEvent: /^(?:key(?:down|press|up))$/,
+    TouchEvent: /^(?:touch(?:start|end|move|cancel))$/
+  };
+
+  /**
+   * Extends a jQuery Plugin.
+   *
+   * @param {String} id
+   *   A jQuery plugin identifier located in $.fn.
+   * @param {Function} callback
+   *   A constructor function used to initialize the for the jQuery plugin.
+   *
+   * @return {Function|Boolean}
+   *   The jQuery plugin constructor or FALSE if the plugin does not exist.
+   */
+  Bootstrap.extendPlugin = function (id, callback) {
+    // Immediately return if plugin doesn't exist.
+    if (typeof $.fn[id] !== 'function') {
+      return this.fatal('Specified jQuery plugin identifier does not exist: @id', {'@id':  id});
+    }
+
+    // Immediately return if callback isn't a function.
+    if (typeof callback !== 'function') {
+      return this.fatal('You must provide a callback function to extend the jQuery plugin "@id": @callback', {'@id': id, '@callback':  callback});
+    }
+
+    // Determine existing plugin constructor.
+    var constructor = $.fn[id] && $.fn[id].Constructor || $.fn[id];
+    var proto = constructor.prototype;
+
+    var obj = callback.apply(constructor, [this.settings]);
+    if (!$.isPlainObject(obj)) {
+      return this.fatal('Returned value from callback is not a plain object that can be used to extend the jQuery plugin "@id": @obj', {'@obj':  obj});
     }
 
     // Add a jQuery UI like option getter/setter method.
-    if ($.fn[id].Constructor.prototype.option === void(0)) {
-      $.fn[id].Constructor.prototype.option = this.option;
+    var option = this.option;
+    if (proto.option === void(0)) {
+      proto.option = function () {
+        return option.apply(this, arguments);
+      };
+    }
+
+    // Handle prototype properties separately.
+    if (obj.prototype !== void 0) {
+      for (var key in obj.prototype) {
+        if (!obj.prototype.hasOwnProperty(key)) continue;
+        var value = obj.prototype[key];
+        if (typeof value === 'function') {
+          proto[key] = this.superWrapper(proto[key] || function () {}, value);
+        }
+        else {
+          proto[key] = $.isPlainObject(value) ? $.extend(true, {}, proto[key], value) : value;
+        }
+      }
+    }
+    delete obj.prototype;
+
+    // Handle static properties.
+    for (key in obj) {
+      if (!obj.hasOwnProperty(key)) continue;
+      value = obj[key];
+      if (typeof value === 'function') {
+        constructor[key] = this.superWrapper(constructor[key] || function () {}, value);
+      }
+      else {
+        constructor[key] = $.isPlainObject(value) ? $.extend(true, {}, constructor[key], value) : value;
+      }
     }
 
-    return $.fn[id].Constructor;
+    return $.fn[id];
+  };
+
+  Bootstrap.superWrapper = function (parent, fn) {
+    return function () {
+      var previousSuper = this.super;
+      this.super = parent;
+      var ret = fn.apply(this, arguments);
+      if (previousSuper) {
+        this.super = previousSuper;
+      }
+      else {
+        delete this.super;
+      }
+      return ret;
+    };
   };
 
   /**
-   * Replaces a Bootstrap jQuery plugin definition.
+   * Provide a helper method for displaying when something is went wrong.
    *
-   * @param {string} id
-   *   A Bootstrap plugin identifier located in $.fn.
-   * @param {function} [callback]
-   *   A callback to replace the jQuery plugin definition. The callback must
-   *   return a function that is used to construct a jQuery plugin.
+   * @param {String} message
+   *   The message to display.
+   * @param {Object} [args]
+   *   An arguments to use in message.
    *
-   * @return {function|boolean}
-   *   The Bootstrap jQuery plugin definition or FALSE if the plugin does not
-   *   exist.
+   * @return {Boolean}
+   *   Always returns FALSE.
    */
-  Drupal.bootstrap.replacePlugin = function (id, callback) {
-    // Immediately return if plugin does not exist or not a valid callback.
-    if (!$.fn[id] || !$.fn[id].Constructor || !$.isFunction(callback)) return false;
-    var constructor = $.fn[id].Constructor;
+  Bootstrap.fatal = function (message, args) {
+    if (this.settings.dev && console.warn) {
+      for (var name in args) {
+        if (args.hasOwnProperty(name) && typeof args[name] === 'object') {
+          args[name] = JSON.stringify(args[name]);
+        }
+      }
+      Drupal.throwError(new Error(Drupal.formatString(message, args)));
+    }
+    return false;
+  };
+
+  /**
+   * Intersects object properties.
+   *
+   * @param {...Object} objects
+   *   Two or more objects. The first object will be used to return properties
+   *   values.
+   *
+   * @return {Object}
+   *   Returns the properties of first passed object that intersects with all
+   *   other passed objects.
+   */
+  Bootstrap.intersectObjects = function (objects) {
+    var args = Array.prototype.slice.call(arguments);
+    return _.pick(args[0], _.intersection.apply(_, _.map(args, function (obj) {
+      return Object.keys(obj);
+    })));
+  };
 
-    var plugin = callback.apply(constructor);
-    if ($.isFunction(plugin)) {
-      plugin.Constructor = constructor;
+  /**
+   * An object based once plugin (similar to jquery.once, but without the DOM).
+   *
+   * @param {String} id
+   *   A unique identifier.
+   * @param {Function} callback
+   *   The callback to invoke if the identifier has not yet been seen.
+   *
+   * @return {Bootstrap}
+   */
+  Bootstrap.once = function (id, callback) {
+    // Immediately return if identifier has already been processed.
+    if (this.processedOnce[id]) {
+      return this;
+    }
+    callback.call(this, this.settings);
+    this.processedOnce[id] = true;
+    return this;
+  };
 
+  /**
+   * Provide jQuery UI like ability to get/set options for Bootstrap plugins.
+   *
+   * @param {string|object} key
+   *   A string value of the option to set, can be dot like to a nested key.
+   *   An object of key/value pairs.
+   * @param {*} [value]
+   *   (optional) A value to set for key.
+   *
+   * @returns {*}
+   *   - Returns nothing if key is an object or both key and value parameters
+   *   were provided to set an option.
+   *   - Returns the a value for a specific setting if key was provided.
+   *   - Returns an object of key/value pairs of all the options if no key or
+   *   value parameter was provided.
+   *
+   * @see https://github.com/jquery/jquery-ui/blob/master/ui/widget.js
+   */
+  Bootstrap.option = function (key, value) {
+    var options = $.isPlainObject(key) ? $.extend({}, key) : {};
+
+    // Get all options (clone so it doesn't reference the internal object).
+    if (arguments.length === 0) {
+      return $.extend({}, this.options);
+    }
+
+    // Get/set single option.
+    if (typeof key === "string") {
+      // Handle nested keys in dot notation.
+      // e.g., "foo.bar" => { foo: { bar: true } }
+      var parts = key.split('.');
+      key = parts.shift();
+      var obj = options;
+      if (parts.length) {
+        for (var i = 0; i < parts.length - 1; i++) {
+          obj[parts[i]] = obj[parts[i]] || {};
+          obj = obj[parts[i]];
+        }
+        key = parts.pop();
+      }
+
+      // Get.
+      if (arguments.length === 1) {
+        return obj[key] === void 0 ? null : obj[key];
+      }
+
+      // Set.
+      obj[key] = value;
+    }
+
+    // Set multiple options.
+    $.extend(true, this.options, options);
+  };
+
+  /**
+   * Adds a ".noConflict()" helper method if needed.
+   *
+   * @param {String} id
+   *   A jQuery plugin identifier located in $.fn.
+   * @param {Function} plugin
+   * @param {Function} plugin
+   *   A constructor function used to initialize the for the jQuery plugin.
+   * @param {Boolean} [noConflict]
+   *   Flag indicating whether or not to create a ".noConflict()" helper method
+   *   for the plugin.
+   */
+  Bootstrap.pluginNoConflict = function (id, plugin, noConflict) {
+    if (plugin.noConflict === void 0 && (noConflict === void 0 || noConflict)) {
       var old = $.fn[id];
       plugin.noConflict = function () {
         $.fn[id] = old;
         return this;
       };
-      $.fn[id] = plugin;
     }
   };
 
   /**
-   * Map of supported events by regular expression.
+   * Replaces a Bootstrap jQuery plugin definition.
    *
-   * @type {Object<Event|MouseEvent|KeyboardEvent|TouchEvent,RegExp>}
+   * @param {String} id
+   *   A jQuery plugin identifier located in $.fn.
+   * @param {Function} callback
+   *   A callback function that is immediately invoked and must return a
+   *   function that will be used as the plugin constructor.
+   * @param {Boolean} [noConflict]
+   *   Flag indicating whether or not to create a ".noConflict()" helper method
+   *   for the plugin.
    */
-  Drupal.bootstrap.eventMap = {
-    Event: /^(?:load|unload|abort|error|select|change|submit|reset|focus|blur|resize|scroll)$/,
-    MouseEvent: /^(?:click|dblclick|mouse(?:down|enter|leave|up|over|move|out))$/,
-    KeyboardEvent: /^(?:key(?:down|press|up))$/,
-    TouchEvent: /^(?:touch(?:start|end|move|cancel))$/
+  Bootstrap.replacePlugin = function (id, callback, noConflict) {
+    // Immediately return if plugin doesn't exist.
+    if (typeof $.fn[id] !== 'function') {
+      return this.fatal('Specified jQuery plugin identifier does not exist: @id', {'@id':  id});
+    }
+
+    // Immediately return if callback isn't a function.
+    if (typeof callback !== 'function') {
+      return this.fatal('You must provide a valid callback function to replace a jQuery plugin: @callback', {'@callback': callback});
+    }
+
+    // Determine existing plugin constructor.
+    var constructor = $.fn[id] && $.fn[id].Constructor || $.fn[id];
+    var plugin = callback.apply(constructor, [this.settings]);
+
+    // Immediately return if plugin isn't a function.
+    if (typeof plugin !== 'function') {
+      return this.fatal('Returned value from callback is not a usable function to replace a jQuery plugin "@id": @plugin', {'@id': id, '@plugin': plugin});
+    }
+
+    // Add a ".noConflict()" helper method.
+    this.pluginNoConflict(id, plugin, noConflict);
+
+    $.fn[id] = plugin;
   };
 
   /**
@@ -125,7 +369,7 @@
    *   object here. This allows, if the browser supports it, to be a truly
    *   simulated event.
    */
-  Drupal.bootstrap.simulate = function (element, type, options) {
+  Bootstrap.simulate = function (element, type, options) {
     // Defer to the jQuery.simulate plugin, if it's available.
     if (typeof $.simulate === 'function') {
       new $.simulate(element, type, options);
@@ -133,8 +377,8 @@
     }
     var event;
     var ctor;
-    for (var name in Drupal.bootstrap.eventMap) {
-      if (Drupal.bootstrap.eventMap[name].test(type)) {
+    for (var name in this.eventMap) {
+      if (this.eventMap[name].test(type)) {
         ctor = name;
         break;
       }
@@ -171,67 +415,30 @@
   };
 
   /**
-   * Provide jQuery UI like ability to get/set options for Bootstrap plugins.
+   * Provide a helper method for displaying when something is unsupported.
    *
-   * @param {string|object} key
-   *   A string value of the option to set, can be dot like to a nested key.
-   *   An object of key/value pairs.
+   * @param {String} type
+   *   The type of unsupported object, e.g. method or option.
+   * @param {String} name
+   *   The name of the unsupported object.
    * @param {*} [value]
-   *   (optional) A value to set for key.
-   *
-   * @returns {*}
-   *   - Returns nothing if key is an object or both key and value parameters
-   *   were provided to set an option.
-   *   - Returns the a value for a specific setting if key was provided.
-   *   - Returns an object of key/value pairs of all the options if no key or
-   *   value parameter was provided.
-   *
-   * @see https://github.com/jquery/jquery-ui/blob/master/ui/widget.js
-   *
-   * @todo This isn't fully working since Bootstrap plugins don't allow
-   * methods to return values.
+   *   The value of the unsupported object.
    */
-  Drupal.bootstrap.option = function (key, value) {
-    var options = key;
-    var parts, curOption, i;
-
-    // Don't return a reference to the internal hash.
-    if (arguments.length === 0) {
-      return $.extend({}, this.options);
+  Bootstrap.unsupported = function (type, name, value) {
+    if (this.settings.dev && console.warn) {
+      console.warn(Drupal.formatString('Unsupported Drupal Bootstrap Modal @type: @name -> @value', {
+        '@type': type,
+        '@name': name,
+        '@value': typeof value === 'object' ? JSON.stringify(value) : value
+      }));
     }
-
-    // Handle a specific option.
-    if (typeof key === "string") {
-      // Handle nested keys, e.g., "foo.bar" => { foo: { bar: ___ } }
-      options = {};
-      parts = key.split(".");
-      key = parts.shift();
-      if (parts.length) {
-        curOption = options[key] = $.extend({}, this.options[key]);
-        for (i = 0; i < parts.length - 1; i++) {
-          curOption[parts[i]] = curOption[parts[i]] || {};
-          curOption = curOption[parts[i]];
-        }
-        key = parts.pop();
-        if (arguments.length === 1) {
-          return curOption[key] === undefined ? null : curOption[key];
-        }
-        curOption[key] = value;
-      }
-      else {
-        if (arguments.length === 1) {
-          return this.options[key] === undefined ? null : this.options[key];
-        }
-        options[key] = value;
-      }
-    }
-
-    // Set the new option(s).
-    for (key in options) {
-      if (!options.hasOwnProperty(key)) continue;
-      this.options[key] = options[key];
-    }
-    return this;
   };
 
-})(window.jQuery, window.Drupal, window.drupalSettings);
+  /**
+   * Add Bootstrap to the global Drupal object.
+   *
+   * @type {Bootstrap}
+   */
+  Drupal.bootstrap = Drupal.bootstrap || Bootstrap;
+
+})(window._, window.jQuery, window.Drupal, window.drupalSettings);
diff --git a/js/misc/dialog.ajax.js b/js/misc/dialog.ajax.js
index b12c7e9..f5c853c 100644
--- a/js/misc/dialog.ajax.js
+++ b/js/misc/dialog.ajax.js
@@ -1,263 +1,62 @@
 /**
  * @file
- * Extends the Drupal AJAX functionality to integrate the dialog API.
+ * dialog.ajax.js
  */
-
 (function ($, Drupal) {
-  'use strict';
 
-  /**
-   * Initialize dialogs for Ajax purposes.
-   *
-   * @type {Drupal~behavior}
-   */
-  Drupal.behaviors.dialog = {
-    attach: function (context, settings) {
-      var $context = $(context);
+  var dialogAjaxCurrentButton;
+  var dialogAjaxOriginalButton;
 
-      // Provide a known 'drupal-modal' DOM element for Drupal-based modal
-      // dialogs. Non-modal dialogs are responsible for creating their own
-      // elements, since there can be multiple non-modal dialogs at a time.
-      if (!$('#drupal-modal').length) {
-        $(Drupal.theme.bootstrapModal()).appendTo('body');
+  $(document)
+    .ajaxSend(function () {
+      if (dialogAjaxCurrentButton && dialogAjaxOriginalButton) {
+        dialogAjaxCurrentButton.html(dialogAjaxOriginalButton.html());
+        dialogAjaxCurrentButton.prop('disabled', dialogAjaxOriginalButton.prop('disabled'));
       }
-
-      // Special behaviors specific when attaching content within a dialog.
-      // These behaviors usually fire after a validation error inside a dialog.
-      var $dialog = $context.closest('.modal');
-      if ($dialog.length) {
-        // Remove and replace the dialog buttons with those from the new form.
-        if ($dialog.modal('option', 'drupalAutoButtons')) {
-          // Trigger an event to detect/sync changes to buttons.
-          $dialog.trigger('dialogButtonsChange');
-        }
-
-        // Attempt to force focus on the first visible input (not :input) in
-        // the modal body when the behavior is run.
-        Drupal.behaviors.dialog.focus($dialog);
+    })
+    .ajaxComplete(function () {
+      if (dialogAjaxCurrentButton && dialogAjaxOriginalButton) {
+        dialogAjaxCurrentButton.html(dialogAjaxOriginalButton.html());
+        dialogAjaxCurrentButton.prop('disabled', dialogAjaxOriginalButton.prop('disabled'));
       }
+      dialogAjaxCurrentButton = null;
+      dialogAjaxOriginalButton = null;
+    })
+  ;
 
-      var originalClose = settings.dialog.close;
-      // Overwrite the close method to remove the dialog on closing.
-      settings.dialog.close = function (event) {
-        originalClose.apply(settings.dialog, arguments);
-        $(event.target).remove();
-      };
-    },
-
-    /**
-     * Attempt to focus the first visible input (not :input) in the modal body.
-     *
-     * @param {jQuery} $dialog
-     *   An jQuery object containing the element that is the dialog target.
-     */
-    focus: function ($dialog) {
-      $dialog.find('.modal-body input:visible:first').trigger('focus');
-    },
-
-    /**
-     * Scan a dialog for any primary buttons and move them to the button area.
-     *
-     * @param {jQuery} $dialog
-     *   An jQuery object containing the element that is the dialog target.
-     *
-     * @return {Array}
-     *   An array of buttons that need to be added to the button area.
-     */
-    prepareDialogButtons: function ($dialog) {
-      var buttons = [];
-      var $buttons = $dialog.find('.form-actions :input[type=submit]');
-      $buttons.each(function () {
-        // Hidden form buttons need special attention. For browser consistency,
-        // the button needs to be "visible" in order to have the enter key fire
-        // the form submit event. So instead of a simple "hide" or
-        // "display: none", we set its dimensions to zero.
-        // See http://mattsnider.com/how-forms-submit-when-pressing-enter/
-        var $originalButton = $(this).css({
-          display: 'block',
-          width: 0,
-          height: 0,
-          padding: 0,
-          border: 0
-        });
-        buttons.push({
-          text: $originalButton.html() || $originalButton.attr('value'),
-          class: $originalButton.attr('class'),
-          click: function (e) {
+  /**
+   * {@inheritdoc}
+   */
+  Drupal.behaviors.dialog.prepareDialogButtons = function prepareDialogButtons($dialog) {
+    var buttons = [];
+    var $buttons = $dialog.find('.form-actions').find('button, input[type=submit], .form-actions a.button');
+    $buttons.each(function () {
+      var $originalButton = $(this).css({
+        display: 'block',
+        width: 0,
+        height: 0,
+        padding: 0,
+        border: 0,
+        overflow: 'hidden'
+      });
+      var classes = $originalButton.attr('class').replace('use-ajax-submit', '');
+      buttons.push({
+        text: $originalButton.html() || $originalButton.attr('value'),
+        class: classes,
+        click: function click(e) {
+          dialogAjaxCurrentButton = $(e.target);
+          dialogAjaxOriginalButton = $originalButton;
+          if ($originalButton.is('a')) {
+            $originalButton[0].click();
+          }
+          else {
             $originalButton.trigger('mousedown').trigger('mouseup').trigger('click');
             e.preventDefault();
           }
-        });
+        }
       });
-      return buttons;
-    }
-  };
-
-  /**
-   * Command to open a dialog.
-   *
-   * @param {Drupal.Ajax} ajax
-   * @param {object} response
-   * @param {number} [status]
-   *
-   * @return {bool|undefined}
-   */
-  Drupal.AjaxCommands.prototype.openDialog = function (ajax, response, status) {
-    if (!response.selector) {
-      return false;
-    }
-
-    var $dialog = $(response.selector);
-    if (!$dialog.length) {
-      // Create the element if needed.
-      $dialog = $(Drupal.theme.bootstrapModal({ id: response.selector.replace(/^#/, '') })).appendTo('body');
-    }
-    // Set up the wrapper, if there isn't one.
-    var id = $dialog.attr('id');
-    if (response.selector === '#' + id) {
-      response.selector = '#' + id + '--body';
-    }
-    else if (!ajax.wrapper) {
-      ajax.wrapper = id + '--body';
-    }
-
-    // Use the ajax.js insert command to populate the dialog contents.
-    response.command = 'insert';
-    response.method = 'html';
-    ajax.commands.insert(ajax, response, status);
-
-    // Move the buttons to the jQuery UI dialog buttons area.
-    if (!response.dialogOptions.buttons) {
-      response.dialogOptions.drupalAutoButtons = true;
-      response.dialogOptions.buttons = Drupal.behaviors.dialog.prepareDialogButtons($dialog);
-    }
-
-    // Bind dialogButtonsChange.
-    $dialog.on('dialogButtonsChange', function () {
-      var buttons = Drupal.behaviors.dialog.prepareDialogButtons($dialog);
-      $dialog.modal('option', 'buttons', buttons);
     });
-
-    // Open the dialog itself.
-    response.dialogOptions = response.dialogOptions || {};
-    var dialog = Drupal.dialog($dialog.get(0), response.dialogOptions);
-    if (response.dialogOptions.modal) {
-      dialog.showModal();
-    }
-    else {
-      dialog.show();
-    }
-
-    // Add the standard Drupal class for buttons for style consistency.
-    $dialog.parent().find('.ui-dialog-buttonset').addClass('form-actions');
+    return buttons;
   };
 
-  /**
-   * Command to close a dialog.
-   *
-   * If no selector is given, it defaults to trying to close the modal.
-   *
-   * @param {Drupal.Ajax} [ajax]
-   * @param {object} response
-   * @param {string} response.selector
-   * @param {bool} response.persist
-   * @param {number} [status]
-   */
-  Drupal.AjaxCommands.prototype.closeDialog = function (ajax, response, status) {
-    var $dialog = $(response.selector);
-    if ($dialog.length) {
-      Drupal.dialog($dialog.get(0)).close();
-      if (!response.persist) {
-        // Wrap this in a timer so animations can finish.
-        setTimeout(function() {
-          $dialog.remove();
-        }, 1000);
-      }
-    }
-
-    // Unbind dialogButtonsChange.
-    $dialog.off('dialogButtonsChange');
-  };
-
-  /**
-   * Command to set a dialog property.
-   *
-   * JQuery UI specific way of setting dialog options.
-   *
-   * @param {Drupal.Ajax} [ajax]
-   * @param {object} response
-   * @param {string} response.selector
-   * @param {string} response.optionsName
-   * @param {string} response.optionValue
-   * @param {number} [status]
-   */
-  Drupal.AjaxCommands.prototype.setDialogOption = function (ajax, response, status) {
-    var $dialog = $(response.selector);
-    if ($dialog.length) {
-      $dialog.modal('option', response.optionName, response.optionValue);
-    }
-  };
-
-  /**
-   * Binds a listener on dialog before creation to setup title and buttons.
-   *
-   * @param {jQuery.Event} e
-   * @param {Drupal.dialog} dialog
-   * @param {jQuery} $element
-   * @param {object} settings
-   */
-  $(window).on('dialog:beforecreate', function (e, dialog, $element, settings) {
-    // Replace title.
-    if (settings.title) {
-      var $header = $element.find('.modal-header');
-      if (!$header[0]) {
-        $header = $(Drupal.theme.bootstrapModalHeader()).prependTo($element.find('.modal-content'));
-      }
-      $header.find('.modal-title').text(Drupal.checkPlain(settings.title));
-    }
-
-    // Remove any existing buttons.
-    $element.find('.modal-footer').remove();
-
-    // Add new buttons.
-    if (settings.buttons && settings.buttons.length) {
-      var $footer = $(Drupal.theme.bootstrapModalFooter('', true)).appendTo($element.find('.modal-content'));
-      for (var i in settings.buttons) {
-        if (!settings.buttons.hasOwnProperty(i)) continue;
-        var button = settings.buttons[i];
-        $('<button class="' + button.class + '">' + button.text + '</button>')
-          .appendTo($footer)
-          .on('click', button.click);
-      }
-    }
-  });
-
-  /**
-   * Binds a listener on dialog creation to handle the cancel link.
-   *
-   * @param {jQuery.Event} e
-   * @param {Drupal.dialog} dialog
-   * @param {jQuery} $element
-   * @param {object} settings
-   */
-  $(window).on('dialog:aftercreate', function (e, dialog, $element, settings) {
-    Drupal.behaviors.dialog.focus($element);
-    $element.on('click.dialog', '.dialog-cancel', function (e) {
-      dialog.close('cancel');
-      e.preventDefault();
-      e.stopPropagation();
-    });
-  });
-
-  /**
-   * Removes all 'dialog' listeners.
-   *
-   * @param {jQuery.Event} e
-   * @param {Drupal.dialog} dialog
-   * @param {jQuery} $element
-   */
-  $(window).on('dialog:beforeclose', function (e, dialog, $element) {
-    $element.off('.modal.drupal');
-  });
-
-})(jQuery, Drupal);
+})(window.jQuery, window.Drupal, window.Drupal.bootstrap);
diff --git a/js/misc/dialog.js b/js/misc/dialog.js
deleted file mode 100644
index 5a92e82..0000000
--- a/js/misc/dialog.js
+++ /dev/null
@@ -1,104 +0,0 @@
-/**
- * @file
- * Dialog API inspired by HTML5 dialog element.
- *
- * @see http://www.whatwg.org/specs/web-apps/current-work/multipage/commands.html#the-dialog-element
- */
-
-(function ($, Drupal, drupalSettings) {
-
-  'use strict';
-
-  /**
-   * Default dialog options.
-   *
-   * @type {object}
-   *
-   * @prop {bool} [autoOpen=true]
-   * @prop {string} [dialogClass='']
-   * @prop {string} [buttonClass='button']
-   * @prop {string} [buttonPrimaryClass='button--primary']
-   * @prop {function} close
-   */
-  drupalSettings.dialog = {
-    autoOpen: true,
-    dialogClass: '',
-    // Drupal-specific extensions: see dialog.jquery-ui.js.
-    buttonClass: 'button',
-    buttonPrimaryClass: 'button--primary',
-    // When using this API directly (when generating dialogs on the client
-    // side), you may want to override this method and do
-    // `jQuery(event.target).remove()` as well, to remove the dialog on
-    // closing.
-    close: function (event) {
-      Drupal.detachBehaviors(event.target, null, 'unload');
-    }
-  };
-
-  /**
-   * @typedef {object} Drupal.dialog~dialogDefinition
-   *
-   * @prop {boolean} open
-   *   Is the dialog open or not.
-   * @prop {*} returnValue
-   *   Return value of the dialog.
-   * @prop {function} show
-   *   Method to display the dialog on the page.
-   * @prop {function} showModal
-   *   Method to display the dialog as a modal on the page.
-   * @prop {function} close
-   *   Method to hide the dialog from the page.
-   */
-
-  /**
-   * Polyfill HTML5 dialog element with jQueryUI.
-   *
-   * @param {HTMLElement} element
-   * @param {object} options
-   *   jQuery UI options to be passed to the dialog.
-   *
-   * @return {Drupal.dialog~dialogDefinition}
-   */
-  Drupal.dialog = function (element, options) {
-    var $element = $(element);
-
-    function openDialog(settings) {
-      settings = $.extend({}, drupalSettings.dialog, options, settings);
-      // Trigger a global event to allow scripts to bind events to the dialog.
-      $(window).trigger('dialog:beforecreate', [dialog, $element, settings]);
-      $element
-        .modal(settings)
-        .on('shown.bs.modal.drupal', function () {
-          dialog.open = true;
-          $(window).trigger('dialog:aftercreate', [dialog, $element, settings]);
-        })
-      ;
-    }
-
-    function closeDialog(value) {
-      $(window).trigger('dialog:beforeclose', [dialog, $element]);
-      $element
-        .on('hidden.bs.modal.drupal', function () {
-          dialog.returnValue = value;
-          dialog.open = false;
-          $(window).trigger('dialog:afterclose', [dialog, $element]);
-        })
-        .modal('hide');
-    }
-
-    var dialog = {
-      open: false,
-      returnValue: void(0),
-      show: function () {
-        openDialog({show: false});
-      },
-      showModal: function () {
-        openDialog({show: true});
-      },
-      close: closeDialog
-    };
-
-    return dialog;
-  };
-
-})(jQuery, Drupal, drupalSettings);
diff --git a/js/modal.jquery.ui.bridge.js b/js/modal.jquery.ui.bridge.js
new file mode 100644
index 0000000..70d14f7
--- /dev/null
+++ b/js/modal.jquery.ui.bridge.js
@@ -0,0 +1,467 @@
+/**
+ * @file
+ * Bootstrap Modals.
+ */
+(function ($, Drupal, Bootstrap, Attributes, drupalSettings) {
+  'use strict';
+
+  /**
+   * Only process this once.
+   */
+  Bootstrap.once('modal.jquery.ui.bridge', function (settings) {
+    // RTL support.
+    var rtl = document.documentElement.getAttribute('dir').toLowerCase() === 'rtl';
+
+    // Override drupal.dialog button classes. This must be done on DOM ready
+    // since core/drupal.dialog technically depends on this file and has not yet
+    // set their default settings.
+    $(function () {
+      drupalSettings.dialog.buttonClass = 'btn';
+      drupalSettings.dialog.buttonPrimaryClass = 'btn-primary';
+    });
+
+    var relayEvent = function ($element, name, stopPropagation) {
+      return function (e) {
+        if (stopPropagation === void 0 || stopPropagation) {
+          e.stopPropagation();
+        }
+        var parts = name.split('.').filter(Boolean);
+        var type = parts.shift();
+        e.target = $element[0];
+        e.currentTarget = $element[0];
+        e.namespace = parts.join('.');
+        e.type = type;
+        $element.trigger(e);
+      };
+    };
+
+    /**
+     * Proxy $.fn.dialog to $.fn.modal.
+     */
+    Bootstrap.createPlugin('dialog', function (options) {
+      // When only options are passed, jQuery UI dialog treats this like a
+      // initialization method. Destroy any existing Bootstrap modal and
+      // recreate it using the contents of the dialog HTML.
+      if (arguments.length === 1 && typeof options === 'object') {
+        this.each(function () {
+          // This part gets a little tricky. Core can potentially already
+          // semi-process this "dialog" if was created using an Ajax command
+          // (i.e. prepareDialogButtons in drupal.ajax.js). Because of this,
+          // we cannot simply dump the existing dialog content into a newly
+          // created modal because that would destroy any existing event bindings.
+          // Instead, we have to create this in steps and "move" (append) the
+          // existing content as needed.
+          var $this = $(this);
+
+          // Create a new modal to get a complete template.
+          var $modal = $(Drupal.theme('bootstrapModal', {attributes: Attributes.create(this).remove('style')}));
+
+          // Save a reference to the inner HTML of the existing dialog.
+          var $existing = $this.children();
+
+          // Destroy any existing Bootstrap Modal data that may have been saved.
+          $this.removeData('bs.modal');
+
+          // Set the attributes of the dialog to that of the newly created modal.
+          $this.attr(Attributes.create($modal).toPlainObject());
+
+          // Append the newly created modal markup.
+          $this.append($modal.html());
+
+          // Move the existing HTML into the modal markup that was just appended.
+          $this.find('.modal-body').append($existing);
+        });
+
+        // Indicate that the modal is a jQuery UI dialog bridge.
+        options.jQueryUiBridge = true;
+
+        // Proxy just the options to the Bootstrap Modal plugin.
+        return $.fn.modal.apply(this, [options]);
+      }
+
+      // Otherwise, proxy all arguments to the Bootstrap Modal plugin.
+      return $.fn.modal.apply(this, arguments);
+    });
+
+    /**
+     * Extend the Bootstrap Modal plugin constructor class.
+     */
+    Bootstrap.extendPlugin('modal', function () {
+      return {
+        DEFAULTS: {
+          // By default, this option is disabled. It's only flagged when a modal
+          // was created using $.fn.dialog above.
+          jQueryUiBridge: false
+        },
+        prototype: {
+
+          /**
+           * Handler for $.fn.dialog('close').
+           */
+          close: function () {
+            this.hide.apply(this, arguments);
+          },
+
+          /**
+           * Creates any necessary buttons from dialog options.
+           */
+          createButtons: function () {
+            this.$footer.find('.modal-buttons').remove();
+            var buttons = this.options.dialogOptions.buttons || [];
+            if (buttons.length) {
+              var $buttons = $('<div class="modal-buttons"/>').appendTo(this.$footer);
+              for (var i = 0, l = buttons.length; i < l; i++) {
+                var button = buttons[i];
+                var $button = $(Drupal.theme('bootstrapModalDialogButton', button));
+                if (typeof button.click === 'function') {
+                  $button.on('click', button.click);
+                }
+                $buttons.append($button);
+              }
+            }
+          },
+
+          /**
+           * Initializes the Bootstrap Modal.
+           */
+          init: function () {
+            // Relay necessary events.
+            if (this.options.jQueryUiBridge) {
+              this.$element.on('hide.bs.modal',   relayEvent(this.$element, 'dialogbeforeclose', false));
+              this.$element.on('hidden.bs.modal', relayEvent(this.$element, 'dialogclose', false));
+              this.$element.on('show.bs.modal',   relayEvent(this.$element, 'dialogcreate', false));
+              this.$element.on('shown.bs.modal',  relayEvent(this.$element, 'dialogopen', false));
+            }
+
+            // Create a footer if one doesn't exist.
+            // This is necessary in case dialog.ajax.js decides to add buttons.
+            if (!this.$footer[0]) {
+              this.$footer = $(Drupal.theme('bootstrapModalFooter', {}, true)).insertAfter(this.$dialogBody);
+            }
+
+            // Create buttons.
+            this.createButtons();
+
+            // Hide the footer if there are no children.
+            if (!this.$footer.children()[0]) {
+              this.$footer.hide();
+            }
+
+            // Now call the parent init method.
+            this.super();
+
+            // Handle autoResize option (this is a drupal.dialog option).
+            if (this.options.dialogOptions.autoResize && this.options.dialogOptions.position) {
+              this.position(this.options.dialogOptions.position);
+            }
+
+            // If show is enabled and currently not shown, show it.
+            if (this.options.show && !this.isShown) {
+              this.show();
+            }
+          },
+
+          /**
+           * Handler for $.fn.dialog('instance').
+           */
+          instance: function () {
+            Bootstrap.unsupported('method', 'instance', arguments);
+          },
+
+          /**
+           * Handler for $.fn.dialog('isOpen').
+           */
+          isOpen: function () {
+            return !!this.isShown;
+          },
+
+          /**
+           * Maps dialog options to the modal.
+           *
+           * @param {Object} options
+           *   The options to map.
+           */
+          mapDialogOptions: function (options) {
+            var $content = this.$dialog.find('.modal-content');
+            var $body = $content.find('.modal-body');
+            var dialogOptions = {};
+            var mappedOptions = {};
+
+            // Handle CSS properties.
+            var cssUnitRegExp = /^([+-]?(?:\d+|\d*\.\d+))([a-z]*|%)?$/;
+            var parseCssUnit = function (value, defaultUnit) {
+              var parts = ('' + value).match(cssUnitRegExp);
+              return parts && parts[1] !== void 0 ? parts[1] + (parts[2] || defaultUnit || 'px') : null;
+            };
+            var styles = {};
+            var cssProperties = ['height', 'maxHeight', 'maxWidth', 'minHeight', 'minWidth', 'width'];
+            for (var i = 0, l = cssProperties.length; i < l; i++) {
+              var prop = cssProperties[i];
+              if (options[prop] !== void 0) {
+                var value = parseCssUnit(options[prop]);
+                if (value) {
+                  dialogOptions[prop] = value;
+                  styles[prop] = value;
+                }
+              }
+            }
+
+            // Apply mapped CSS styles to the modal-body container.
+            $body.css(styles);
+
+            // Handle deprecated "dialogClass" option by merging it with "classes".
+            var classesMap = {
+              'ui-dialog': 'modal-content',
+              'ui-dialog-titlebar': 'modal-header',
+              'ui-dialog-title': 'modal-title',
+              'ui-dialog-titlebar-close': 'close',
+              'ui-dialog-content': 'modal-body',
+              'ui-dialog-buttonpane': 'modal-footer'
+            };
+            if (options.dialogClass) {
+              if (options.classes === void 0) {
+                options.classes = {};
+              }
+              if (options.classes['ui-dialog'] === void 0) {
+                options.classes['ui-dialog'] = '';
+              }
+              var dialogClass = options.classes['ui-dialog'].split(' ');
+              dialogClass.push(options.dialogClass);
+              options.classes['ui-dialog'] = dialogClass.join(' ');
+              delete options.dialogClass;
+            }
+
+
+            // Bind events.
+            var events = [
+              'beforeClose', 'close',
+              'create',
+              'drag', 'dragStart', 'dragStop',
+              'focus',
+              'open',
+              'resize', 'resizeStart', 'resizeStop'
+            ];
+            for (i = 0, l = events.length; i < l; i++) {
+              var event = events[i].toLowerCase();
+              if (options[event] === void 0 || typeof options[event] !== 'function') continue;
+              this.$element.on('dialog' + event, options[event]);
+            }
+
+            // Handle the reset of the options.
+            for (var name in options) {
+              if (!options.hasOwnProperty(name) || options[name] === void 0) continue;
+
+              switch (name) {
+                case 'appendTo':
+                  Bootstrap.unsupported('option', name, options.appendTo);
+                  break;
+
+                case 'autoOpen':
+                  mappedOptions.show = !!options.autoOpen;
+                  break;
+
+                // This is really a drupal.dialog option, not jQuery UI.
+                case 'autoResize':
+                  dialogOptions.autoResize = !!options.autoResize;
+                  break;
+
+                case 'buttons':
+                  dialogOptions.buttons = options.buttons;
+                  break;
+
+                case 'classes':
+                  dialogOptions.classes = options.classes;
+                  for (var key in options.classes) {
+                    if (options.classes.hasOwnProperty(key) && classesMap[key] !== void 0) {
+                      // Run through Attributes to sanitize classes.
+                      var attributes = Attributes.create().addClass(options.classes[key]).toPlainObject();
+                      var selector = '.' + classesMap[key];
+                      this.$element.find(selector).addClass(attributes['class']);
+                    }
+                  }
+                  break;
+
+                case 'closeOnEscape':
+                  dialogOptions.closeOnEscape = options.closeOnEscape;
+                  mappedOptions.keyboard = !!options.closeOnEscape;
+                  break;
+
+                case 'closeText':
+                  Bootstrap.unsupported('option', name, options.closeText);
+                  break;
+
+                case 'draggable':
+                  dialogOptions.draggable = options.draggable;
+                  $content
+                    .draggable({
+                      handle: '.modal-header',
+                      drag: relayEvent(this.$element, 'dialogdrag'),
+                      start: relayEvent(this.$element, 'dialogdragstart'),
+                      end: relayEvent(this.$element, 'dialogdragend')
+                    })
+                    .draggable(options.draggable ? 'enable' : 'disable');
+                  break;
+
+                case 'hide':
+                  if (options.hide === false || options.hide === true) {
+                    this.$element[options.hide ? 'addClass' : 'removeClass']('fade');
+                    mappedOptions.animation = options.hide;
+                  }
+                  else {
+                    Bootstrap.unsupported('option', name + ' (complex animation)', options.hide);
+                  }
+                  break;
+
+                case 'modal':
+                  mappedOptions.backdrop = !!options.modal;
+                  dialogOptions.modal = options.modal;
+
+                  // If not a modal and no initial position, center it.
+                  if (!options.modal && !options.position) {
+                    this.position({ my: 'center', of: window });
+                  }
+                  break;
+
+                case 'position':
+                  dialogOptions.position = options.position;
+                  this.position(options.position);
+                  break;
+
+                // Resizable support (must initialize first).
+                case 'resizable':
+                  dialogOptions.resizeable = options.resizable;
+                  $content
+                    .resizable({
+                      resize: relayEvent(this.$element, 'dialogresize'),
+                      start: relayEvent(this.$element, 'dialogresizestart'),
+                      end: relayEvent(this.$element, 'dialogresizeend')
+                    })
+                    .resizable(options.resizable ? 'enable' : 'disable');
+                  break;
+
+                case 'show':
+                  if (options.show === false || options.show === true) {
+                    this.$element[options.show ? 'addClass' : 'removeClass']('fade');
+                    mappedOptions.animation = options.show;
+                  }
+                  else {
+                    Bootstrap.unsupported('option', name + ' (complex animation)', options.show);
+                  }
+                  break;
+
+                case 'title':
+                  dialogOptions.title = options.title;
+                  this.$dialog.find('.modal-title').text(options.title);
+                  break;
+
+              }
+            }
+
+            // Add the supported dialog options to the mapped options.
+            mappedOptions.dialogOptions = dialogOptions;
+
+            return mappedOptions;
+          },
+
+          /**
+           * Handler for $.fn.dialog('moveToTop').
+           */
+          moveToTop: function () {
+            Bootstrap.unsupported('method', 'moveToTop', arguments);
+          },
+
+          /**
+           * Handler for $.fn.dialog('option').
+           */
+          option: function () {
+            var clone = {options: $.extend({}, this.options)};
+
+            // Apply the parent option method to the clone of current options.
+            this.super.apply(clone, arguments);
+
+            // Merge in the cloned mapped options.
+            $.extend(true, this.options, this.mapDialogOptions(clone.options));
+          },
+
+          position: function(position) {
+            // Reset modal styling.
+            this.$element.css({
+              bottom: 'initial',
+              overflow: 'visible',
+              right: 'initial'
+            });
+
+            // Position the modal.
+            this.$element.position(position);
+          },
+
+          /**
+           * Handler for $.fn.dialog('open').
+           */
+          open: function () {
+            this.show.apply(this, arguments);
+          },
+
+          /**
+           * Handler for $.fn.dialog('widget').
+           */
+          widget: function () {
+            return this.$element;
+          }
+        }
+      };
+    });
+
+    /**
+     * Extend Drupal theming functions.
+     */
+    $.extend(Drupal.theme, /** @lend Drupal.theme */ {
+
+      /**
+       * Renders a jQuery UI Dialog compatible button element.
+       *
+       * @param {Object} button
+       *   The button object passed in the dialog options.
+       *
+       * @return {String}
+       *   The modal dialog button markup.
+       *
+       * @see http://api.jqueryui.com/dialog/#option-buttons
+       * @see http://api.jqueryui.com/button/
+       */
+      bootstrapModalDialogButton: function (button) {
+        var attributes = Attributes.create();
+
+        var icon = '';
+        var iconPosition = button.iconPosition || 'beginning';
+        iconPosition = (iconPosition === 'end' && !rtl) || (iconPosition === 'beginning' && rtl) ? 'after' : 'before';
+        if (button.icon) {
+          var iconAttributes = Attributes.create()
+            .addClass(['ui-icon', button.icon])
+            .set('aria-hidden', 'true');
+          icon = '<span' + iconAttributes + '></span>';
+        }
+
+        // Value.
+        var value = button.text;
+        attributes.set('value', iconPosition === 'before' ? icon + value : value + icon);
+
+        // Handle disabled.
+        attributes[button.disabled ? 'set' :'remove']('disabled', 'disabled');
+
+        if (button.classes) {
+          attributes.addClass(Object.values(button.classes));
+        }
+        if (button['class']) {
+          attributes.addClass(button['class']);
+        }
+
+        return Drupal.theme('button', attributes);
+      }
+
+    });
+
+  });
+
+
+})(window.jQuery, window.Drupal, window.Drupal.bootstrap, window.Attributes, window.drupalSettings);
diff --git a/js/modal.js b/js/modal.js
index 8f32e5b..2e44294 100644
--- a/js/modal.js
+++ b/js/modal.js
@@ -2,218 +2,469 @@
  * @file
  * Bootstrap Modals.
  */
-(function ($, Drupal, Bootstrap) {
-  "use strict";
+(function ($, Drupal, Bootstrap, Attributes) {
+  'use strict';
 
   /**
-   * Extend the Bootstrap Modal plugin constructor class.
+   * Only process this once.
    */
-  Bootstrap.extendPlugin('modal', function (settings) {
-    return {
-      DEFAULTS: {
+  Bootstrap.once('modal', function (settings) {
+
+    /**
+     * Replace the Bootstrap Modal jQuery plugin definition.
+     *
+     * This adds a little bit of functionality so it works better with Drupal.
+     */
+    Bootstrap.replacePlugin('modal', function () {
+      var BootstrapModal = this;
+
+      // Override the Modal constructor.
+      var Modal = function (element, options) {
+        this.options             = options;
+        this.$body               = $(document.body);
+        this.$element            = $(element);
+        this.$dialog             = this.$element.find('.modal-dialog');
+        this.$content            = this.$dialog.find('.modal-content');
+        this.$dialogBody         = this.$content.find('.modal-body');
+        this.$footer             = this.$dialog.find('.modal-footer');
+        this.$backdrop           = null;
+        this.isShown             = null;
+        this.originalBodyPad     = null;
+        this.scrollbarWidth      = 0;
+        this.ignoreBackdropClick = false;
+      };
+
+      // Extend defaults to take into account for theme settings.
+      Modal.DEFAULTS = $.extend({}, BootstrapModal.DEFAULTS, {
         animation: !!settings.modal_animation,
         backdrop: settings.modal_backdrop === 'static' ? 'static' : !!settings.modal_backdrop,
         keyboard: !!settings.modal_keyboard,
         show: !!settings.modal_show,
         size: settings.modal_size
-      }
-    };
-  });
-
-  /**
-   * Replace the Bootstrap Modal jQuery plugin definition.
-   *
-   * Replacing this is needed so that the "option" method can return values.
-   */
-  Bootstrap.replacePlugin('modal', function () {
-    var Modal = this;
-
-    // Extract the arguments.
-    var args = Array.prototype.slice.call(arguments, 1);
-
-    // Modal jQuery Plugin Definition.
-    return function (option, _relatedTarget) {
-      var ret = void(0);
-      this.each(function () {
-        var $this   = $(this);
-        var data    = $this.data('bs.modal');
-        var options = $.extend({}, Modal.DEFAULTS, $this.data(), typeof option == 'object' && option);
-
-        if (!data) $this.data('bs.modal', (data = new Modal(this, options)));
-        if (typeof option == 'string') ret = data[option].apply(data, args);
-        else if (options.show) data.show(_relatedTarget);
       });
 
-      // If just one element and there was a result returned for the option passed,
-      // then return the result. Otherwise, just return the jQuery object.
-      return this.length === 1 && ret !== void(0) ? ret : this;
-    }
-  });
+      // Copy over the original prototype methods.
+      Modal.prototype = BootstrapModal.prototype;
 
-  /**
-   * Extend Drupal theming functions.
-   */
-  $.extend(Drupal.theme, /** @lend Drupal.theme */ {
-    /**
-     * Theme function for a Bootstrap Modal.
-     *
-     * @param {object}[variables]
-     *   An object with the following keys:
-     *   - title: The name of the tab.
-     *
-     * @return {string}
-     *   The HTML for the modal.
-     */
-    bootstrapModal: function (variables) {
-      var settings = drupalSettings.bootstrap || {};
-      var defaults = {
-        body: '',
-        closeButton: true,
-        description: {
-          content: null,
-          position: 'before'
-        },
-        footer: '',
-        id: 'drupal-modal',
-        size: settings.modal_size ? settings.modal_size : '',
-        title: Drupal.t('Loading...')
+      /**
+       * Handler for $.fn.modal('destroy').
+       */
+      Modal.prototype.destroy = function () {
+        this.hide();
+        Drupal.detachBehaviors(this.$element[0]);
+        this.$element.removeData('bs.modal').remove();
       };
-      variables = $.extend(true, {}, defaults, variables);
-      var output = '';
 
-      // Build the modal wrapper.
-      var classes = ['modal'];
-      if (settings.modal_animation) {
-        classes.push('fade');
-      }
-      output += '<div id="' + variables.id + '" class="' + classes.join(' ') + '" tabindex="-1" role="dialog">';
+      /**
+       * Initialize the modal.
+       */
+      Modal.prototype.init = function () {
+        if (this.options.remote) {
+          this.$content.load(this.options.remote, $.proxy(function () {
+            this.$element.trigger('loaded.bs.modal');
+          }, this));
+        }
+      };
 
-      // Build the modal-dialog wrapper.
-      var dialogClasses = ['modal-dialog'];
-      if (variables.size) {
-        // @todo This should really be a clean CSS class method instead.
-        dialogClasses.push(Drupal.checkPlain(variables.size));
-      }
-      output += '<div class="' + dialogClasses.join(' ') + '" role="document">';
+      // Modal jQuery Plugin Definition.
+      var Plugin = function () {
+        // Extract the arguments.
+        var args = Array.prototype.slice.call(arguments);
+        var method = args.shift();
+        var options = {};
+        if ($.isPlainObject(method)) {
+          options = method;
+          method = null;
+        }
+        var ret = void 0;
+        this.each(function () {
+          var $this   = $(this);
+          var data    = $this.data('bs.modal');
+          var initialize = false;
 
-      // Build the modal-content wrapper.
-      output += '<div class="modal-content">';
+          options = $.extend({}, Modal.DEFAULTS, data && data.options, $this.data(), options);
+          if (!data) {
+            // When initializing the Bootstrap Modal, only pass the "supported"
+            // options by intersecting the default options. This allows plugins
+            // like the jQuery UI bridge to properly detect when options have
+            // changed when they're set below as a global "option" method.
+            $this.data('bs.modal', (data = new Modal(this, Bootstrap.intersectObjects(options, Modal.DEFAULTS))));
+            initialize = true;
+          }
 
-      // Build the header wrapper and title.
-      output += Drupal.theme.bootstrapModalHeader(variables.title, variables.closeButton);
+          // If no method or arguments, treat it like it's initializing the modal.
+          if (!method && !args.length) {
+            data.option(options);
+            initialize = true;
+          }
 
-      // Build the body.
-      output += Drupal.theme.bootstrapModalBody(variables.id + '--body', variables.body, variables.description);
+          // Initialize the modal.
+          if (initialize) {
+            data.init();
+          }
 
-      // Build the footer.
-      output += Drupal.theme.bootstrapModalFooter(variables.footer);
+          if (method) {
+            if (typeof data[method] === 'function') {
+              try {
+                ret = data[method].apply(data, args);
+              }
+              catch (e) {
+                Drupal.throwError(e);
+              }
+            }
+            else {
+              Bootstrap.unsupported('method', method);
+            }
+          }
+        });
 
-      // Close the modal-content wrapper.
-      output += '</div>';
+        // If just one element and there was a result returned for the option passed,
+        // then return the result. Otherwise, just return the jQuery object.
+        return this.length === 1 && ret !== void 0 ? ret : this;
+      };
 
-      // Close the modal-dialog wrapper.
-      output += '</div>';
+      // Replace the plugin constructor with the new Modal constructor.
+      Plugin.Constructor = Modal;
 
-      // Close the modal wrapper.
-      output += '</div>';
+      // Replace the data API so that it calls $.fn.modal rather than Plugin.
+      // This allows sub-themes to replace the jQuery Plugin if they like with
+      // out having to redo all this boilerplate.
+      $(document)
+        .off('click.bs.modal.data-api')
+        .on('click.bs.modal.data-api', '[data-toggle="modal"]', function (e) {
+          var $this   = $(this);
+          var href    = $this.attr('href');
+          var $target = $($this.attr('data-target') || (href && href.replace(/.*(?=#[^\s]+$)/, ''))); // strip for ie7
+          var option  = $target.data('bs.modal') ? 'toggle' : $.extend({ remote: !/#/.test(href) && href }, $target.data(), $this.data());
 
-      // Return the constructed modal.
-      return output;
-    },
+          if ($this.is('a')) e.preventDefault();
 
-    /**
-     * Theme function for a Bootstrap Modal body markup.
-     *
-     * @param {string} id
-     *   A unique ID for the modal body div.
-     * @param {string} body
-     *   The HTML markup to place in the body.
-     * @param {string|object} description
-     *   A description to show. Can either be a string or an object with the
-     *   following key/value pairs:
-     *   - content: The description value.
-     *   - position: (optional) A display setting that can have these values:
-     *     - before: The description is displayed before the body. This is the
-     *       default value.
-     *     - after: The description is display after the body.
-     *     - invisible: The description is displayed after the element, hidden
-     *       visually but available to screen readers.
-     *
-     * @return {string}
-     *   The HTML for the modal close button.
-     */
-    bootstrapModalBody: function (id, body, description) {
-      var output = '';
-      output += '<div id="' + id + '" class="modal-body">';
-      if (!description || !$.isPlainObject(description)) {
-        description = { content: description};
-      }
-      description = $.extend({ position: 'before' }, description);
+          $target.one('show.bs.modal', function (showEvent) {
+            // Only register focus restorer if modal will actually get shown.
+            if (showEvent.isDefaultPrevented()) return;
+            $target.one('hidden.bs.modal', function () {
+              $this.is(':visible') && $this.trigger('focus');
+            });
+          });
+          $target.modal(option, this);
+        });
 
-      var descriptionClasses = ['help-block'];
-      if (description.content && description.position === 'invisible') {
-        descriptionClasses.push('sr-only');
-      }
-      if (description.content && description.position === 'before') {
-        output += '<p class="' + descriptionClasses.join(' ') + '">' + description.content + '</p>';
-      }
-      output += body;
-      if (description.content && (description.position === 'after' || description.position === 'invisible')) {
-        output += '<p class="' + descriptionClasses.join(' ') + '">' + description.content + '</p>';
-      }
-      output += '</div>';
-      return output;
-    },
+      return Plugin;
+    });
 
     /**
-     * Theme function for a Bootstrap Modal close button.
-     *
-     * @return {string}
-     *   The HTML for the modal close button.
+     * Extend Drupal theming functions.
      */
-    bootstrapModalClose: function () {
-      return '<button type="button" class="close" data-dismiss="modal" aria-label="' + Drupal.t('Close') + '"><span aria-hidden="true">&times;</span></button>';
-    },
+    $.extend(Drupal.theme, /** @lend Drupal.theme */ {
+      /**
+       * Theme function for a Bootstrap Modal.
+       *
+       * @param {Object} [variables]
+       *   An object containing key/value pairs of variables.
+       *
+       * @return {string}
+       *   The HTML for the modal.
+       */
+      bootstrapModal: function (variables) {
+        var output = '';
+        var settings = drupalSettings.bootstrap || {};
+        var defaults = {
+          attributes: {
+            class: ['modal'],
+            tabindex: -1,
+            role: 'dialog'
+          },
+          body: '',
+          closeButton: true,
+          description: {
+            attributes: {
+              class: ['help-block']
+            },
+            content: null,
+            position: 'before'
+          },
+          footer: '',
+          id: 'drupal-modal',
+          size: settings.modal_size ? settings.modal_size : '',
+          title: {
+            attributes: {
+              class: ['modal-title']
+            },
+            content: Drupal.t('Loading...'),
+            html: false,
+            tag: 'h4'
+          }
+        };
+        variables = $.extend(true, {}, defaults, variables);
 
-    /**
-     * Theme function for a Bootstrap Modal footer.
-     *
-     * @param {string} [footer]
-     *   The HTML markup to place in the footer.
-     * @param {boolean} [force]
-     *   Flag to force the rendering of the footer.
-     *
-     * @return {string}
-     *   The HTML for the modal footer.
-     */
-    bootstrapModalFooter: function (footer, force) {
-      return footer || force ? '<div class="modal-footer">' + (footer || '') + '</div>' : '';
-    },
+        var attributes = Attributes.create(defaults.attributes).merge(variables.attributes);
+        attributes.set('id', attributes.get('id', variables.id));
 
-    /**
-     * Theme function for a Bootstrap Modal header.
-     *
-     * @param {string} [title]
-     *   The title for the header.
-     * @param {boolean} [closeButton]
-     *   Flag indicating whether or not to show the close button in the header.
-     *
-     * @return {string}
-     *   The HTML for the modal header.
-     */
-    bootstrapModalHeader: function (title, closeButton) {
-      var output = '';
-      if (title) {
-        closeButton = closeButton !== void(0) ? closeButton : true;
-        output += '<div class="modal-header">';
-        if (closeButton) {
-          output += Drupal.theme.bootstrapModalClose();
+        if (settings.modal_animation) {
+          attributes.addClass('fade');
         }
-        output += '<h4 class="modal-title">' + Drupal.checkPlain(title) + '</h4>';
+
+        // Build the modal wrapper.
+        output += '<div' + attributes + '>';
+
+        // Build the modal-dialog wrapper.
+        output += Drupal.theme('bootstrapModalDialog', _.omit(variables, 'attributes'));
+
+        // Close the modal wrapper.
         output += '</div>';
+
+        // Return the constructed modal.
+        return output;
+      },
+
+      /**
+       * Theme function for a Bootstrap Modal dialog markup.
+       *
+       * @param {Object} [variables]
+       *   An object containing key/value pairs of variables.
+       *
+       * @return {string}
+       *   The HTML for the modal close button.
+       */
+      bootstrapModalDialog: function (variables) {
+        var output = '';
+
+        var defaults = {
+          attributes: {
+            class: ['modal-dialog'],
+            role: 'document'
+          },
+          id: 'drupal-modal'
+        };
+        variables = $.extend(true, {}, defaults, variables);
+
+        var attributes = Attributes.create(defaults.attributes).merge(variables.attributes);
+        attributes.set('id', attributes.get('id', variables.id + '--dialog'));
+
+        if (variables.size) {
+          attributes.addClass(variables.size);
+        }
+        output += '<div' + attributes + '>';
+
+        // Build the modal-content wrapper.
+        output += Drupal.theme('bootstrapModalContent', _.omit(variables, 'attributes'));
+
+        // Close the modal-dialog wrapper.
+        output += '</div>';
+        return output;
+      },
+
+      /**
+       * Theme function for a Bootstrap Modal content markup.
+       *
+       * @param {Object} [variables]
+       *   An object containing key/value pairs of variables.
+       *
+       * @return {string}
+       *   The HTML for the modal close button.
+       */
+      bootstrapModalContent: function (variables) {
+        var output = '';
+
+        var defaults = {
+          attributes: {
+            class: ['modal-content']
+          },
+          id: 'drupal-modal'
+        };
+        variables = $.extend(true, {}, defaults, variables);
+
+        var attributes = Attributes.create(defaults.attributes).merge(variables.attributes);
+        attributes.set('id', attributes.get('id', variables.id + '--content'));
+
+        // Build the modal-content wrapper.
+        output += '<div' + attributes + '>';
+        variables = _.omit(variables, 'attributes');
+
+        // Build the header wrapper and title.
+        output += Drupal.theme('bootstrapModalHeader', variables);
+
+        // Build the body.
+        output += Drupal.theme('bootstrapModalBody', variables);
+
+        // Build the footer.
+        output += Drupal.theme('bootstrapModalFooter', variables);
+
+        // Close the modal-content wrapper.
+        output += '</div>';
+
+        return output;
+      },
+
+      /**
+       * Theme function for a Bootstrap Modal body markup.
+       *
+       * @param {Object} [variables]
+       *   An object containing key/value pairs of variables.
+       *
+       * @return {string}
+       *   The HTML for the modal close button.
+       */
+      bootstrapModalBody: function (variables) {
+        var output = '';
+
+        var defaults = {
+          attributes: {
+            class: ['modal-body']
+          },
+          body: '',
+          description: {
+            attributes: {
+              class: ['help-block']
+            },
+            content: null,
+            position: 'before'
+          },
+          id: 'drupal-modal'
+        };
+        variables = $.extend(true, {}, defaults, variables);
+
+        var attributes = Attributes.create(defaults.attributes).merge(variables.attributes);
+        attributes.set('id', attributes.get('id', variables.id + '--body'));
+
+        output += '<div' + attributes + '>';
+
+        if (typeof variables.description === 'string') {
+          variables.description = $.extend({}, defaults.description, { content: variables.description });
+        }
+
+        var description = variables.description;
+        description.attributes = Attributes.create(defaults.description.attributes).merge(description.attributes);
+
+        if (description.content && description.position === 'invisible') {
+          description.attributes.addClass('sr-only');
+        }
+
+        if (description.content && description.position === 'before') {
+          output += '<p' + description.attributes + '>' + description.content + '</p>';
+        }
+
+        output += variables.body;
+
+        if (description.content && (description.position === 'after' || description.position === 'invisible')) {
+          output += '<p' + description.attributes + '>' + description.content + '</p>';
+        }
+
+        output += '</div>';
+
+        return output;
+      },
+
+      /**
+       * Theme function for a Bootstrap Modal close button.
+       *
+       * @param {Object} [variables]
+       *   An object containing key/value pairs of variables.
+       *
+       * @return {string}
+       *   The HTML for the modal close button.
+       */
+      bootstrapModalClose: function (variables) {
+        var defaults = {
+          attributes: {
+            'aria-label': Drupal.t('Close'),
+            class: ['close'],
+            'data-dismiss': 'modal',
+            type: 'button'
+          }
+        };
+        variables = $.extend(true, {}, defaults, variables);
+        var attributes = Attributes.create(defaults.attributes).merge(variables.attributes);
+        return '<button' + attributes + '><span aria-hidden="true">&times;</span></button>';
+      },
+
+      /**
+       * Theme function for a Bootstrap Modal footer.
+       *
+       * @param {Object} [variables]
+       *   An object containing key/value pairs of variables.
+       * @param {boolean} [force]
+       *   Flag to force rendering the footer, regardless if there's content.
+       *
+       * @return {string}
+       *   The HTML for the modal footer.
+       */
+      bootstrapModalFooter: function (variables, force) {
+        var output = '';
+        var defaults = {
+          attributes: {
+            class: ['modal-footer']
+          },
+          footer: '',
+          id: 'drupal-modal'
+        };
+
+        variables = $.extend(true, {}, defaults, variables);
+
+        if (force || variables.footer) {
+          var attributes = Attributes.create(defaults.attributes).merge(variables.attributes);
+          attributes.set('id', attributes.get('id', variables.id + '--footer'));
+          output += '<div' + attributes + '>';
+          output += variables.footer;
+          output += '</div>';
+        }
+
+        return output;
+      },
+
+      /**
+       * Theme function for a Bootstrap Modal header.
+       *
+       * @param {Object} [variables]
+       *   An object containing key/value pairs of variables.
+       *
+       * @return {string}
+       *   The HTML for the modal header.
+       */
+      bootstrapModalHeader: function (variables) {
+        var output = '';
+
+        var defaults = {
+          attributes: {
+            class: ['modal-header']
+          },
+          closeButton: true,
+          id: 'drupal-modal',
+          title: {
+            attributes: {
+              class: ['modal-title']
+            },
+            content: Drupal.t('Loading...'),
+            html: false,
+            tag: 'h4'
+          }
+        };
+        variables = $.extend(true, {}, defaults, variables);
+
+        var title = variables.title;
+        if (title) {
+          var attributes = Attributes.create(defaults.attributes).merge(variables.attributes);
+          attributes.set('id', attributes.get('id', variables.id + '--header'));
+
+          if (typeof title === 'string') {
+            title = $.extend({}, defaults.title, { content: title });
+          }
+
+          output += '<div' + attributes + '>';
+
+          if (variables.closeButton) {
+            output += Drupal.theme('bootstrapModalClose', _.omit(variables, 'attributes'));
+          }
+
+          output += '<' + Drupal.checkPlain(title.tag) + Attributes.create(defaults.title.attributes).merge(title.attributes) + '>' + (title.html ? title.content : Drupal.checkPlain(title.content)) + '</' + Drupal.checkPlain(title.tag) + '>';
+
+          output += '</div>';
+        }
+
+        return output;
       }
-      return output;
-    }
-  })
+    })
+
+  });
 
-})(window.jQuery, window.Drupal, window.Drupal.bootstrap);
+})(window.jQuery, window.Drupal, window.Drupal.bootstrap, window.Attributes);
diff --git a/js/theme-settings.js b/js/theme-settings.js
index 64ec490..b66bb28 100644
--- a/js/theme-settings.js
+++ b/js/theme-settings.js
@@ -62,7 +62,12 @@
       $context.find('#edit-javascript').drupalSetSummary(function () {
         var summary = [];
         if ($context.find('input[name="modal_enabled"]').is(':checked')) {
-          summary.push(Drupal.t('Modals'));
+          if ($context.find('input[name="modal_jquery_ui_bridge"]').is(':checked')) {
+            summary.push(Drupal.t('Modals (Bridged)'));
+          }
+          else {
+            summary.push(Drupal.t('Modals'));
+          }
         }
         if ($context.find('input[name="popover_enabled"]').is(':checked')) {
           summary.push(Drupal.t('Popovers'));
diff --git a/js/theme.js b/js/theme.js
index 7e3de0a..2f52c70 100644
--- a/js/theme.js
+++ b/js/theme.js
@@ -2,7 +2,7 @@
  * @file
  * Theme hooks for the Drupal Bootstrap base theme.
  */
-(function ($, Drupal, Bootstrap) {
+(function ($, Drupal, Bootstrap, Attributes) {
 
   /**
    * Fallback for theming an icon if the Icon API module is not installed.
@@ -24,7 +24,7 @@
        */
       icon: function (bundle, icon, attributes) {
         if (!Drupal.icon.bundles[bundle]) return '';
-        attributes = Attributes(attributes).addClass('icon').set('aria-hidden', 'true');
+        attributes = Attributes.create(attributes).addClass('icon').set('aria-hidden', 'true');
         icon = Drupal.icon.bundles[bundle](icon, attributes);
         return '<span' + attributes + '></span>';
       }
@@ -54,7 +54,7 @@
      * @returns {string}
      */
     ajaxThrobber: function () {
-      return Drupal.theme.bootstrapIcon('refresh', {'class': ['ajax-throbber', 'glyphicon-spin'] });
+      return Drupal.theme('bootstrapIcon', 'refresh', {'class': ['ajax-throbber', 'glyphicon-spin'] });
     },
 
     /**
@@ -75,13 +75,19 @@
      * @returns {string}
      */
     button: function (attributes) {
-      attributes = Attributes(attributes).addClass('btn');
+      attributes = Attributes.create(attributes).addClass('btn');
       var context = attributes.get('context', 'default');
       var label = attributes.get('value', '');
       attributes.remove('context').remove('value');
       if (!attributes.hasClass(['btn-default', 'btn-primary', 'btn-success', 'btn-info', 'btn-warning', 'btn-danger', 'btn-link'])) {
         attributes.addClass('btn-' + Bootstrap.checkPlain(context));
       }
+
+      // Attempt to, intelligently, provide a default button "type".
+      if (!attributes.exists('type')) {
+        attributes.set('type', attributes.hasClass('form-submit') ? 'submit' : 'button');
+      }
+
       return '<button' + attributes + '>' + label + '</button>';
     },
 
@@ -110,7 +116,7 @@
      * @returns {string}
      */
     'btn-block': function (attributes) {
-      return Drupal.theme('button', Attributes(attributes).addClass('btn-block'));
+      return Drupal.theme('button', Attributes.create(attributes).addClass('btn-block'));
     },
 
     /**
@@ -124,7 +130,7 @@
      * @returns {string}
      */
     'btn-lg': function (attributes) {
-      return Drupal.theme('button', Attributes(attributes).addClass('btn-lg'));
+      return Drupal.theme('button', Attributes.create(attributes).addClass('btn-lg'));
     },
 
     /**
@@ -138,7 +144,7 @@
      * @returns {string}
      */
     'btn-sm': function (attributes) {
-      return Drupal.theme('button', Attributes(attributes).addClass('btn-sm'));
+      return Drupal.theme('button', Attributes.create(attributes).addClass('btn-sm'));
     },
 
     /**
@@ -152,7 +158,7 @@
      * @returns {string}
      */
     'btn-xs': function (attributes) {
-      return Drupal.theme('button', Attributes(attributes).addClass('btn-xs'));
+      return Drupal.theme('button', Attributes.create(attributes).addClass('btn-xs'));
     },
 
     /**
@@ -171,4 +177,4 @@
 
   });
 
-})(window.jQuery, window.Drupal, window.Drupal.bootstrap);
+})(window.jQuery, window.Drupal, window.Drupal.bootstrap, window.Attributes);
diff --git a/src/Plugin/Alter/LibraryInfo.php b/src/Plugin/Alter/LibraryInfo.php
index 7665cb9..6ab4ef4 100644
--- a/src/Plugin/Alter/LibraryInfo.php
+++ b/src/Plugin/Alter/LibraryInfo.php
@@ -77,8 +77,16 @@ class LibraryInfo extends PluginBase implements AlterInterface {
     elseif ($extension === 'core') {
       // Replace core dialog/jQuery UI implementations with Bootstrap Modals.
       if ($this->theme->getSetting('modal_enabled')) {
-        $libraries['drupal.dialog']['override'] = 'bootstrap/drupal.dialog';
-        $libraries['drupal.dialog.ajax']['override'] = 'bootstrap/drupal.dialog.ajax';
+        // Replace dependencies if using bridge so jQuery UI is not loaded
+        // and remove dialog.jquery-ui.js since the dialog widget isn't loaded.
+        if ($this->theme->getSetting('modal_jquery_ui_bridge')) {
+          $libraries['drupal.dialog']['dependencies'] = ['bootstrap/modal.jquery.ui.bridge'];
+          unset($libraries['drupal.dialog']['js']['misc/dialog/dialog.jquery-ui.js']);
+        }
+        // Otherwise, just append the modal.
+        else {
+          $libraries['drupal.dialog']['dependencies'][] =  'bootstrap/modal';
+        }
       }
     }
   }
diff --git a/src/Plugin/Setting/JavaScript/Modals/ModalEnabled.php b/src/Plugin/Setting/JavaScript/Modals/ModalEnabled.php
index d28ceb6..20e911f 100644
--- a/src/Plugin/Setting/JavaScript/Modals/ModalEnabled.php
+++ b/src/Plugin/Setting/JavaScript/Modals/ModalEnabled.php
@@ -15,7 +15,6 @@ use Drupal\Core\Form\FormStateInterface;
  *   id = "modal_enabled",
  *   type = "checkbox",
  *   title = @Translation("Enable Bootstrap Modals"),
- *   description = @Translation("Enabling this will replace core's jQuery UI Dialog implementations with modals from the Bootstrap Framework."),
  *   defaultValue = 1,
  *   weight = -1,
  *   groups = {
diff --git a/src/Plugin/Setting/JavaScript/Modals/ModalJqueryUIBridge.php b/src/Plugin/Setting/JavaScript/Modals/ModalJqueryUIBridge.php
new file mode 100644
index 0000000..51bfe9e
--- /dev/null
+++ b/src/Plugin/Setting/JavaScript/Modals/ModalJqueryUIBridge.php
@@ -0,0 +1,55 @@
+<?php
+/**
+ * @file
+ * Contains \Drupal\bootstrap\Plugin\Setting\JavaScript\Modals\ModalEnabled.
+ */
+
+namespace Drupal\bootstrap\Plugin\Setting\JavaScript\Modals;
+
+use Drupal\bootstrap\Annotation\BootstrapSetting;
+use Drupal\bootstrap\Plugin\Setting\SettingBase;
+use Drupal\bootstrap\Utility\Element;
+use Drupal\Core\Annotation\Translation;
+use Drupal\Core\Form\FormStateInterface;
+
+/**
+ * The "modal_jquery_ui_bridge" theme setting.
+ *
+ * @ingroup plugins_setting
+ *
+ * @BootstrapSetting(
+ *   id = "modal_jquery_ui_bridge",
+ *   type = "checkbox",
+ *   title = @Translation("jQuery UI Bridge"),
+ *   description = @Translation("Enabling this replaces the core/jquery.ui.dialog dependency in the core/drupal.dialog library with a bridge. This bridge adds support to Bootstrap Modals so that it may interpret jQuery UI Dialog functionality. It is highly recommended that this remain enabled unless you know what you're really doing."),
+ *   defaultValue = 1,
+ *   weight = 0,
+ *   groups = {
+ *     "javascript" = @Translation("JavaScript"),
+ *     "modals" = @Translation("Modals"),
+ *   },
+ * )
+ */
+class ModalJqueryUIBridge extends SettingBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function alterFormElement(Element $form, FormStateInterface $form_state, $form_id = NULL) {
+    parent::alterFormElement($form, $form_state, $form_id);
+    $setting = $this->getSettingElement($form, $form_state);
+    $setting->setProperty('states', [
+      'visible' => [
+        ':input[name="modal_enabled"]' => ['checked' => TRUE],
+      ],
+    ]);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCacheTags() {
+    return ['rendered', 'library_info'];
+  }
+
+}
-- 
2.15.1

