5 (function ($, Drupal, Bootstrap, Attributes, drupalSettings) {
9 * Only process this once.
11 Bootstrap.once('modal.jquery.ui.bridge', function (settings) {
13 var rtl = document.documentElement.getAttribute('dir').toLowerCase() === 'rtl';
15 // Override drupal.dialog button classes. This must be done on DOM ready
16 // since core/drupal.dialog technically depends on this file and has not
17 // yet set their default settings.
19 drupalSettings.dialog.buttonClass = 'btn';
20 drupalSettings.dialog.buttonPrimaryClass = 'btn-primary';
23 var relayEvent = function ($element, name, stopPropagation) {
25 if (stopPropagation === void 0 || stopPropagation) {
28 var parts = name.split('.').filter(Boolean);
29 var type = parts.shift();
30 e.target = $element[0];
31 e.currentTarget = $element[0];
32 e.namespace = parts.join('.');
39 * Proxy $.fn.dialog to $.fn.modal.
41 Bootstrap.createPlugin('dialog', function (options) {
42 // When only options are passed, jQuery UI dialog treats this like a
43 // initialization method. Destroy any existing Bootstrap modal and
44 // recreate it using the contents of the dialog HTML.
45 if (arguments.length === 1 && typeof options === 'object') {
46 this.each(function () {
47 // This part gets a little tricky. Core can potentially already
48 // semi-process this "dialog" if was created using an Ajax command
49 // (i.e. prepareDialogButtons in drupal.ajax.js). Because of this,
50 // we cannot simply dump the existing dialog content into a newly
51 // created modal because that would destroy any existing event
52 // bindings. Instead, we have to create this in steps and "move"
53 // (append) the existing content as needed.
56 // Create a new modal to get a complete template.
57 var $modal = $(Drupal.theme('bootstrapModal', {attributes: Attributes.create(this).remove('style')}));
59 // Store a reference to the content inside the existing dialog.
60 // This references the actual DOM node elements which will allow
61 // jQuery to "move" then when appending below. Using $.fn.children()
62 // does not return any text nodes present and $.fn.html() only returns
63 // a string representation of the content, which effectively destroys
64 // any prior event bindings or processing.
65 var $existing = $this.contents();
67 // Destroy any existing Bootstrap Modal data that may have been saved.
68 $this.removeData('bs.modal');
70 // Set the attributes of the dialog to that of the newly created modal.
71 $this.attr(Attributes.create($modal).toPlainObject());
73 // Append the newly created modal markup.
74 $this.append($modal.html());
76 // Move the existing HTML into the modal markup that was just appended.
77 $this.find('.modal-body').append($existing);
80 // Indicate that the modal is a jQuery UI dialog bridge.
81 options.jQueryUiBridge = true;
83 // Proxy just the options to the Bootstrap Modal plugin.
84 return $.fn.modal.apply(this, [options]);
87 // Otherwise, proxy all arguments to the Bootstrap Modal plugin.
88 return $.fn.modal.apply(this, arguments);
92 * Extend the Bootstrap Modal plugin constructor class.
94 Bootstrap.extendPlugin('modal', function () {
99 // By default, this option is disabled. It's only flagged when a modal
100 // was created using $.fn.dialog above.
101 jQueryUiBridge: false
106 * Handler for $.fn.dialog('close').
111 this.hide.apply(this, arguments);
113 // For some reason (likely due to the transition event not being
114 // registered properly), the backdrop doesn't always get removed
115 // after the above "hide" method is invoked . Instead, ensure the
116 // backdrop is removed after the transition duration by manually
117 // invoking the internal "hideModal" method shortly thereafter.
118 setTimeout(function () {
119 if (!_this.isShown && _this.$backdrop) {
122 }, (Modal.TRANSITION_DURATION !== void 0 ? Modal.TRANSITION_DURATION : 300) + 10);
126 * Creates any necessary buttons from dialog options.
128 createButtons: function () {
129 this.$footer.find('.modal-buttons').remove();
131 // jQuery UI supports both objects and arrays. Unfortunately
132 // developers have misunderstood and abused this by simply placing
133 // the objects that should be in an array inside an object with
134 // arbitrary keys (likely to target specific buttons as a hack).
135 var buttons = this.options.dialogOptions && this.options.dialogOptions.buttons || [];
136 if (!Array.isArray(buttons)) {
138 for (var k in buttons) {
139 // Support the proper object values: label => click callback.
140 if (typeof buttons[k] === 'function') {
146 // Support nested objects, but log a warning.
147 else if (buttons[k].text || buttons[k].label) {
148 Bootstrap.warn('Malformed jQuery UI dialog button: @key. The button object should be inside an array.', {
151 array.push(buttons[k]);
154 Bootstrap.unsupported('button', k, buttons[k]);
160 if (buttons.length) {
161 var $buttons = $('<div class="modal-buttons"/>').appendTo(this.$footer);
162 for (var i = 0, l = buttons.length; i < l; i++) {
163 var button = buttons[i];
164 var $button = $(Drupal.theme('bootstrapModalDialogButton', button));
166 // Invoke the "create" method for jQuery UI buttons.
167 if (typeof button.create === 'function') {
168 button.create.call($button[0]);
171 // Bind the "click" method for jQuery UI buttons to the modal.
172 if (typeof button.click === 'function') {
173 $button.on('click', button.click.bind(this.$element));
176 $buttons.append($button);
180 // Toggle footer visibility based on whether it has child elements.
181 this.$footer[this.$footer.children()[0] ? 'show' : 'hide']();
185 * Initializes the Bootstrap Modal.
188 // Relay necessary events.
189 if (this.options.jQueryUiBridge) {
190 this.$element.on('hide.bs.modal', relayEvent(this.$element, 'dialogbeforeclose', false));
191 this.$element.on('hidden.bs.modal', relayEvent(this.$element, 'dialogclose', false));
192 this.$element.on('show.bs.modal', relayEvent(this.$element, 'dialogcreate', false));
193 this.$element.on('shown.bs.modal', relayEvent(this.$element, 'dialogopen', false));
196 // Create a footer if one doesn't exist.
197 // This is necessary in case dialog.ajax.js decides to add buttons.
198 if (!this.$footer[0]) {
199 this.$footer = $(Drupal.theme('bootstrapModalFooter', {}, true)).insertAfter(this.$dialogBody);
202 // Now call the parent init method.
205 // Handle autoResize option (this is a drupal.dialog option).
206 if (this.options.dialogOptions && this.options.dialogOptions.autoResize && this.options.dialogOptions.position) {
207 this.position(this.options.dialogOptions.position);
210 // If show is enabled and currently not shown, show it.
211 if (this.options.show && !this.isShown) {
217 * Handler for $.fn.dialog('instance').
219 instance: function () {
220 Bootstrap.unsupported('method', 'instance', arguments);
224 * Handler for $.fn.dialog('isOpen').
226 isOpen: function () {
227 return !!this.isShown;
231 * Maps dialog options to the modal.
233 * @param {Object} options
234 * The options to map.
236 mapDialogOptions: function (options) {
237 var dialogOptions = {};
238 var mappedOptions = {};
240 // Handle CSS properties.
241 var cssUnitRegExp = /^([+-]?(?:\d+|\d*\.\d+))([a-z]*|%)?$/;
242 var parseCssUnit = function (value, defaultUnit) {
243 var parts = ('' + value).match(cssUnitRegExp);
244 return parts && parts[1] !== void 0 ? parts[1] + (parts[2] || defaultUnit || 'px') : null;
247 var cssProperties = ['height', 'maxHeight', 'maxWidth', 'minHeight', 'minWidth', 'width'];
248 for (var i = 0, l = cssProperties.length; i < l; i++) {
249 var prop = cssProperties[i];
250 if (options[prop] !== void 0) {
251 var value = parseCssUnit(options[prop]);
253 dialogOptions[prop] = value;
254 styles[prop] = value;
256 // If there's a defined height of some kind, enforce the modal
257 // to use flex (on modern browsers). This will ensure that
258 // the core autoResize calculations don't cause the content
260 if (options.autoResize && (prop === 'height' || prop === 'maxHeight')) {
261 styles.display = 'flex';
262 styles.flexDirection = 'column';
263 this.$dialogBody.css('overflow', 'scroll');
269 // Apply mapped CSS styles to the modal-content container.
270 this.$content.css(styles);
272 // Handle deprecated "dialogClass" option by merging it with "classes".
274 'ui-dialog': 'modal-content',
275 'ui-dialog-titlebar': 'modal-header',
276 'ui-dialog-title': 'modal-title',
277 'ui-dialog-titlebar-close': 'close',
278 'ui-dialog-content': 'modal-body',
279 'ui-dialog-buttonpane': 'modal-footer'
281 if (options.dialogClass) {
282 if (options.classes === void 0) {
283 options.classes = {};
285 if (options.classes['ui-dialog'] === void 0) {
286 options.classes['ui-dialog'] = '';
288 var dialogClass = options.classes['ui-dialog'].split(' ');
289 dialogClass.push(options.dialogClass);
290 options.classes['ui-dialog'] = dialogClass.join(' ');
291 delete options.dialogClass;
294 // Add jQuery UI classes to elements in case developers target them
296 for (var k in classesMap) {
297 this.$element.find('.' + classesMap[k]).addClass(k);
302 'beforeClose', 'close',
304 'drag', 'dragStart', 'dragStop',
307 'resize', 'resizeStart', 'resizeStop'
309 for (i = 0, l = events.length; i < l; i++) {
310 var event = events[i].toLowerCase();
311 if (options[event] === void 0 || typeof options[event] !== 'function') continue;
312 this.$element.on('dialog' + event, options[event]);
315 // Support title attribute on the modal.
317 if ((options.title === null || options.title === void 0) && (title = this.$element.attr('title'))) {
318 options.title = title;
321 // Handle the reset of the options.
322 for (var name in options) {
323 if (!options.hasOwnProperty(name) || options[name] === void 0) continue;
327 Bootstrap.unsupported('option', name, options.appendTo);
331 mappedOptions.show = !!options.autoOpen;
334 // This is really a drupal.dialog option, not jQuery UI.
336 dialogOptions.autoResize = !!options.autoResize;
340 dialogOptions.buttons = options.buttons;
344 dialogOptions.classes = options.classes;
345 for (var key in options.classes) {
346 if (options.classes.hasOwnProperty(key) && classesMap[key] !== void 0) {
347 // Run through Attributes to sanitize classes.
348 var attributes = Attributes.create().addClass(options.classes[key]).toPlainObject();
349 var selector = '.' + classesMap[key];
350 this.$element.find(selector).addClass(attributes['class']);
355 case 'closeOnEscape':
356 dialogOptions.closeOnEscape = options.closeOnEscape;
357 mappedOptions.keyboard = !!options.closeOnEscape;
358 this.$close[options.closeOnEscape ? 'show' : 'hide']();
359 if (!options.closeOnEscape && options.modal) {
360 mappedOptions.backdrop = options.modal = 'static';
365 Bootstrap.unsupported('option', name, options.closeText);
369 dialogOptions.draggable = options.draggable;
372 handle: '.modal-header',
373 drag: relayEvent(this.$element, 'dialogdrag'),
374 start: relayEvent(this.$element, 'dialogdragstart'),
375 end: relayEvent(this.$element, 'dialogdragend')
377 .draggable(options.draggable ? 'enable' : 'disable');
381 if (options.hide === false || options.hide === true) {
382 this.$element[options.hide ? 'addClass' : 'removeClass']('fade');
383 mappedOptions.animation = options.hide;
386 Bootstrap.unsupported('option', name + ' (complex animation)', options.hide);
391 mappedOptions.backdrop = options.modal;
392 dialogOptions.modal = !!options.modal;
394 // If not a modal and no initial position, center it.
395 if (!options.modal && !options.position) {
396 this.position({ my: 'center', of: window });
401 dialogOptions.position = options.position;
402 this.position(options.position);
405 // Resizable support (must initialize first).
407 dialogOptions.resizeable = options.resizable;
410 resize: relayEvent(this.$element, 'dialogresize'),
411 start: relayEvent(this.$element, 'dialogresizestart'),
412 end: relayEvent(this.$element, 'dialogresizeend')
414 .resizable(options.resizable ? 'enable' : 'disable');
418 if (options.show === false || options.show === true) {
419 this.$element[options.show ? 'addClass' : 'removeClass']('fade');
420 mappedOptions.animation = options.show;
423 Bootstrap.unsupported('option', name + ' (complex animation)', options.show);
428 dialogOptions.title = options.title;
429 this.$dialog.find('.modal-title').text(options.title);
435 // Add the supported dialog options to the mapped options.
436 mappedOptions.dialogOptions = dialogOptions;
438 return mappedOptions;
442 * Handler for $.fn.dialog('moveToTop').
444 moveToTop: function () {
445 Bootstrap.unsupported('method', 'moveToTop', arguments);
449 * Handler for $.fn.dialog('option').
451 option: function () {
452 var clone = {options: $.extend({}, this.options)};
454 // Apply the parent option method to the clone of current options.
455 this.super.apply(clone, arguments);
457 // Merge in the cloned mapped options.
458 $.extend(true, this.options, this.mapDialogOptions(clone.options));
461 this.createButtons();
464 position: function(position) {
465 // Reset modal styling.
472 // Position the modal.
473 this.$element.position(position);
477 * Handler for $.fn.dialog('open').
480 this.show.apply(this, arguments);
484 * Handler for $.fn.dialog('widget').
486 widget: function () {
487 return this.$element;
494 * Extend Drupal theming functions.
496 $.extend(Drupal.theme, /** @lend Drupal.theme */ {
499 * Renders a jQuery UI Dialog compatible button element.
501 * @param {Object} button
502 * The button object passed in the dialog options.
505 * The modal dialog button markup.
507 * @see http://api.jqueryui.com/dialog/#option-buttons
508 * @see http://api.jqueryui.com/button/
510 bootstrapModalDialogButton: function (button) {
511 var attributes = Attributes.create();
514 var iconPosition = button.iconPosition || 'beginning';
515 iconPosition = (iconPosition === 'end' && !rtl) || (iconPosition === 'beginning' && rtl) ? 'after' : 'before';
517 // Handle Bootstrap icons differently.
518 if (button.bootstrapIcon) {
519 icon = Drupal.theme('icon', 'bootstrap', button.icon);
521 // Otherwise, assume it's a jQuery UI icon.
522 // @todo Map jQuery UI icons to Bootstrap icons?
523 else if (button.icon) {
524 var iconAttributes = Attributes.create()
525 .addClass(['ui-icon', button.icon])
526 .set('aria-hidden', 'true');
527 icon = '<span' + iconAttributes + '></span>';
530 // Label. Note: jQuery UI dialog has an inconsistency where it uses
531 // "text" instead of "label", so both need to be supported.
532 var value = button.label || button.text;
535 if (icon && ((button.showLabel !== void 0 && !button.showLabel) || (button.text !== void 0 && !button.text))) {
536 value = '<span' + Attributes.create().addClass('sr-only') + '>' + value + '</span>';
538 attributes.set('value', iconPosition === 'before' ? icon + value : value + icon);
541 attributes[button.disabled ? 'set' :'remove']('disabled', 'disabled');
543 if (button.classes) {
544 attributes.addClass(Object.keys(button.classes).map(function(key) { return button.classes[key]; }));
546 if (button['class']) {
547 attributes.addClass(button['class']);
549 if (button.primary) {
550 attributes.addClass('btn-primary');
553 return Drupal.theme('button', attributes);
561 })(window.jQuery, window.Drupal, window.Drupal.bootstrap, window.Attributes, window.drupalSettings);