442c5b0f052cfafc30902d59b4905b21df59e3d6
[moodle.git] / lib / amd / src / modal.js
1 // This file is part of Moodle - http://moodle.org/
2 //
3 // Moodle is free software: you can redistribute it and/or modify
4 // it under the terms of the GNU General Public License as published by
5 // the Free Software Foundation, either version 3 of the License, or
6 // (at your option) any later version.
7 //
8 // Moodle is distributed in the hope that it will be useful,
9 // but WITHOUT ANY WARRANTY; without even the implied warranty of
10 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11 // GNU General Public License for more details.
12 //
13 // You should have received a copy of the GNU General Public License
14 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
16 /**
17  * Contain the logic for modals.
18  *
19  * @module     core/modal
20  * @class      modal
21  * @package    core
22  * @copyright  2016 Ryan Wyllie <ryan@moodle.com>
23  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
24  */
25 define([
26     'jquery',
27     'core/templates',
28     'core/notification',
29     'core/key_codes',
30     'core/custom_interaction_events',
31     'core/modal_backdrop',
32     'core/event',
33     'core/modal_events',
34     'core/local/aria/focuslock',
35     'core/pending',
36 ], function($, Templates, Notification, KeyCodes, CustomEvents, ModalBackdrop, Event, ModalEvents, FocusLock, Pending) {
38     var SELECTORS = {
39         CONTAINER: '[data-region="modal-container"]',
40         MODAL: '[data-region="modal"]',
41         HEADER: '[data-region="header"]',
42         TITLE: '[data-region="title"]',
43         BODY: '[data-region="body"]',
44         FOOTER: '[data-region="footer"]',
45         HIDE: '[data-action="hide"]',
46         DIALOG: '[role=dialog]',
47         FORM: 'form',
48         MENU_BAR: '[role=menubar]',
49         HAS_Z_INDEX: '.moodle-has-zindex',
50         CAN_RECEIVE_FOCUS: 'input:not([type="hidden"]), a[href], button, textarea, select, [tabindex]',
51     };
53     var TEMPLATES = {
54         LOADING: 'core/loading',
55         BACKDROP: 'core/modal_backdrop',
56     };
58     /**
59      * Module singleton for the backdrop to be reused by all Modal instances.
60      */
61     var backdropPromise;
63     /**
64      * A counter that gets incremented for each modal created. This can be
65      * used to generate unique values for the modals.
66      */
67     var modalCounter = 0;
69     /**
70      * Constructor for the Modal.
71      *
72      * @param {object} root The root jQuery element for the modal
73      */
74     var Modal = function(root) {
75         this.root = $(root);
76         this.modal = this.root.find(SELECTORS.MODAL);
77         this.header = this.modal.find(SELECTORS.HEADER);
78         this.headerPromise = $.Deferred();
79         this.title = this.header.find(SELECTORS.TITLE);
80         this.titlePromise = $.Deferred();
81         this.body = this.modal.find(SELECTORS.BODY);
82         this.bodyPromise = $.Deferred();
83         this.footer = this.modal.find(SELECTORS.FOOTER);
84         this.footerPromise = $.Deferred();
85         this.hiddenSiblings = [];
86         this.isAttached = false;
87         this.bodyJS = null;
88         this.footerJS = null;
89         this.modalCount = modalCounter++;
91         if (!this.root.is(SELECTORS.CONTAINER)) {
92             Notification.exception({message: 'Element is not a modal container'});
93         }
95         if (!this.modal.length) {
96             Notification.exception({message: 'Container does not contain a modal'});
97         }
99         if (!this.header.length) {
100             Notification.exception({message: 'Modal is missing a header region'});
101         }
103         if (!this.title.length) {
104             Notification.exception({message: 'Modal header is missing a title region'});
105         }
107         if (!this.body.length) {
108             Notification.exception({message: 'Modal is missing a body region'});
109         }
111         if (!this.footer.length) {
112             Notification.exception({message: 'Modal is missing a footer region'});
113         }
115         this.registerEventListeners();
116     };
118     /**
119      * Add the modal to the page, if it hasn't already been added. This includes running any
120      * javascript that has been cached until now.
121      *
122      * @method attachToDOM
123      */
124     Modal.prototype.attachToDOM = function() {
125         if (this.isAttached) {
126             return;
127         }
129         $('body').append(this.root);
130         FocusLock.trapFocus(this.root[0]);
132         // If we'd cached any JS then we can run it how that the modal is
133         // attached to the DOM.
134         if (this.bodyJS) {
135             Templates.runTemplateJS(this.bodyJS);
136             this.bodyJS = null;
137         }
139         if (this.footerJS) {
140             Templates.runTemplateJS(this.footerJS);
141             this.footerJS = null;
142         }
144         this.isAttached = true;
145     };
147     /**
148      * Count the number of other visible modals (not including this one).
149      *
150      * @method countOtherVisibleModals
151      * @return {int}
152      */
153     Modal.prototype.countOtherVisibleModals = function() {
154         var count = 0;
155         $('body').find(SELECTORS.CONTAINER).each(function(index, element) {
156             element = $(element);
158             // If we haven't found ourself and the element is visible.
159             if (!this.root.is(element) && element.hasClass('show')) {
160                 count++;
161             }
162         }.bind(this));
164         return count;
165     };
167     /**
168      * Get the modal backdrop.
169      *
170      * @method getBackdrop
171      * @return {object} jQuery promise
172      */
173     Modal.prototype.getBackdrop = function() {
174         if (!backdropPromise) {
175             backdropPromise = Templates.render(TEMPLATES.BACKDROP, {})
176                 .then(function(html) {
177                     var element = $(html);
179                     return new ModalBackdrop(element);
180                 })
181                 .fail(Notification.exception);
182         }
184         return backdropPromise;
185     };
187     /**
188      * Get the root element of this modal.
189      *
190      * @method getRoot
191      * @return {object} jQuery object
192      */
193     Modal.prototype.getRoot = function() {
194         return this.root;
195     };
197     /**
198      * Get the modal element of this modal.
199      *
200      * @method getModal
201      * @return {object} jQuery object
202      */
203     Modal.prototype.getModal = function() {
204         return this.modal;
205     };
207     /**
208      * Get the modal title element.
209      *
210      * @method getTitle
211      * @return {object} jQuery object
212      */
213     Modal.prototype.getTitle = function() {
214         return this.title;
215     };
217     /**
218      * Get the modal body element.
219      *
220      * @method getBody
221      * @return {object} jQuery object
222      */
223     Modal.prototype.getBody = function() {
224         return this.body;
225     };
227     /**
228      * Get the modal footer element.
229      *
230      * @method getFooter
231      * @return {object} jQuery object
232      */
233     Modal.prototype.getFooter = function() {
234         return this.footer;
235     };
237     /**
238      * Get a promise resolving to the title region.
239      *
240      * @method getTitlePromise
241      * @return {Promise}
242      */
243     Modal.prototype.getTitlePromise = function() {
244         return this.titlePromise;
245     };
247     /**
248      * Get a promise resolving to the body region.
249      *
250      * @method getBodyPromise
251      * @return {object} jQuery object
252      */
253     Modal.prototype.getBodyPromise = function() {
254         return this.bodyPromise;
255     };
257     /**
258      * Get a promise resolving to the footer region.
259      *
260      * @method getFooterPromise
261      * @return {object} jQuery object
262      */
263     Modal.prototype.getFooterPromise = function() {
264         return this.footerPromise;
265     };
267     /**
268      * Get the unique modal count.
269      *
270      * @method getModalCount
271      * @return {int}
272      */
273     Modal.prototype.getModalCount = function() {
274         return this.modalCount;
275     };
277     /**
278      * Set the modal title element.
279      *
280      * This method is overloaded to take either a string value for the title or a jQuery promise that is resolved with
281      * HTML most commonly from a Str.get_string call.
282      *
283      * @method setTitle
284      * @param {(string|object)} value The title string or jQuery promise which resolves to the title.
285      */
286     Modal.prototype.setTitle = function(value) {
287         var title = this.getTitle();
288         this.titlePromise = $.Deferred();
290         this.asyncSet(value, title.html.bind(title))
291         .then(function() {
292             this.titlePromise.resolve(title);
293         }.bind(this))
294         .catch(Notification.exception);
295     };
297     /**
298      * Set the modal body element.
299      *
300      * This method is overloaded to take either a string value for the body or a jQuery promise that is resolved with
301      * HTML and Javascript most commonly from a Templates.render call.
302      *
303      * @method setBody
304      * @param {(string|object)} value The body string or jQuery promise which resolves to the body.
305      */
306     Modal.prototype.setBody = function(value) {
307         this.bodyPromise = $.Deferred();
309         var body = this.getBody();
311         if (typeof value === 'string') {
312             // Just set the value if it's a string.
313             body.html(value);
314             Event.notifyFilterContentUpdated(body);
315             this.getRoot().trigger(ModalEvents.bodyRendered, this);
316             this.bodyPromise.resolve(body);
317         } else {
318             var jsPendingId = 'amd-modal-js-pending-id-' + this.getModalCount();
319             M.util.js_pending(jsPendingId);
320             // Otherwise we assume it's a promise to be resolved with
321             // html and javascript.
322             var contentPromise = null;
323             body.css('overflow', 'hidden');
325             // Ensure that the `value` is a jQuery Promise.
326             value = $.when(value);
328             if (value.state() == 'pending') {
329                 // We're still waiting for the body promise to resolve so
330                 // let's show a loading icon.
331                 var height = body.innerHeight();
332                 if (height < 100) {
333                     height = 100;
334                 }
336                 body.animate({height: height + 'px'}, 150);
338                 body.html('');
339                 contentPromise = Templates.render(TEMPLATES.LOADING, {})
340                     .then(function(html) {
341                         var loadingIcon = $(html).hide();
342                         body.html(loadingIcon);
343                         loadingIcon.fadeIn(150);
345                         // We only want the loading icon to fade out
346                         // when the content for the body has finished
347                         // loading.
348                         return $.when(loadingIcon.promise(), value);
349                     })
350                     .then(function(loadingIcon) {
351                         // Once the content has finished loading and
352                         // the loading icon has been shown then we can
353                         // fade the icon away to reveal the content.
354                         return loadingIcon.fadeOut(100).promise();
355                     })
356                     .then(function() {
357                         return value;
358                     });
359             } else {
360                 // The content is already loaded so let's just display
361                 // it to the user. No need for a loading icon.
362                 contentPromise = value;
363             }
365             // Now we can actually display the content.
366             contentPromise.then(function(html, js) {
367                 var result = null;
369                 if (this.isVisible()) {
370                     // If the modal is visible then we should display
371                     // the content gracefully for the user.
372                     body.css('opacity', 0);
373                     var currentHeight = body.innerHeight();
374                     body.html(html);
375                     // We need to clear any height values we've set here
376                     // in order to measure the height of the content being
377                     // added. This then allows us to animate the height
378                     // transition.
379                     body.css('height', '');
380                     var newHeight = body.innerHeight();
381                     body.css('height', currentHeight + 'px');
382                     result = body.animate(
383                         {height: newHeight + 'px', opacity: 1},
384                         {duration: 150, queue: false}
385                     ).promise();
386                 } else {
387                     // Since the modal isn't visible we can just immediately
388                     // set the content. No need to animate it.
389                     body.html(html);
390                 }
392                 if (js) {
393                     if (this.isAttached) {
394                         // If we're in the DOM then run the JS immediately.
395                         Templates.runTemplateJS(js);
396                     } else {
397                         // Otherwise cache it to be run when we're attached.
398                         this.bodyJS = js;
399                     }
400                 }
402                 return result;
403             }.bind(this))
404             .then(function(result) {
405                 Event.notifyFilterContentUpdated(body);
406                 this.getRoot().trigger(ModalEvents.bodyRendered, this);
407                 return result;
408             }.bind(this))
409             .then(function() {
410                 this.bodyPromise.resolve(body);
411                 return;
412             }.bind(this))
413             .fail(Notification.exception)
414             .always(function() {
415                 // When we're done displaying all of the content we need
416                 // to clear the custom values we've set here.
417                 body.css('height', '');
418                 body.css('overflow', '');
419                 body.css('opacity', '');
420                 M.util.js_complete(jsPendingId);
422                 return;
423             })
424             .fail(Notification.exception);
425         }
426     };
428     /**
429      * Set the modal footer element. The footer element is made visible, if it
430      * isn't already.
431      *
432      * This method is overloaded to take either a string
433      * value for the body or a jQuery promise that is resolved with HTML and Javascript
434      * most commonly from a Templates.render call.
435      *
436      * @method setFooter
437      * @param {(string|object)} value The footer string or jQuery promise
438      */
439     Modal.prototype.setFooter = function(value) {
440         // Make sure the footer is visible.
441         this.showFooter();
442         this.footerPromise = $.Deferred();
444         var footer = this.getFooter();
446         if (typeof value === 'string') {
447             // Just set the value if it's a string.
448             footer.html(value);
449             this.footerPromise.resolve(footer);
450         } else {
451             // Otherwise we assume it's a promise to be resolved with
452             // html and javascript.
453             Templates.render(TEMPLATES.LOADING, {})
454             .then(function(html) {
455                 footer.html(html);
457                 return value;
458             })
459             .then(function(html, js) {
460                 footer.html(html);
462                 if (js) {
463                     if (this.isAttached) {
464                         // If we're in the DOM then run the JS immediately.
465                         Templates.runTemplateJS(js);
466                     } else {
467                         // Otherwise cache it to be run when we're attached.
468                         this.footerJS = js;
469                     }
470                 }
472                 return footer;
473             }.bind(this))
474             .then(function(footer) {
475                 this.footerPromise.resolve(footer);
476                 return;
477             }.bind(this))
478             .catch(Notification.exception);
479         }
480     };
482     /**
483      * Check if the footer has any content in it.
484      *
485      * @method hasFooterContent
486      * @return {bool}
487      */
488     Modal.prototype.hasFooterContent = function() {
489         return this.getFooter().children().length ? true : false;
490     };
492     /**
493      * Hide the footer element.
494      *
495      * @method hideFooter
496      */
497     Modal.prototype.hideFooter = function() {
498         this.getFooter().addClass('hidden');
499     };
501     /**
502      * Show the footer element.
503      *
504      * @method showFooter
505      */
506     Modal.prototype.showFooter = function() {
507         this.getFooter().removeClass('hidden');
508     };
510     /**
511      * Mark the modal as a large modal.
512      *
513      * @method setLarge
514      */
515     Modal.prototype.setLarge = function() {
516         if (this.isLarge()) {
517             return;
518         }
520         this.getModal().addClass('modal-lg');
521     };
523     /**
524      * Check if the modal is a large modal.
525      *
526      * @method isLarge
527      * @return {bool}
528      */
529     Modal.prototype.isLarge = function() {
530         return this.getModal().hasClass('modal-lg');
531     };
533     /**
534      * Mark the modal as a small modal.
535      *
536      * @method setSmall
537      */
538     Modal.prototype.setSmall = function() {
539         if (this.isSmall()) {
540             return;
541         }
543         this.getModal().removeClass('modal-lg');
544     };
546     /**
547      * Check if the modal is a small modal.
548      *
549      * @method isSmall
550      * @return {bool}
551      */
552     Modal.prototype.isSmall = function() {
553         return !this.getModal().hasClass('modal-lg');
554     };
556     /**
557      * Set this modal to be scrollable or not.
558      *
559      * @method setScrollable
560      * @param {bool} value Whether the modal is scrollable or not
561      */
562     Modal.prototype.setScrollable = function(value) {
563         if (!value) {
564             this.getModal()[0].classList.remove('modal-dialog-scrollable');
565             return;
566         }
568         this.getModal()[0].classList.add('modal-dialog-scrollable');
569     };
572     /**
573      * Determine the highest z-index value currently on the page.
574      *
575      * @method calculateZIndex
576      * @return {int}
577      */
578     Modal.prototype.calculateZIndex = function() {
579         var items = $(SELECTORS.DIALOG + ', ' + SELECTORS.MENU_BAR + ', ' + SELECTORS.HAS_Z_INDEX);
580         var zIndex = parseInt(this.root.css('z-index'));
582         items.each(function(index, item) {
583             item = $(item);
584             // Note that webkit browsers won't return the z-index value from the CSS stylesheet
585             // if the element doesn't have a position specified. Instead it'll return "auto".
586             var itemZIndex = item.css('z-index') ? parseInt(item.css('z-index')) : 0;
588             if (itemZIndex > zIndex) {
589                 zIndex = itemZIndex;
590             }
591         });
593         return zIndex;
594     };
596     /**
597      * Check if this modal is visible.
598      *
599      * @method isVisible
600      * @return {bool}
601      */
602     Modal.prototype.isVisible = function() {
603         return this.root.hasClass('show');
604     };
606     /**
607      * Check if this modal has focus.
608      *
609      * @method hasFocus
610      * @return {bool}
611      */
612     Modal.prototype.hasFocus = function() {
613         var target = $(document.activeElement);
614         return this.root.is(target) || this.root.has(target).length;
615     };
617     /**
618      * Check if this modal has CSS transitions applied.
619      *
620      * @method hasTransitions
621      * @return {bool}
622      */
623     Modal.prototype.hasTransitions = function() {
624         return this.getRoot().hasClass('fade');
625     };
627     /**
628      * Display this modal. The modal will be attached to the DOM if it hasn't
629      * already been.
630      *
631      * @method show
632      * @returns {Promise}
633      */
634     Modal.prototype.show = function() {
635         if (this.isVisible()) {
636             return $.Deferred().resolve();
637         }
639         var pendingPromise = new Pending('core/modal:show');
641         if (this.hasFooterContent()) {
642             this.showFooter();
643         } else {
644             this.hideFooter();
645         }
647         if (!this.isAttached) {
648             this.attachToDOM();
649         }
651         return this.getBackdrop()
652         .then(function(backdrop) {
653             var currentIndex = this.calculateZIndex();
654             var newIndex = currentIndex + 2;
655             var newBackdropIndex = newIndex - 1;
656             this.root.css('z-index', newIndex);
657             backdrop.setZIndex(newBackdropIndex);
658             backdrop.show();
660             this.root.removeClass('hide').addClass('show');
661             this.accessibilityShow();
662             this.getModal().focus();
663             $('body').addClass('modal-open');
664             this.root.trigger(ModalEvents.shown, this);
666             return;
667         }.bind(this))
668         .then(pendingPromise.resolve);
669     };
671     /**
672      * Hide this modal if it does not contain a form.
673      *
674      * @method hideIfNotForm
675      */
676     Modal.prototype.hideIfNotForm = function() {
677         var formElement = this.modal.find(SELECTORS.FORM);
678         if (formElement.length == 0) {
679             this.hide();
680         }
681     };
683     /**
684      * Hide this modal.
685      *
686      * @method hide
687      */
688     Modal.prototype.hide = function() {
689         this.getBackdrop().done(function(backdrop) {
690             FocusLock.untrapFocus();
691             if (!this.countOtherVisibleModals()) {
692                 // Hide the backdrop if we're the last open modal.
693                 backdrop.hide();
694                 $('body').removeClass('modal-open');
695             }
697             var currentIndex = parseInt(this.root.css('z-index'));
698             this.root.css('z-index', '');
699             backdrop.setZIndex(currentIndex - 3);
701             this.accessibilityHide();
703             if (this.hasTransitions()) {
704                 // Wait for CSS transitions to complete before hiding the element.
705                 this.getRoot().one('transitionend webkitTransitionEnd oTransitionEnd', function() {
706                     this.getRoot().removeClass('show').addClass('hide');
707                 }.bind(this));
708             } else {
709                 this.getRoot().removeClass('show').addClass('hide');
710             }
712             this.root.trigger(ModalEvents.hidden, this);
713         }.bind(this));
714     };
716     /**
717      * Remove this modal from the DOM.
718      *
719      * @method destroy
720      */
721     Modal.prototype.destroy = function() {
722         this.hide();
723         this.root.remove();
724         this.root.trigger(ModalEvents.destroyed, this);
725     };
727     /**
728      * Sets the appropriate aria attributes on this dialogue and the other
729      * elements in the DOM to ensure that screen readers are able to navigate
730      * the dialogue popup correctly.
731      *
732      * @method accessibilityShow
733      */
734     Modal.prototype.accessibilityShow = function() {
735         // We need to get a list containing each sibling element and the shallowest
736         // non-ancestral nodes in the DOM. We can shortcut this a little by leveraging
737         // the fact that this dialogue is always appended to the document body therefore
738         // it's siblings are the shallowest non-ancestral nodes. If that changes then
739         // this code should also be updated.
740         $('body').children().each(function(index, child) {
741             // Skip the current modal.
742             if (!this.root.is(child)) {
743                 child = $(child);
744                 var hidden = child.attr('aria-hidden');
745                 // If they are already hidden we can ignore them.
746                 if (hidden !== 'true') {
747                     // Save their current state.
748                     child.data('previous-aria-hidden', hidden);
749                     this.hiddenSiblings.push(child);
751                     // Hide this node from screen readers.
752                     child.attr('aria-hidden', 'true');
753                 }
754             }
755         }.bind(this));
757         // Make us visible to screen readers.
758         this.root.attr('aria-hidden', 'false');
759     };
761     /**
762      * Restores the aria visibility on the DOM elements changed when displaying
763      * the dialogue popup and makes the dialogue aria hidden to allow screen
764      * readers to navigate the main page correctly when the dialogue is closed.
765      *
766      * @method accessibilityHide
767      */
768     Modal.prototype.accessibilityHide = function() {
769         this.root.attr('aria-hidden', 'true');
771         // Restore the sibling nodes back to their original values.
772         $.each(this.hiddenSiblings, function(index, sibling) {
773             sibling = $(sibling);
774             var previousValue = sibling.data('previous-aria-hidden');
775             // If the element didn't previously have an aria-hidden attribute
776             // then we can just remove the one we set.
777             if (typeof previousValue == 'undefined') {
778                 sibling.removeAttr('aria-hidden');
779             } else {
780                 // Otherwise set it back to the old value (which will be false).
781                 sibling.attr('aria-hidden', previousValue);
782             }
783         });
785         // Clear the cache. No longer need to store these.
786         this.hiddenSiblings = [];
787     };
789     /**
790      * Set up all of the event handling for the modal.
791      *
792      * @method registerEventListeners
793      */
794     Modal.prototype.registerEventListeners = function() {
795         this.getRoot().on('keydown', function(e) {
796             if (!this.isVisible()) {
797                 return;
798             }
800             if (e.keyCode == KeyCodes.escape) {
801                 this.hide();
802             }
803         }.bind(this));
805         // Listen for clicks on the modal container.
806         this.getRoot().click(function(e) {
807             // If the click wasn't inside the modal element then we should
808             // hide the modal.
809             if (!$(e.target).closest(SELECTORS.MODAL).length) {
810                 // The check above fails to detect the click was inside the modal when the DOM tree is already changed.
811                 // So, we check if we can still find the container element or not. If not, then the DOM tree is changed.
812                 // It's best not to hide the modal in that case.
813                 if ($(e.target).closest(SELECTORS.CONTAINER).length) {
814                     this.hideIfNotForm();
815                 }
816             }
817         }.bind(this));
819         CustomEvents.define(this.getModal(), [CustomEvents.events.activate]);
820         this.getModal().on(CustomEvents.events.activate, SELECTORS.HIDE, function(e, data) {
821             this.hide();
822             data.originalEvent.preventDefault();
823         }.bind(this));
824     };
826     /**
827      * Register a listener to close the dialogue when the cancel button is pressed.
828      *
829      * @method registerCloseOnCancel
830      */
831     Modal.prototype.registerCloseOnCancel = function() {
832         // Handle the clicking of the Cancel button.
833         this.getModal().on(CustomEvents.events.activate, this.getActionSelector('cancel'), function(e, data) {
834             var cancelEvent = $.Event(ModalEvents.cancel);
835             this.getRoot().trigger(cancelEvent, this);
837             if (!cancelEvent.isDefaultPrevented()) {
838                 data.originalEvent.preventDefault();
840                 if (this.removeOnClose) {
841                     this.destroy();
842                 } else {
843                     this.hide();
844                 }
845             }
846         }.bind(this));
847     };
849     /**
850      * Register a listener to close the dialogue when the save button is pressed.
851      *
852      * @method registerCloseOnSave
853      */
854     Modal.prototype.registerCloseOnSave = function() {
855         // Handle the clicking of the Cancel button.
856         this.getModal().on(CustomEvents.events.activate, this.getActionSelector('save'), function(e, data) {
857             var saveEvent = $.Event(ModalEvents.save);
858             this.getRoot().trigger(saveEvent, this);
860             if (!saveEvent.isDefaultPrevented()) {
861                 data.originalEvent.preventDefault();
863                 if (this.removeOnClose) {
864                     this.destroy();
865                 } else {
866                     this.hide();
867                 }
868             }
869         }.bind(this));
870     };
872     /**
873      * Set or resolve and set the value using the function.
874      *
875      * @method asyncSet
876      * @param {(string|object)} value The string or jQuery promise.
877      * @param {function} setFunction The setter
878      * @return {Promise}
879      */
880     Modal.prototype.asyncSet = function(value, setFunction) {
881         var p = value;
882         if (typeof value !== 'object' || !value.hasOwnProperty('then')) {
883             p = $.Deferred();
884             p.resolve(value);
885         }
887         p.then(function(content) {
888             setFunction(content);
890             return;
891         })
892         .fail(Notification.exception);
894         return p;
895     };
897     /**
898      * Set the title text of a button.
899      *
900      * This method is overloaded to take either a string value for the button title or a jQuery promise that is resolved with
901      * text most commonly from a Str.get_string call.
902      *
903      * @param {DOMString} action The action of the button
904      * @param {(String|object)} value The button text, or a promise which will resolve to it
905      * @returns {Promise}
906      */
907     Modal.prototype.setButtonText = function(action, value) {
908         const button = this.getFooter().find(this.getActionSelector(action));
910         if (!button) {
911             throw new Error("Unable to find the '" + action + "' button");
912         }
914         return this.asyncSet(value, button.text.bind(button));
915     };
917     /**
918      * Get the Selector for an action.
919      *
920      * @param {String} action
921      * @returns {DOMString}
922      */
923     Modal.prototype.getActionSelector = function(action) {
924         return "[data-action='" + action + "']";
925     };
927     /**
928      * Set the flag to remove the modal from the DOM on close.
929      *
930      * @param {Boolean} remove
931      */
932     Modal.prototype.setRemoveOnClose = function(remove) {
933         this.removeOnClose = remove;
934     };
936     return Modal;
937 });