/** * @file * Bootstrap Modals. * * @param {jQuery} $ * @param {Drupal} Drupal * @param {Drupal.bootstrap} Bootstrap * @param {Attributes} Attributes * @param {drupalSettings} drupalSettings */ (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'; }); // Create the "dialog" plugin bridge. Bootstrap.Dialog.Bridge = function (options) { var args = Array.prototype.slice.call(arguments); var $element = $(this); var type = options && options.dialogType || $element[0].dialogType || 'modal'; $element[0].dialogType = type; var handler = Bootstrap.Dialog.Handler.get(type); // 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 (args.length === 1 && typeof options === 'object') { this.each(function () { handler.ensureModalStructure(this, options); }); // Proxy to the Bootstrap Modal plugin, indicating that this is a // jQuery UI dialog bridge. return handler.invoke(this, { dialogOptions: options, jQueryUiBridge: true }); } // Otherwise, proxy all arguments to the Bootstrap Modal plugin. var ret; try { ret = handler.invoke.apply(handler, [this].concat(args)); } catch (e) { Bootstrap.warn(e); } // 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; }; // Assign the jQuery "dialog" plugin to use to the bridge. Bootstrap.createPlugin('dialog', Bootstrap.Dialog.Bridge); // Create the "modal" plugin bridge. Bootstrap.Modal.Bridge = function () { var Modal = this; 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 () { var _this = this; this.hide.apply(this, arguments); // For some reason (likely due to the transition event not being // registered properly), the backdrop doesn't always get removed // after the above "hide" method is invoked . Instead, ensure the // backdrop is removed after the transition duration by manually // invoking the internal "hideModal" method shortly thereafter. setTimeout(function () { if (!_this.isShown && _this.$backdrop) { _this.hideModal(); } }, (Modal.TRANSITION_DURATION !== void 0 ? Modal.TRANSITION_DURATION : 300) + 10); }, /** * Creates any necessary buttons from dialog options. */ createButtons: function () { var handler = Bootstrap.Dialog.Handler.get(this.$element); this.$footer.find(handler.selectors.buttons).remove(); // jQuery UI supports both objects and arrays. Unfortunately // developers have misunderstood and abused this by simply placing // the objects that should be in an array inside an object with // arbitrary keys (likely to target specific buttons as a hack). var buttons = this.options.dialogOptions && this.options.dialogOptions.buttons || []; if (!Array.isArray(buttons)) { var array = []; for (var k in buttons) { // Support the proper object values: label => click callback. if (typeof buttons[k] === 'function') { array.push({ label: k, click: buttons[k], }); } // Support nested objects, but log a warning. else if (buttons[k].text || buttons[k].label) { Bootstrap.warn('Malformed jQuery UI dialog button: @key. The button object should be inside an array.', { '@key': k }); array.push(buttons[k]); } else { Bootstrap.unsupported('button', k, buttons[k]); } } buttons = array; } if (buttons.length) { var $buttons = $('
').appendTo(this.$footer); for (var i = 0, l = buttons.length; i < l; i++) { var button = buttons[i]; var $button = $(Drupal.theme('bootstrapModalDialogButton', button)); // Invoke the "create" method for jQuery UI buttons. if (typeof button.create === 'function') { button.create.call($button[0]); } // Bind the "click" method for jQuery UI buttons to the modal. if (typeof button.click === 'function') { $button.on('click', button.click.bind(this.$element)); } $buttons.append($button); } } // Toggle footer visibility based on whether it has child elements. this.$footer[this.$footer.children()[0] ? 'show' : 'hide'](); }, /** * Initializes the Bootstrap Modal. */ init: function () { var handler = Bootstrap.Dialog.Handler.get(this.$element); if (!this.$dialog) { this.$dialog = this.$element.find(handler.selectors.dialog); } this.$dialog.addClass('js-drupal-dialog'); if (!this.$header) { this.$header = this.$dialog.find(handler.selectors.header); } if (!this.$title) { this.$title = this.$dialog.find(handler.selectors.title); } if (!this.$close) { this.$close = this.$header.find(handler.selectors.close); } if (!this.$footer) { this.$footer = this.$dialog.find(handler.selectors.footer); } if (!this.$content) { this.$content = this.$dialog.find(handler.selectors.content); } if (!this.$dialogBody) { this.$dialogBody = this.$dialog.find(handler.selectors.body); } // Relay necessary events. if (this.options.jQueryUiBridge) { this.$element.on('hide.bs.modal', Bootstrap.relayEvent(this.$element, 'dialogbeforeclose', false)); this.$element.on('hidden.bs.modal', Bootstrap.relayEvent(this.$element, 'dialogclose', false)); this.$element.on('show.bs.modal', Bootstrap.relayEvent(this.$element, 'dialogcreate', false)); this.$element.on('shown.bs.modal', Bootstrap.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 = handler.theme('footer', {}, true).insertAfter(this.$dialogBody); } // Map the initial options. $.extend(true, this.options, this.mapDialogOptions(this.options)); // Update buttons. this.createButtons(); // Now call the parent init method. this.super(); // Handle autoResize option (this is a drupal.dialog option). if (this.options.dialogOptions && 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.jQueryUiBridge && 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) { // Retrieve the dialog handler for this type. var handler = Bootstrap.Dialog.Handler.get(this.$element); var mappedOptions = {}; var dialogOptions = options.dialogOptions || {}; // Remove any existing dialog options. delete options.dialogOptions; // Separate Bootstrap modal options from jQuery UI dialog options. for (var k in options) { if (Modal.DEFAULTS.hasOwnProperty(k)) { mappedOptions[k] = options[k]; } else { dialogOptions[k] = options[k]; } } // 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 (dialogOptions[prop] !== void 0) { var value = parseCssUnit(dialogOptions[prop]); if (value) { styles[prop] = value; // If there's a defined height of some kind, enforce the modal // to use flex (on modern browsers). This will ensure that // the core autoResize calculations don't cause the content // to overflow. if (dialogOptions.autoResize && (prop === 'height' || prop === 'maxHeight')) { styles.display = 'flex'; styles.flexDirection = 'column'; this.$dialogBody.css('overflow', 'scroll'); } } } } // Apply mapped CSS styles to the modal-content container. this.$content.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 (dialogOptions.dialogClass) { if (dialogOptions.classes === void 0) { dialogOptions.classes = {}; } if (dialogOptions.classes['ui-dialog'] === void 0) { dialogOptions.classes['ui-dialog'] = ''; } var dialogClass = dialogOptions.classes['ui-dialog'].split(' '); dialogClass.push(dialogOptions.dialogClass); dialogOptions.classes['ui-dialog'] = dialogClass.join(' '); delete dialogOptions.dialogClass; } // Add jQuery UI classes to elements in case developers target them // in callbacks. for (k in classesMap) { this.$element.find('.' + classesMap[k]).addClass(k); } // 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 (dialogOptions[event] === void 0 || typeof dialogOptions[event] !== 'function') continue; this.$element.on('dialog' + event, dialogOptions[event]); } // Support title attribute on the modal. var title; if ((dialogOptions.title === null || dialogOptions.title === void 0) && (title = this.$element.attr('title'))) { dialogOptions.title = title; } // Handle the reset of the options. for (var name in dialogOptions) { if (!dialogOptions.hasOwnProperty(name) || dialogOptions[name] === void 0) continue; switch (name) { case 'appendTo': Bootstrap.unsupported('option', name, dialogOptions.appendTo); break; case 'autoOpen': mappedOptions.show = dialogOptions.show = !!dialogOptions.autoOpen; break; case 'classes': if (dialogOptions.classes) { for (var key in dialogOptions.classes) { if (dialogOptions.classes.hasOwnProperty(key) && classesMap[key] !== void 0) { // Run through Attributes to sanitize classes. var attributes = Attributes.create().addClass(dialogOptions.classes[key]).toPlainObject(); var selector = '.' + classesMap[key]; this.$element.find(selector).addClass(attributes['class']); } } } break; case 'closeOnEscape': mappedOptions.keyboard = !!dialogOptions.closeOnEscape; if (!dialogOptions.closeOnEscape && dialogOptions.modal) { mappedOptions.backdrop = 'static'; } break; case 'closeText': Bootstrap.unsupported('option', name, dialogOptions.closeText); break; case 'draggable': this.$content .draggable({ handle: handler.selectors.header, drag: Bootstrap.relayEvent(this.$element, 'dialogdrag'), start: Bootstrap.relayEvent(this.$element, 'dialogdragstart'), end: Bootstrap.relayEvent(this.$element, 'dialogdragend') }) .draggable(dialogOptions.draggable ? 'enable' : 'disable'); break; case 'hide': if (dialogOptions.hide === false || dialogOptions.hide === true) { this.$element[dialogOptions.hide ? 'addClass' : 'removeClass']('fade'); mappedOptions.animation = dialogOptions.hide; } else { Bootstrap.unsupported('option', name + ' (complex animation)', dialogOptions.hide); } break; case 'modal': if (!dialogOptions.closeOnEscape && dialogOptions.modal) { mappedOptions.backdrop = 'static'; } else { mappedOptions.backdrop = dialogOptions.modal; } // If not a modal and no initial position, center it. if (!dialogOptions.modal && !dialogOptions.position) { this.position({ my: 'center', of: window }); } break; case 'position': this.position(dialogOptions.position); break; // Resizable support (must initialize first). case 'resizable': this.$content .resizable({ resize: Bootstrap.relayEvent(this.$element, 'dialogresize'), start: Bootstrap.relayEvent(this.$element, 'dialogresizestart'), end: Bootstrap.relayEvent(this.$element, 'dialogresizeend') }) .resizable(dialogOptions.resizable ? 'enable' : 'disable'); break; case 'show': if (dialogOptions.show === false || dialogOptions.show === true) { this.$element[dialogOptions.show ? 'addClass' : 'removeClass']('fade'); mappedOptions.animation = dialogOptions.show; } else { Bootstrap.unsupported('option', name + ' (complex animation)', dialogOptions.show); } break; case 'title': this.$title.text(dialogOptions.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: {}}; // 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)); // Update buttons. this.createButtons(); }, 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 the Bootstrap Modal plugin constructor class. Bootstrap.extendPlugin('modal', Bootstrap.Modal.Bridge); // Register default core dialog type handlers. Bootstrap.Dialog.Handler.register('dialog'); Bootstrap.Dialog.Handler.register('modal'); /** * 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'; // Handle Bootstrap icons differently. if (button.bootstrapIcon) { icon = Drupal.theme('icon', 'bootstrap', button.icon); } // Otherwise, assume it's a jQuery UI icon. // @todo Map jQuery UI icons to Bootstrap icons? else if (button.icon) { var iconAttributes = Attributes.create() .addClass(['ui-icon', button.icon]) .set('aria-hidden', 'true'); icon = ''; } // Label. Note: jQuery UI dialog has an inconsistency where it uses // "text" instead of "label", so both need to be supported. var value = button.label || button.text; // Show/hide label. if (icon && ((button.showLabel !== void 0 && !button.showLabel) || (button.text !== void 0 && !button.text))) { value = '' + value + ''; } attributes.set('value', iconPosition === 'before' ? icon + value : value + icon); // Handle disabled. attributes[button.disabled ? 'set' :'remove']('disabled', 'disabled'); if (button.classes) { attributes.addClass(Object.keys(button.classes).map(function(key) { return button.classes[key]; })); } if (button['class']) { attributes.addClass(button['class']); } if (button.primary) { attributes.addClass('btn-primary'); } return Drupal.theme('button', attributes); } }); }); })(window.jQuery, window.Drupal, window.Drupal.bootstrap, window.Attributes, window.drupalSettings);