cd9509251baa9c321cd8951d900ff510e50ad44d
[yaffs-website] / web / themes / contrib / bootstrap / js / modal.jquery.ui.bridge.js
1 /**
2  * @file
3  * Bootstrap Modals.
4  */
5 (function ($, Drupal, Bootstrap, Attributes, drupalSettings) {
6   'use strict';
7
8   /**
9    * Only process this once.
10    */
11   Bootstrap.once('modal.jquery.ui.bridge', function (settings) {
12     // RTL support.
13     var rtl = document.documentElement.getAttribute('dir').toLowerCase() === 'rtl';
14
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.
18     $(function () {
19       drupalSettings.dialog.buttonClass = 'btn';
20       drupalSettings.dialog.buttonPrimaryClass = 'btn-primary';
21     });
22
23     var relayEvent = function ($element, name, stopPropagation) {
24       return function (e) {
25         if (stopPropagation === void 0 || stopPropagation) {
26           e.stopPropagation();
27         }
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('.');
33         e.type = type;
34         $element.trigger(e);
35       };
36     };
37
38     /**
39      * Proxy $.fn.dialog to $.fn.modal.
40      */
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.
54           var $this = $(this);
55
56           // Create a new modal to get a complete template.
57           var $modal = $(Drupal.theme('bootstrapModal', {attributes: Attributes.create(this).remove('style')}));
58
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();
66
67           // Destroy any existing Bootstrap Modal data that may have been saved.
68           $this.removeData('bs.modal');
69
70           // Set the attributes of the dialog to that of the newly created modal.
71           $this.attr(Attributes.create($modal).toPlainObject());
72
73           // Append the newly created modal markup.
74           $this.append($modal.html());
75
76           // Move the existing HTML into the modal markup that was just appended.
77           $this.find('.modal-body').append($existing);
78         });
79
80         // Indicate that the modal is a jQuery UI dialog bridge.
81         options.jQueryUiBridge = true;
82
83         // Proxy just the options to the Bootstrap Modal plugin.
84         return $.fn.modal.apply(this, [options]);
85       }
86
87       // Otherwise, proxy all arguments to the Bootstrap Modal plugin.
88       return $.fn.modal.apply(this, arguments);
89     });
90
91     /**
92      * Extend the Bootstrap Modal plugin constructor class.
93      */
94     Bootstrap.extendPlugin('modal', function () {
95       var Modal = this;
96
97       return {
98         DEFAULTS: {
99           // By default, this option is disabled. It's only flagged when a modal
100           // was created using $.fn.dialog above.
101           jQueryUiBridge: false
102         },
103         prototype: {
104
105           /**
106            * Handler for $.fn.dialog('close').
107            */
108           close: function () {
109             var _this = this;
110
111             this.hide.apply(this, arguments);
112
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) {
120                 _this.hideModal();
121               }
122             }, (Modal.TRANSITION_DURATION !== void 0 ? Modal.TRANSITION_DURATION : 300) + 10);
123           },
124
125           /**
126            * Creates any necessary buttons from dialog options.
127            */
128           createButtons: function () {
129             this.$footer.find('.modal-buttons').remove();
130
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)) {
137               var array = [];
138               for (var k in buttons) {
139                 // Support the proper object values: label => click callback.
140                 if (typeof buttons[k] === 'function') {
141                   array.push({
142                     label: k,
143                     click: buttons[k],
144                   });
145                 }
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.', {
149                     '@key': k
150                   });
151                   array.push(buttons[k]);
152                 }
153                 else {
154                   Bootstrap.unsupported('button', k, buttons[k]);
155                 }
156               }
157               buttons = array;
158             }
159
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));
165
166                 // Invoke the "create" method for jQuery UI buttons.
167                 if (typeof button.create === 'function') {
168                   button.create.call($button[0]);
169                 }
170
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));
174                 }
175
176                 $buttons.append($button);
177               }
178             }
179
180             // Toggle footer visibility based on whether it has child elements.
181             this.$footer[this.$footer.children()[0] ? 'show' : 'hide']();
182           },
183
184           /**
185            * Initializes the Bootstrap Modal.
186            */
187           init: function () {
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));
194             }
195
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);
200             }
201
202             // Now call the parent init method.
203             this.super();
204
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);
208             }
209
210             // If show is enabled and currently not shown, show it.
211             if (this.options.show && !this.isShown) {
212               this.show();
213             }
214           },
215
216           /**
217            * Handler for $.fn.dialog('instance').
218            */
219           instance: function () {
220             Bootstrap.unsupported('method', 'instance', arguments);
221           },
222
223           /**
224            * Handler for $.fn.dialog('isOpen').
225            */
226           isOpen: function () {
227             return !!this.isShown;
228           },
229
230           /**
231            * Maps dialog options to the modal.
232            *
233            * @param {Object} options
234            *   The options to map.
235            */
236           mapDialogOptions: function (options) {
237             var dialogOptions = {};
238             var mappedOptions = {};
239
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;
245             };
246             var styles = {};
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]);
252                 if (value) {
253                   dialogOptions[prop] = value;
254                   styles[prop] = value;
255
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
259                   // to overflow.
260                   if (options.autoResize && (prop === 'height' || prop === 'maxHeight')) {
261                     styles.display = 'flex';
262                     styles.flexDirection = 'column';
263                     this.$dialogBody.css('overflow', 'scroll');
264                   }
265                 }
266               }
267             }
268
269             // Apply mapped CSS styles to the modal-content container.
270             this.$content.css(styles);
271
272             // Handle deprecated "dialogClass" option by merging it with "classes".
273             var classesMap = {
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'
280             };
281             if (options.dialogClass) {
282               if (options.classes === void 0) {
283                 options.classes = {};
284               }
285               if (options.classes['ui-dialog'] === void 0) {
286                 options.classes['ui-dialog'] = '';
287               }
288               var dialogClass = options.classes['ui-dialog'].split(' ');
289               dialogClass.push(options.dialogClass);
290               options.classes['ui-dialog'] = dialogClass.join(' ');
291               delete options.dialogClass;
292             }
293
294             // Add jQuery UI classes to elements in case developers target them
295             // in callbacks.
296             for (var k in classesMap) {
297               this.$element.find('.' + classesMap[k]).addClass(k);
298             }
299
300             // Bind events.
301             var events = [
302               'beforeClose', 'close',
303               'create',
304               'drag', 'dragStart', 'dragStop',
305               'focus',
306               'open',
307               'resize', 'resizeStart', 'resizeStop'
308             ];
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]);
313             }
314
315             // Support title attribute on the modal.
316             var title;
317             if ((options.title === null || options.title === void 0) && (title = this.$element.attr('title'))) {
318               options.title = title;
319             }
320
321             // Handle the reset of the options.
322             for (var name in options) {
323               if (!options.hasOwnProperty(name) || options[name] === void 0) continue;
324
325               switch (name) {
326                 case 'appendTo':
327                   Bootstrap.unsupported('option', name, options.appendTo);
328                   break;
329
330                 case 'autoOpen':
331                   mappedOptions.show = !!options.autoOpen;
332                   break;
333
334                 // This is really a drupal.dialog option, not jQuery UI.
335                 case 'autoResize':
336                   dialogOptions.autoResize = !!options.autoResize;
337                   break;
338
339                 case 'buttons':
340                   dialogOptions.buttons = options.buttons;
341                   break;
342
343                 case 'classes':
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']);
351                     }
352                   }
353                   break;
354
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';
361                   }
362                   break;
363
364                 case 'closeText':
365                   Bootstrap.unsupported('option', name, options.closeText);
366                   break;
367
368                 case 'draggable':
369                   dialogOptions.draggable = options.draggable;
370                   this.$content
371                     .draggable({
372                       handle: '.modal-header',
373                       drag: relayEvent(this.$element, 'dialogdrag'),
374                       start: relayEvent(this.$element, 'dialogdragstart'),
375                       end: relayEvent(this.$element, 'dialogdragend')
376                     })
377                     .draggable(options.draggable ? 'enable' : 'disable');
378                   break;
379
380                 case 'hide':
381                   if (options.hide === false || options.hide === true) {
382                     this.$element[options.hide ? 'addClass' : 'removeClass']('fade');
383                     mappedOptions.animation = options.hide;
384                   }
385                   else {
386                     Bootstrap.unsupported('option', name + ' (complex animation)', options.hide);
387                   }
388                   break;
389
390                 case 'modal':
391                   mappedOptions.backdrop = options.modal;
392                   dialogOptions.modal = !!options.modal;
393
394                   // If not a modal and no initial position, center it.
395                   if (!options.modal && !options.position) {
396                     this.position({ my: 'center', of: window });
397                   }
398                   break;
399
400                 case 'position':
401                   dialogOptions.position = options.position;
402                   this.position(options.position);
403                   break;
404
405                 // Resizable support (must initialize first).
406                 case 'resizable':
407                   dialogOptions.resizeable = options.resizable;
408                   this.$content
409                     .resizable({
410                       resize: relayEvent(this.$element, 'dialogresize'),
411                       start: relayEvent(this.$element, 'dialogresizestart'),
412                       end: relayEvent(this.$element, 'dialogresizeend')
413                     })
414                     .resizable(options.resizable ? 'enable' : 'disable');
415                   break;
416
417                 case 'show':
418                   if (options.show === false || options.show === true) {
419                     this.$element[options.show ? 'addClass' : 'removeClass']('fade');
420                     mappedOptions.animation = options.show;
421                   }
422                   else {
423                     Bootstrap.unsupported('option', name + ' (complex animation)', options.show);
424                   }
425                   break;
426
427                 case 'title':
428                   dialogOptions.title = options.title;
429                   this.$dialog.find('.modal-title').text(options.title);
430                   break;
431
432               }
433             }
434
435             // Add the supported dialog options to the mapped options.
436             mappedOptions.dialogOptions = dialogOptions;
437
438             return mappedOptions;
439           },
440
441           /**
442            * Handler for $.fn.dialog('moveToTop').
443            */
444           moveToTop: function () {
445             Bootstrap.unsupported('method', 'moveToTop', arguments);
446           },
447
448           /**
449            * Handler for $.fn.dialog('option').
450            */
451           option: function () {
452             var clone = {options: $.extend({}, this.options)};
453
454             // Apply the parent option method to the clone of current options.
455             this.super.apply(clone, arguments);
456
457             // Merge in the cloned mapped options.
458             $.extend(true, this.options, this.mapDialogOptions(clone.options));
459
460             // Update buttons.
461             this.createButtons();
462           },
463
464           position: function(position) {
465             // Reset modal styling.
466             this.$element.css({
467               bottom: 'initial',
468               overflow: 'visible',
469               right: 'initial'
470             });
471
472             // Position the modal.
473             this.$element.position(position);
474           },
475
476           /**
477            * Handler for $.fn.dialog('open').
478            */
479           open: function () {
480             this.show.apply(this, arguments);
481           },
482
483           /**
484            * Handler for $.fn.dialog('widget').
485            */
486           widget: function () {
487             return this.$element;
488           }
489         }
490       };
491     });
492
493     /**
494      * Extend Drupal theming functions.
495      */
496     $.extend(Drupal.theme, /** @lend Drupal.theme */ {
497
498       /**
499        * Renders a jQuery UI Dialog compatible button element.
500        *
501        * @param {Object} button
502        *   The button object passed in the dialog options.
503        *
504        * @return {String}
505        *   The modal dialog button markup.
506        *
507        * @see http://api.jqueryui.com/dialog/#option-buttons
508        * @see http://api.jqueryui.com/button/
509        */
510       bootstrapModalDialogButton: function (button) {
511         var attributes = Attributes.create();
512
513         var icon = '';
514         var iconPosition = button.iconPosition || 'beginning';
515         iconPosition = (iconPosition === 'end' && !rtl) || (iconPosition === 'beginning' && rtl) ? 'after' : 'before';
516
517         // Handle Bootstrap icons differently.
518         if (button.bootstrapIcon) {
519           icon = Drupal.theme('icon', 'bootstrap', button.icon);
520         }
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>';
528         }
529
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;
533
534         // Show/hide label.
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>';
537         }
538         attributes.set('value', iconPosition === 'before' ? icon + value : value + icon);
539
540         // Handle disabled.
541         attributes[button.disabled ? 'set' :'remove']('disabled', 'disabled');
542
543         if (button.classes) {
544           attributes.addClass(Object.keys(button.classes).map(function(key) { return button.classes[key]; }));
545         }
546         if (button['class']) {
547           attributes.addClass(button['class']);
548         }
549         if (button.primary) {
550           attributes.addClass('btn-primary');
551         }
552
553         return Drupal.theme('button', attributes);
554       }
555
556     });
557
558   });
559
560
561 })(window.jQuery, window.Drupal, window.Drupal.bootstrap, window.Attributes, window.drupalSettings);