586 lines
22 KiB
JavaScript
586 lines
22 KiB
JavaScript
/**
|
|
* @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 = $('<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));
|
|
|
|
// 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 = '<span' + iconAttributes + '></span>';
|
|
}
|
|
|
|
// 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 = '<span' + Attributes.create().addClass('sr-only') + '>' + value + '</span>';
|
|
}
|
|
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);
|