1/** 2 * @file 3 * Bootstrap Modals. 4 * 5 * @param {jQuery} $ 6 * @param {Drupal} Drupal 7 * @param {Drupal.bootstrap} Bootstrap 8 * @param {Attributes} Attributes 9 * @param {drupalSettings} drupalSettings 10 */ 11(function ($, Drupal, Bootstrap, Attributes, drupalSettings) { 12 'use strict'; 13 14 /** 15 * Only process this once. 16 */ 17 Bootstrap.once('modal.jquery.ui.bridge', function (settings) { 18 // RTL support. 19 var rtl = document.documentElement.getAttribute('dir').toLowerCase() === 'rtl'; 20 21 // Override drupal.dialog button classes. This must be done on DOM ready 22 // since core/drupal.dialog technically depends on this file and has not 23 // yet set their default settings. 24 $(function () { 25 drupalSettings.dialog.buttonClass = 'btn'; 26 drupalSettings.dialog.buttonPrimaryClass = 'btn-primary'; 27 }); 28 29 // Create the "dialog" plugin bridge. 30 Bootstrap.Dialog.Bridge = function (options) { 31 var args = Array.prototype.slice.call(arguments); 32 var $element = $(this); 33 var type = options && options.dialogType || $element[0].dialogType || 'modal'; 34 35 $element[0].dialogType = type; 36 37 var handler = Bootstrap.Dialog.Handler.get(type); 38 39 // When only options are passed, jQuery UI dialog treats this like a 40 // initialization method. Destroy any existing Bootstrap modal and 41 // recreate it using the contents of the dialog HTML. 42 if (args.length === 1 && typeof options === 'object') { 43 this.each(function () { 44 handler.ensureModalStructure(this, options); 45 }); 46 47 // Proxy to the Bootstrap Modal plugin, indicating that this is a 48 // jQuery UI dialog bridge. 49 return handler.invoke(this, { 50 dialogOptions: options, 51 jQueryUiBridge: true 52 }); 53 } 54 55 // Otherwise, proxy all arguments to the Bootstrap Modal plugin. 56 var ret; 57 try { 58 ret = handler.invoke.apply(handler, [this].concat(args)); 59 } 60 catch (e) { 61 Bootstrap.warn(e); 62 } 63 64 // If just one element and there was a result returned for the option passed, 65 // then return the result. Otherwise, just return the jQuery object. 66 return this.length === 1 && ret !== void 0 ? ret : this; 67 }; 68 69 // Assign the jQuery "dialog" plugin to use to the bridge. 70 Bootstrap.createPlugin('dialog', Bootstrap.Dialog.Bridge); 71 72 // Create the "modal" plugin bridge. 73 Bootstrap.Modal.Bridge = function () { 74 var Modal = this; 75 76 return { 77 DEFAULTS: { 78 // By default, this option is disabled. It's only flagged when a modal 79 // was created using $.fn.dialog above. 80 jQueryUiBridge: false 81 }, 82 prototype: { 83 84 /** 85 * Handler for $.fn.dialog('close'). 86 */ 87 close: function () { 88 var _this = this; 89 90 this.hide.apply(this, arguments); 91 92 // For some reason (likely due to the transition event not being 93 // registered properly), the backdrop doesn't always get removed 94 // after the above "hide" method is invoked . Instead, ensure the 95 // backdrop is removed after the transition duration by manually 96 // invoking the internal "hideModal" method shortly thereafter. 97 setTimeout(function () { 98 if (!_this.isShown && _this.$backdrop) { 99 _this.hideModal(); 100 } 101 }, (Modal.TRANSITION_DURATION !== void 0 ? Modal.TRANSITION_DURATION : 300) + 10); 102 }, 103 104 /** 105 * Creates any necessary buttons from dialog options. 106 */ 107 createButtons: function () { 108 var handler = Bootstrap.Dialog.Handler.get(this.$element); 109 this.$footer.find(handler.selectors.buttons).remove(); 110 111 // jQuery UI supports both objects and arrays. Unfortunately 112 // developers have misunderstood and abused this by simply placing 113 // the objects that should be in an array inside an object with 114 // arbitrary keys (likely to target specific buttons as a hack). 115 var buttons = this.options.dialogOptions && this.options.dialogOptions.buttons || []; 116 if (!Array.isArray(buttons)) { 117 var array = []; 118 for (var k in buttons) {
119 // Support the proper object values: label => click callback. 120 if (typeof buttons[k] === 'function') { 121 array.push({ 122 label: k, 123 click: buttons[k], 124 }); 125 } 126 // Support nested objects, but log a warning. 127 else if (buttons[k].text || buttons[k].label) { 128 Bootstrap.warn('Malformed jQuery UI dialog button: @key. The button object should be inside an array.', { 129 '@key': k 130 }); 131 array.push(buttons[k]); 132 } 133 else { 134 Bootstrap.unsupported('button', k, buttons[k]); 135 } 136 } 137 buttons = array; 138 } 139 140 if (buttons.length) { 141 var $buttons = $('<div class="modal-buttons"/>').appendTo(this.$footer); 142 for (var i = 0, l = buttons.length; i < l; i++) { 143 var button = buttons[i]; 144 var $button = $(Drupal.theme('bootstrapModalDialogButton', button)); 145 146 // Invoke the "create" method for jQuery UI buttons. 147 if (typeof button.create === 'function') { 148 button.create.call($button[0]); 149 } 150 151 // Bind the "click" method for jQuery UI buttons to the modal. 152 if (typeof button.click === 'function') { 153 $button.on('click', button.click.bind(this.$element)); 154 } 155 156 $buttons.append($button); 157 } 158 } 159 160 // Toggle footer visibility based on whether it has child elements. 161 this.$footer[this.$footer.children()[0] ? 'show' : 'hide'](); 162 }, 163 164 /** 165 * Initializes the Bootstrap Modal. 166 */ 167 init: function () { 168 var handler = Bootstrap.Dialog.Handler.get(this.$element); 169 if (!this.$dialog) { 170 this.$dialog = this.$element.find(handler.selectors.dialog); 171 } 172 this.$dialog.addClass('js-drupal-dialog'); 173 174 if (!this.$header) { 175 this.$header = this.$dialog.find(handler.selectors.header); 176 } 177 if (!this.$title) { 178 this.$title = this.$dialog.find(handler.selectors.title); 179 } 180 if (!this.$close) { 181 this.$close = this.$header.find(handler.selectors.close); 182 } 183 if (!this.$footer) { 184 this.$footer = this.$dialog.find(handler.selectors.footer); 185 } 186 if (!this.$content) { 187 this.$content = this.$dialog.find(handler.selectors.content); 188 } 189 if (!this.$dialogBody) { 190 this.$dialogBody = this.$dialog.find(handler.selectors.body); 191 } 192 193 // Relay necessary events. 194 if (this.options.jQueryUiBridge) { 195 this.$element.on('hide.bs.modal', Bootstrap.relayEvent(this.$element, 'dialogbeforeclose', false)); 196 this.$element.on('hidden.bs.modal', Bootstrap.relayEvent(this.$element, 'dialogclose', false)); 197 this.$element.on('show.bs.modal', Bootstrap.relayEvent(this.$element, 'dialogcreate', false)); 198 this.$element.on('shown.bs.modal', Bootstrap.relayEvent(this.$element, 'dialogopen', false)); 199 } 200 201 // Create a footer if one doesn't exist. 202 // This is necessary in case dialog.ajax.js decides to add buttons. 203 if (!this.$footer[0]) { 204 this.$footer = handler.theme('footer', {}, true).insertAfter(this.$dialogBody); 205 } 206 207 // Map the initial options. 208 $.extend(true, this.options, this.mapDialogOptions(this.options)); 209 210 // Update buttons. 211 this.createButtons(); 212 213 // Now call the parent init method. 214 this.super(); 215 216 // Handle autoResize option (this is a drupal.dialog option). 217 if (this.options.dialogOptions && this.options.dialogOptions.autoResize && this.options.dialogOptions.position) { 218 this.position(this.options.dialogOptions.position); 219 } 220 221 // If show is enabled and currently not shown, show it. 222 if (this.options.jQueryUiBridge && this.options.show && !this.isShown) { 223 this.show(); 224 } 225 }, 226 227 /** 228 * Handler for $.fn.dialog('instance'). 229 */ 230 instance: function () { 231 Bootstrap.unsupported('method', 'instance', arguments); 232 }, 233 234 /** 235 * Handler for $.fn.dialog('isOpen'). 236 */ 237 isOpen: function () { 238 return !!this.isShown; 239 }, 240 241 /** 242 * Maps dialog options to the modal. 243 * 244 * @param {Object} options 245 * The options to map. 246 */ 247 mapDialogOptions: function (options) { 248 // Retrieve the dialog handler for this type. 249 var handler = Bootstrap.Dialog.Handler.get(this.$element); 250 251 var mappedOptions = {}; 252 var dialogOptions = options.dialogOptions || {}; 253 254 // Remove any existing dialog options. 255 delete options.dialogOptions; 256 257 // Separate Bootstrap modal options from jQuery UI dialog options. 258 for (var k in options) { 259 if (Modal.DEFAULTS.hasOwnProperty(k)) { 260 mappedOptions[k] = options[k]; 261 } 262 else { 263 dialogOptions[k] = options[k]; 264 } 265 } 266 267 268 // Handle CSS properties. 269 var cssUnitRegExp = /^([+-]?(?:\d+|\d*\.\d+))([a-z]*|%)?$/; 270 var parseCssUnit = function (value, defaultUnit) { 271 var parts = ('' + value).match(cssUnitRegExp); 272 return parts && parts[1] !== void 0 ? parts[1] + (parts[2] || defaultUnit || 'px') : null; 273 }; 274 var styles = {}; 275 var cssProperties = ['height', 'maxHeight', 'maxWidth', 'minHeight', 'minWidth', 'width']; 276 for (var i = 0, l = cssProperties.length; i < l; i++) { 277 var prop = cssProperties[i]; 278 if (dialogOptions[prop] !== void 0) { 279 var value = parseCssUnit(dialogOptions[prop]); 280 if (value) { 281 styles[prop] = value; 282 283 // If there's a defined height of some kind, enforce the modal 284 // to use flex (on modern browsers). This will ensure that 285 // the core autoResize calculations don't cause the content 286 // to overflow. 287 if (dialogOptions.autoResize && (prop === 'height' || prop === 'maxHeight')) { 288 styles.display = 'flex'; 289 styles.flexDirection = 'column'; 290 this.$dialogBody.css('overflow', 'scroll'); 291 } 292 } 293 } 294 } 295
296 // Apply mapped CSS styles to the modal-content container. 297 this.$content.css(styles); 298 299 // Handle deprecated "dialogClass" option by merging it with "classes". 300 var classesMap = { 301 'ui-dialog': 'modal-content', 302 'ui-dialog-titlebar': 'modal-header', 303 'ui-dialog-title': 'modal-title', 304 'ui-dialog-titlebar-close': 'close', 305 'ui-dialog-content': 'modal-body', 306 'ui-dialog-buttonpane': 'modal-footer' 307 }; 308 if (dialogOptions.dialogClass) { 309 if (dialogOptions.classes === void 0) { 310 dialogOptions.classes = {}; 311 } 312 if (dialogOptions.classes['ui-dialog'] === void 0) { 313 dialogOptions.classes['ui-dialog'] = ''; 314 } 315 var dialogClass = dialogOptions.classes['ui-dialog'].split(' '); 316 dialogClass.push(dialogOptions.dialogClass); 317 dialogOptions.classes['ui-dialog'] = dialogClass.join(' '); 318 delete dialogOptions.dialogClass; 319 } 320 321 // Add jQuery UI classes to elements in case developers target them 322 // in callbacks. 323 for (k in classesMap) { 324 this.$element.find('.' + classesMap[k]).addClass(k); 325 } 326 327 // Bind events. 328 var events = [ 329 'beforeClose', 'close', 330 'create', 331 'drag', 'dragStart', 'dragStop', 332 'focus', 333 'open', 334 'resize', 'resizeStart', 'resizeStop' 335 ]; 336 for (i = 0, l = events.length; i < l; i++) { 337 var event = events[i].toLowerCase(); 338 if (dialogOptions[event] === void 0 || typeof dialogOptions[event] !== 'function') continue; 339 this.$element.on('dialog' + event, dialogOptions[event]); 340 } 341 342 // Support title attribute on the modal. 343 var title; 344 if ((dialogOptions.title === null || dialogOptions.title === void 0) && (title = this.$element.attr('title'))) { 345 dialogOptions.title = title; 346 } 347 348 // Handle the reset of the options. 349 for (var name in dialogOptions) { 350 if (!dialogOptions.hasOwnProperty(name) || dialogOptions[name] === void 0) continue; 351 352 switch (name) { 353 case 'appendTo': 354 Bootstrap.unsupported('option', name, dialogOptions.appendTo); 355 break; 356 357 case 'autoOpen': 358 mappedOptions.show = dialogOptions.show = !!dialogOptions.autoOpen; 359 break; 360 361 case 'classes': 362 if (dialogOptions.classes) { 363 for (var key in dialogOptions.classes) { 364 if (dialogOptions.classes.hasOwnProperty(key) && classesMap[key] !== void 0) { 365 // Run through Attributes to sanitize classes. 366 var attributes = Attributes.create().addClass(dialogOptions.classes[key]).toPlainObject(); 367 var selector = '.' + classesMap[key]; 368 this.$element.find(selector).addClass(attributes['class']); 369 } 370 } 371 } 372 break; 373 374 case 'closeOnEscape': 375 mappedOptions.keyboard = !!dialogOptions.closeOnEscape; 376 if (!dialogOptions.closeOnEscape && dialogOptions.modal) { 377 mappedOptions.backdrop = 'static'; 378 } 379 break; 380 381 case 'closeText': 382 Bootstrap.unsupported('option', name, dialogOptions.closeText); 383 break; 384 385 case 'draggable': 386 this.$content 387 .draggable({ 388 handle: handler.selectors.header, 389 drag: Bootstrap.relayEvent(this.$element, 'dialogdrag'), 390 start: Bootstrap.relayEvent(this.$element, 'dialogdragstart'), 391 end: Bootstrap.relayEvent(this.$element, 'dialogdragend') 392 }) 393 .draggable(dialogOptions.draggable ? 'enable' : 'disable'); 394 break; 395 396 case 'hide': 397 if (dialogOptions.hide === false || dialogOptions.hide === true) { 398 this.$element[dialogOptions.hide ? 'addClass' : 'removeClass']('fade'); 399 mappedOptions.animation = dialogOptions.hide; 400 } 401 else { 402 Bootstrap.unsupported('option', name + ' (complex animation)', dialogOptions.hide); 403 } 404 break; 405 406 case 'modal': 407 if (!dialogOptions.closeOnEscape && dialogOptions.modal) { 408 mappedOptions.backdrop = 'static'; 409 } 410 else { 411 mappedOptions.backdrop = dialogOptions.modal; 412 } 413
414 // If not a modal and no initial position, center it. 415 if (!dialogOptions.modal && !dialogOptions.position) { 416 this.position({ my: 'center', of: window }); 417 } 418 break; 419 420 case 'position': 421 this.position(dialogOptions.position); 422 break; 423 424 // Resizable support (must initialize first). 425 case 'resizable': 426 this.$content 427 .resizable({ 428 resize: Bootstrap.relayEvent(this.$element, 'dialogresize'), 429 start: Bootstrap.relayEvent(this.$element, 'dialogresizestart'), 430 end: Bootstrap.relayEvent(this.$element, 'dialogresizeend') 431 }) 432 .resizable(dialogOptions.resizable ? 'enable' : 'disable'); 433 break; 434 435 case 'show': 436 if (dialogOptions.show === false || dialogOptions.show === true) { 437 this.$element[dialogOptions.show ? 'addClass' : 'removeClass']('fade'); 438 mappedOptions.animation = dialogOptions.show; 439 } 440 else { 441 Bootstrap.unsupported('option', name + ' (complex animation)', dialogOptions.show); 442 } 443 break; 444 445 case 'title': 446 this.$title.text(dialogOptions.title); 447 break; 448 449 } 450 } 451 452 // Add the supported dialog options to the mapped options. 453 mappedOptions.dialogOptions = dialogOptions; 454 455 return mappedOptions; 456 }, 457 458 /** 459 * Handler for $.fn.dialog('moveToTop'). 460 */ 461 moveToTop: function () { 462 Bootstrap.unsupported('method', 'moveToTop', arguments); 463 }, 464 465 /** 466 * Handler for $.fn.dialog('option'). 467 */ 468 option: function () { 469 var clone = {options: {}}; 470 471 // Apply the parent option method to the clone of current options. 472 this.super.apply(clone, arguments); 473 474 // Merge in the cloned mapped options. 475 $.extend(true, this.options, this.mapDialogOptions(clone.options)); 476 477 // Update buttons. 478 this.createButtons(); 479 }, 480 481 position: function(position) { 482 // Reset modal styling. 483 this.$element.css({ 484 bottom: 'initial', 485 overflow: 'visible', 486 right: 'initial' 487 }); 488 489 // Position the modal. 490 this.$element.position(position); 491 }, 492 493 /** 494 * Handler for $.fn.dialog('open'). 495 */ 496 open: function () { 497 this.show.apply(this, arguments); 498 }, 499 500 /** 501 * Handler for $.fn.dialog('widget'). 502 */ 503 widget: function () { 504 return this.$element; 505 } 506 } 507 }; 508 }; 509 510 // Extend the Bootstrap Modal plugin constructor class. 511 Bootstrap.extendPlugin('modal', Bootstrap.Modal.Bridge); 512 513 // Register default core dialog type handlers. 514 Bootstrap.Dialog.Handler.register('dialog'); 515 Bootstrap.Dialog.Handler.register('modal'); 516 517 /** 518 * Extend Drupal theming functions. 519 */ 520 $.extend(Drupal.theme, /** @lend Drupal.theme */ { 521 522 /** 523 * Renders a jQuery UI Dialog compatible button element. 524 * 525 * @param {Object} button 526 * The button object passed in the dialog options. 527 * 528 * @return {String} 529 * The modal dialog button markup. 530 * 531 * @see http://api.jqueryui.com/dialog/#option-buttons 532 * @see http://api.jqueryui.com/button/ 533 */ 534 bootstrapModalDialogButton: function (button) { 535 var attributes = Attributes.create(); 536 537 var icon = ''; 538 var iconPosition = button.iconPosition || 'beginning'; 539 iconPosition = (iconPosition === 'end' && !rtl) || (iconPosition === 'beginning' && rtl) ? 'after' : 'before'; 540 541 // Handle Bootstrap icons differently. 542 if (button.bootstrapIcon) { 543 icon = Drupal.theme('icon', 'bootstrap', button.icon); 544 } 545 // Otherwise, assume it's a jQuery UI icon. 546 // @todo Map jQuery UI icons to Bootstrap icons? 547 else if (button.icon) { 548 var iconAttributes = Attributes.create() 549 .addClass(['ui-icon', button.icon]) 550 .set('aria-hidden', 'true'); 551 icon = '<span' + iconAttributes + '></span>'; 552 } 553 554 // Label. Note: jQuery UI dialog has an inconsistency where it uses 555 // "text" instead of "label", so both need to be supported. 556 var value = button.label || button.text; 557 558 // Show/hide label. 559 if (icon && ((button.showLabel !== void 0 && !button.showLabel) || (button.text !== void 0 && !button.text))) { 560 value = '<span' + Attributes.create().addClass('sr-only') + '>
560' + value + '</span>'; 561 } 562 attributes.set('value', iconPosition === 'before' ? icon + value : value + icon); 563 564 // Handle disabled. 565 attributes[button.disabled ? 'set' :'remove']('disabled', 'disabled'); 566 567 if (button.classes) { 568 attributes.addClass(Object.keys(button.classes).map(function(key) { return button.classes[key]; })); 569 } 570 if (button['class']) { 571 attributes.addClass(button['class']); 572 } 573 if (button.primary) { 574 attributes.addClass('btn-primary'); 575 } 576 577 return Drupal.theme('button', attributes); 578 } 579 580 }); 581 582 }); 583 584 585})(window.jQuery, window.Drupal, window.Drupal.bootstrap, window.Attributes, window.drupalSettings);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.