* @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.$header = this.$dialog.find('.modal-header');
+ this.$close = this.$header.find('.close');
+ this.$footer = this.$dialog.find('.modal-footer');
+ this.$content = this.$dialog.find('.modal-content');
+ this.$dialogBody = this.$content.find('.modal-body');
+ 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">×</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">×</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);