/** * @file * Bootstrap Modals. * * @param {jQuery} $ * @param {Drupal} Drupal * @param {Drupal.bootstrap} Bootstrap * @param {Attributes} Attributes */ (function ($, Drupal, Bootstrap, Attributes) { 'use strict'; /** * Document jQuery object. * * @type {jQuery} */ var $document = $(document); /** * Finds the first available and visible focusable input element. * * This is abstracted from the main code below so sub-themes can override * this method to return their own element if desired. * * @param {Modal} modal * The Bootstrap modal instance. * * @return {jQuery} * A jQuery object containing the element that should be focused. Note: if * this object contains multiple elements, only the first visible one will * be used. */ Bootstrap.modalFindFocusableElement = function (modal) { return modal.$dialogBody.find(':input,:button,.btn').not('.visually-hidden,.sr-only'); }; $document.on('shown.bs.modal', function (e) { var $modal = $(e.target); var modal = $modal.data('bs.modal'); // Focus the first input element found. if (modal && modal.options.focusInput) { var $focusable = Bootstrap.modalFindFocusableElement(modal); if ($focusable && $focusable[0]) { var $input = $focusable.filter(':visible:first').focus(); // Select text if input is text. if (modal.options.selectText && $input.is(':text')) { $input[0].setSelectionRange(0, $input[0].value.length) } } else if (modal.$close.is(':visible')) { modal.$close.focus(); } } }); /** * Only process this once. */ 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. Bootstrap.Modal = function (element, options) { this.$body = $(document.body); this.$element = $(element); this.$dialog = this.$element.find('.modal-dialog'); this.$header = this.$dialog.find('.modal-header'); this.$title = this.$dialog.find('.modal-title'); this.$close = this.$header.find('.close'); this.$footer = this.$dialog.find('.modal-footer'); this.$content = this.$dialog.find('.modal-content'); this.$dialogBody = this.$dialog.find('.modal-body'); this.$backdrop = null; this.isShown = null; this.originalBodyPad = null; this.scrollbarWidth = 0; this.ignoreBackdropClick = false; this.options = this.mapDialogOptions(options); }; // Extend defaults to take into account for theme settings. Bootstrap.Modal.DEFAULTS = $.extend({}, BootstrapModal.DEFAULTS, { animation: !!settings.modal_animation, backdrop: settings.modal_backdrop === 'static' ? 'static' : !!settings.modal_backdrop, focusInput: !!settings.modal_focus_input, selectText: !!settings.modal_select_text, keyboard: !!settings.modal_keyboard, remote: null, show: !!settings.modal_show, size: settings.modal_size }); // Copy over the original prototype methods. Bootstrap.Modal.prototype = BootstrapModal.prototype; /** * Handler for $.fn.modal('destroy'). */ Bootstrap.Modal.prototype.destroy = function () { this.hide(); Drupal.detachBehaviors(this.$element[0]); this.$element.removeData('bs.modal').remove(); }; /** * Initialize the modal. */ Bootstrap.Modal.prototype.init = function () { if (this.options.remote) { this.$content.load(this.options.remote, $.proxy(function () { this.$element.trigger('loaded.bs.modal'); }, this)); } }; /** * Map dialog options. * * Note: this is primarily for use in modal.jquery.ui.bridge.js. * * @param {Object} options * The passed options. */ Bootstrap.Modal.prototype.mapDialogOptions = function (options) { return options || {}; } // Modal jQuery Plugin Definition. var Plugin = function () { // Extract the arguments. var args = Array.prototype.slice.call(arguments); var method = args[0]; var options = args[1] || {}; var relatedTarget = args[2] || null; // Move arguments down if no method was passed. if ($.isPlainObject(method)) { relatedTarget = options || null; options = method; method = null; } var ret = void 0; this.each(function () { var $this = $(this); var data = $this.data('bs.modal'); var initialize = false; // Immediately return if there's no instance to invoke a valid method. var showMethods = ['open', 'show', 'toggle']; if (!data && method && showMethods.indexOf(method) === -1) { return; } options = Bootstrap.normalizeObject($.extend({}, Bootstrap.Modal.DEFAULTS, data && data.options, $this.data(), options)); delete options['bs.modal']; if (!data) { $this.data('bs.modal', (data = new Bootstrap.Modal(this, options))); initialize = true; } // Initialize the modal. if (initialize || (!method && !args.length)) { data.init(); } // Explicit method passed. if (method) { if (typeof data[method] === 'function') { try { ret = data[method].apply(data, args.slice(1)); } catch (e) { Drupal.throwError(e); } } else { Bootstrap.unsupported('method', method); } } // No method, set options and open if necessary. else { data.option(options); if (options.show && !data.isShown) { 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; }; // Replace the plugin constructor with the new Modal constructor. Plugin.Constructor = Bootstrap.Modal; // 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 $target = $document.find(target); var options = $target.data('bs.modal') ? 'toggle' : $.extend({ remote: !/#/.test(href) && href }, $target.data(), $this.data()); if ($this.is('a')) e.preventDefault(); $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(options, this); }); return Plugin; }); /** * Extend Drupal theming functions. */ $.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); var attributes = Attributes.create(defaults.attributes).merge(variables.attributes); attributes.set('id', attributes.get('id', variables.id)); if (settings.modal_animation) { attributes.addClass('fade'); } // Build the modal wrapper. output += ''; // Build the modal-dialog wrapper. output += Drupal.theme('bootstrapModalDialog', _.omit(variables, 'attributes')); // Close the modal wrapper. output += ''; // 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 += ''; // Build the modal-content wrapper. output += Drupal.theme('bootstrapModalContent', _.omit(variables, 'attributes')); // Close the modal-dialog wrapper. output += ''; 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 += ''; 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 += ''; 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 += ''; 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 += '' + description.content + '

'; } output += variables.body; if (description.content && (description.position === 'after' || description.position === 'invisible')) { output += '' + description.content + '

'; } output += ''; 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 ''; }, /** * 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 += ''; output += variables.footer; output += ''; } 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); if (typeof variables.title === 'string') { variables.title = $.extend({}, defaults.title, { content: variables.title }); } var title = Drupal.theme('bootstrapModalTitle', variables.title); if (title) { var attributes = Attributes.create(defaults.attributes).merge(variables.attributes); attributes.set('id', attributes.get('id', variables.id + '--header')); output += ''; if (variables.closeButton) { output += Drupal.theme('bootstrapModalClose', _.omit(variables, 'attributes')); } output += title; output += ''; } return output; }, /** * Theme function for a Bootstrap Modal title. * * @param {Object} [variables] * An object containing key/value pairs of variables. * * @return {string} * The HTML for the modal title. */ bootstrapModalTitle: function (variables) { var output = ''; var defaults = { attributes: { class: ['modal-title'] }, closeButton: true, id: 'drupal-modal', content: Drupal.t('Loading...'), html: false, tag: 'h4' }; if (typeof variables === 'string') { variables = $.extend({}, defaults, { content: title }); } variables = $.extend(true, {}, defaults, variables); var attributes = Attributes.create(defaults.attributes).merge(variables.attributes); attributes.set('id', attributes.get('id', variables.id + '--title')); output += '<' + Drupal.checkPlain(variables.tag) + Attributes.create(defaults.attributes).merge(variables.attributes) + '>'; if (variables.closeButton) { output += Drupal.theme('bootstrapModalClose', _.omit(variables, 'attributes')); } output += (variables.html ? variables.content : Drupal.checkPlain(variables.content)); output += ''; return output; } }) }); })(window.jQuery, window.Drupal, window.Drupal.bootstrap, window.Attributes);