MDL-68409 js: Add setButtonText modal helper
[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             if (value.state() == 'pending') {
326                 // We're still waiting for the body promise to resolve so
327                 // let's show a loading icon.
328                 var height = body.innerHeight();
329                 if (height < 100) {
330                     height = 100;
331                 }
333                 body.animate({height: height + 'px'}, 150);
335                 body.html('');
336                 contentPromise = Templates.render(TEMPLATES.LOADING, {})
337                     .then(function(html) {
338                         var loadingIcon = $(html).hide();
339                         body.html(loadingIcon);
340                         loadingIcon.fadeIn(150);
342                         // We only want the loading icon to fade out
343                         // when the content for the body has finished
344                         // loading.
345                         return $.when(loadingIcon.promise(), value);
346                     })
347                     .then(function(loadingIcon) {
348                         // Once the content has finished loading and
349                         // the loading icon has been shown then we can
350                         // fade the icon away to reveal the content.
351                         return loadingIcon.fadeOut(100).promise();
352                     })
353                     .then(function() {
354                         return value;
355                     });
356             } else {
357                 // The content is already loaded so let's just display
358                 // it to the user. No need for a loading icon.
359                 contentPromise = value;
360             }
362             // Now we can actually display the content.
363             contentPromise.then(function(html, js) {
364                 var result = null;
366                 if (this.isVisible()) {
367                     // If the modal is visible then we should display
368                     // the content gracefully for the user.
369                     body.css('opacity', 0);
370                     var currentHeight = body.innerHeight();
371                     body.html(html);
372                     // We need to clear any height values we've set here
373                     // in order to measure the height of the content being
374                     // added. This then allows us to animate the height
375                     // transition.
376                     body.css('height', '');
377                     var newHeight = body.innerHeight();
378                     body.css('height', currentHeight + 'px');
379                     result = body.animate(
380                         {height: newHeight + 'px', opacity: 1},
381                         {duration: 150, queue: false}
382                     ).promise();
383                 } else {
384                     // Since the modal isn't visible we can just immediately
385                     // set the content. No need to animate it.
386                     body.html(html);
387                 }
389                 if (js) {
390                     if (this.isAttached) {
391                         // If we're in the DOM then run the JS immediately.
392                         Templates.runTemplateJS(js);
393                     } else {
394                         // Otherwise cache it to be run when we're attached.
395                         this.bodyJS = js;
396                     }
397                 }
399                 return result;
400             }.bind(this))
401             .then(function(result) {
402                 Event.notifyFilterContentUpdated(body);
403                 this.getRoot().trigger(ModalEvents.bodyRendered, this);
404                 return result;
405             }.bind(this))
406             .then(function() {
407                 this.bodyPromise.resolve(body);
408                 return;
409             }.bind(this))
410             .fail(Notification.exception)
411             .always(function() {
412                 // When we're done displaying all of the content we need
413                 // to clear the custom values we've set here.
414                 body.css('height', '');
415                 body.css('overflow', '');
416                 body.css('opacity', '');
417                 M.util.js_complete(jsPendingId);
419                 return;
420             })
421             .fail(Notification.exception);
422         }
423     };
425     /**
426      * Set the modal footer element. The footer element is made visible, if it
427      * isn't already.
428      *
429      * This method is overloaded to take either a string
430      * value for the body or a jQuery promise that is resolved with HTML and Javascript
431      * most commonly from a Templates.render call.
432      *
433      * @method setFooter
434      * @param {(string|object)} value The footer string or jQuery promise
435      */
436     Modal.prototype.setFooter = function(value) {
437         // Make sure the footer is visible.
438         this.showFooter();
439         this.footerPromise = $.Deferred();
441         var footer = this.getFooter();
443         if (typeof value === 'string') {
444             // Just set the value if it's a string.
445             footer.html(value);
446             this.footerPromise.resolve(footer);
447         } else {
448             // Otherwise we assume it's a promise to be resolved with
449             // html and javascript.
450             Templates.render(TEMPLATES.LOADING, {})
451             .then(function(html) {
452                 footer.html(html);
454                 return value;
455             })
456             .then(function(html, js) {
457                 footer.html(html);
459                 if (js) {
460                     if (this.isAttached) {
461                         // If we're in the DOM then run the JS immediately.
462                         Templates.runTemplateJS(js);
463                     } else {
464                         // Otherwise cache it to be run when we're attached.
465                         this.footerJS = js;
466                     }
467                 }
469                 return footer;
470             }.bind(this))
471             .then(function(footer) {
472                 this.footerPromise.resolve(footer);
473                 return;
474             }.bind(this))
475             .catch(Notification.exception);
476         }
477     };
479     /**
480      * Check if the footer has any content in it.
481      *
482      * @method hasFooterContent
483      * @return {bool}
484      */
485     Modal.prototype.hasFooterContent = function() {
486         return this.getFooter().children().length ? true : false;
487     };
489     /**
490      * Hide the footer element.
491      *
492      * @method hideFooter
493      */
494     Modal.prototype.hideFooter = function() {
495         this.getFooter().addClass('hidden');
496     };
498     /**
499      * Show the footer element.
500      *
501      * @method showFooter
502      */
503     Modal.prototype.showFooter = function() {
504         this.getFooter().removeClass('hidden');
505     };
507     /**
508      * Mark the modal as a large modal.
509      *
510      * @method setLarge
511      */
512     Modal.prototype.setLarge = function() {
513         if (this.isLarge()) {
514             return;
515         }
517         this.getModal().addClass('modal-lg');
518     };
520     /**
521      * Check if the modal is a large modal.
522      *
523      * @method isLarge
524      * @return {bool}
525      */
526     Modal.prototype.isLarge = function() {
527         return this.getModal().hasClass('modal-lg');
528     };
530     /**
531      * Mark the modal as a small modal.
532      *
533      * @method setSmall
534      */
535     Modal.prototype.setSmall = function() {
536         if (this.isSmall()) {
537             return;
538         }
540         this.getModal().removeClass('modal-lg');
541     };
543     /**
544      * Check if the modal is a small modal.
545      *
546      * @method isSmall
547      * @return {bool}
548      */
549     Modal.prototype.isSmall = function() {
550         return !this.getModal().hasClass('modal-lg');
551     };
553     /**
554      * Determine the highest z-index value currently on the page.
555      *
556      * @method calculateZIndex
557      * @return {int}
558      */
559     Modal.prototype.calculateZIndex = function() {
560         var items = $(SELECTORS.DIALOG + ', ' + SELECTORS.MENU_BAR + ', ' + SELECTORS.HAS_Z_INDEX);
561         var zIndex = parseInt(this.root.css('z-index'));
563         items.each(function(index, item) {
564             item = $(item);
565             // Note that webkit browsers won't return the z-index value from the CSS stylesheet
566             // if the element doesn't have a position specified. Instead it'll return "auto".
567             var itemZIndex = item.css('z-index') ? parseInt(item.css('z-index')) : 0;
569             if (itemZIndex > zIndex) {
570                 zIndex = itemZIndex;
571             }
572         });
574         return zIndex;
575     };
577     /**
578      * Check if this modal is visible.
579      *
580      * @method isVisible
581      * @return {bool}
582      */
583     Modal.prototype.isVisible = function() {
584         return this.root.hasClass('show');
585     };
587     /**
588      * Check if this modal has focus.
589      *
590      * @method hasFocus
591      * @return {bool}
592      */
593     Modal.prototype.hasFocus = function() {
594         var target = $(document.activeElement);
595         return this.root.is(target) || this.root.has(target).length;
596     };
598     /**
599      * Check if this modal has CSS transitions applied.
600      *
601      * @method hasTransitions
602      * @return {bool}
603      */
604     Modal.prototype.hasTransitions = function() {
605         return this.getRoot().hasClass('fade');
606     };
608     /**
609      * Display this modal. The modal will be attached to the DOM if it hasn't
610      * already been.
611      *
612      * @method show
613      */
614     Modal.prototype.show = function() {
615         if (this.isVisible()) {
616             return;
617         }
619         var pendingPromise = new Pending('core/modal:show');
621         if (this.hasFooterContent()) {
622             this.showFooter();
623         } else {
624             this.hideFooter();
625         }
627         if (!this.isAttached) {
628             this.attachToDOM();
629         }
631         this.getBackdrop()
632         .then(function(backdrop) {
633             var currentIndex = this.calculateZIndex();
634             var newIndex = currentIndex + 2;
635             var newBackdropIndex = newIndex - 1;
636             this.root.css('z-index', newIndex);
637             backdrop.setZIndex(newBackdropIndex);
638             backdrop.show();
640             this.root.removeClass('hide').addClass('show');
641             this.accessibilityShow();
642             this.getModal().focus();
643             $('body').addClass('modal-open');
644             this.root.trigger(ModalEvents.shown, this);
646             return;
647         }.bind(this))
648         .then(pendingPromise.resolve);
649     };
651     /**
652      * Hide this modal if it does not contain a form.
653      *
654      * @method hideIfNotForm
655      */
656     Modal.prototype.hideIfNotForm = function() {
657         var formElement = this.modal.find(SELECTORS.FORM);
658         if (formElement.length == 0) {
659             this.hide();
660         }
661     };
663     /**
664      * Hide this modal.
665      *
666      * @method hide
667      */
668     Modal.prototype.hide = function() {
669         this.getBackdrop().done(function(backdrop) {
670             FocusLock.untrapFocus();
671             if (!this.countOtherVisibleModals()) {
672                 // Hide the backdrop if we're the last open modal.
673                 backdrop.hide();
674                 $('body').removeClass('modal-open');
675             }
677             var currentIndex = parseInt(this.root.css('z-index'));
678             this.root.css('z-index', '');
679             backdrop.setZIndex(currentIndex - 3);
681             this.accessibilityHide();
683             if (this.hasTransitions()) {
684                 // Wait for CSS transitions to complete before hiding the element.
685                 this.getRoot().one('transitionend webkitTransitionEnd oTransitionEnd', function() {
686                     this.getRoot().removeClass('show').addClass('hide');
687                 }.bind(this));
688             } else {
689                 this.getRoot().removeClass('show').addClass('hide');
690             }
692             this.root.trigger(ModalEvents.hidden, this);
693         }.bind(this));
694     };
696     /**
697      * Remove this modal from the DOM.
698      *
699      * @method destroy
700      */
701     Modal.prototype.destroy = function() {
702         this.hide();
703         this.root.remove();
704         this.root.trigger(ModalEvents.destroyed, this);
705     };
707     /**
708      * Sets the appropriate aria attributes on this dialogue and the other
709      * elements in the DOM to ensure that screen readers are able to navigate
710      * the dialogue popup correctly.
711      *
712      * @method accessibilityShow
713      */
714     Modal.prototype.accessibilityShow = function() {
715         // We need to get a list containing each sibling element and the shallowest
716         // non-ancestral nodes in the DOM. We can shortcut this a little by leveraging
717         // the fact that this dialogue is always appended to the document body therefore
718         // it's siblings are the shallowest non-ancestral nodes. If that changes then
719         // this code should also be updated.
720         $('body').children().each(function(index, child) {
721             // Skip the current modal.
722             if (!this.root.is(child)) {
723                 child = $(child);
724                 var hidden = child.attr('aria-hidden');
725                 // If they are already hidden we can ignore them.
726                 if (hidden !== 'true') {
727                     // Save their current state.
728                     child.data('previous-aria-hidden', hidden);
729                     this.hiddenSiblings.push(child);
731                     // Hide this node from screen readers.
732                     child.attr('aria-hidden', 'true');
733                 }
734             }
735         }.bind(this));
737         // Make us visible to screen readers.
738         this.root.attr('aria-hidden', 'false');
739     };
741     /**
742      * Restores the aria visibility on the DOM elements changed when displaying
743      * the dialogue popup and makes the dialogue aria hidden to allow screen
744      * readers to navigate the main page correctly when the dialogue is closed.
745      *
746      * @method accessibilityHide
747      */
748     Modal.prototype.accessibilityHide = function() {
749         this.root.attr('aria-hidden', 'true');
751         // Restore the sibling nodes back to their original values.
752         $.each(this.hiddenSiblings, function(index, sibling) {
753             sibling = $(sibling);
754             var previousValue = sibling.data('previous-aria-hidden');
755             // If the element didn't previously have an aria-hidden attribute
756             // then we can just remove the one we set.
757             if (typeof previousValue == 'undefined') {
758                 sibling.removeAttr('aria-hidden');
759             } else {
760                 // Otherwise set it back to the old value (which will be false).
761                 sibling.attr('aria-hidden', previousValue);
762             }
763         });
765         // Clear the cache. No longer need to store these.
766         this.hiddenSiblings = [];
767     };
769     /**
770      * Set up all of the event handling for the modal.
771      *
772      * @method registerEventListeners
773      */
774     Modal.prototype.registerEventListeners = function() {
775         this.getRoot().on('keydown', function(e) {
776             if (!this.isVisible()) {
777                 return;
778             }
780             if (e.keyCode == KeyCodes.escape) {
781                 this.hide();
782             }
783         }.bind(this));
785         // Listen for clicks on the modal container.
786         this.getRoot().click(function(e) {
787             // If the click wasn't inside the modal element then we should
788             // hide the modal.
789             if (!$(e.target).closest(SELECTORS.MODAL).length) {
790                 // The check above fails to detect the click was inside the modal when the DOM tree is already changed.
791                 // So, we check if we can still find the container element or not. If not, then the DOM tree is changed.
792                 // It's best not to hide the modal in that case.
793                 if ($(e.target).closest(SELECTORS.CONTAINER).length) {
794                     this.hideIfNotForm();
795                 }
796             }
797         }.bind(this));
799         CustomEvents.define(this.getModal(), [CustomEvents.events.activate]);
800         this.getModal().on(CustomEvents.events.activate, SELECTORS.HIDE, function(e, data) {
801             this.hide();
802             data.originalEvent.preventDefault();
803         }.bind(this));
804     };
806     /**
807      * Register a listener to close the dialogue when the cancel button is pressed.
808      *
809      * @method registerCloseOnCancel
810      */
811     Modal.prototype.registerCloseOnCancel = function() {
812         // Handle the clicking of the Cancel button.
813         this.getModal().on(CustomEvents.events.activate, this.getActionSelector('cancel'), function(e, data) {
814             var cancelEvent = $.Event(ModalEvents.cancel);
815             this.getRoot().trigger(cancelEvent, this);
817             if (!cancelEvent.isDefaultPrevented()) {
818                 data.originalEvent.preventDefault();
820                 if (this.removeOnClose) {
821                     this.destroy();
822                 } else {
823                     this.hide();
824                 }
825             }
826         }.bind(this));
827     };
829     /**
830      * Register a listener to close the dialogue when the save button is pressed.
831      *
832      * @method registerCloseOnSave
833      */
834     Modal.prototype.registerCloseOnSave = function() {
835         // Handle the clicking of the Cancel button.
836         this.getModal().on(CustomEvents.events.activate, this.getActionSelector('save'), function(e, data) {
837             var saveEvent = $.Event(ModalEvents.save);
838             this.getRoot().trigger(saveEvent, this);
840             if (!saveEvent.isDefaultPrevented()) {
841                 data.originalEvent.preventDefault();
843                 if (this.removeOnClose) {
844                     this.destroy();
845                 } else {
846                     this.hide();
847                 }
848             }
849         }.bind(this));
850     };
852     /**
853      * Set or resolve and set the value using the function.
854      *
855      * @method asyncSet
856      * @param {(string|object)} value The string or jQuery promise.
857      * @param {function} setFunction The setter
858      * @return {Promise}
859      */
860     Modal.prototype.asyncSet = function(value, setFunction) {
861         var p = value;
862         if (typeof value !== 'object' || !value.hasOwnProperty('then')) {
863             p = $.Deferred();
864             p.resolve(value);
865         }
867         p.then(function(content) {
868             setFunction(content);
870             return;
871         })
872         .fail(Notification.exception);
874         return p;
875     };
877     /**
878      * Set the title text of a button.
879      *
880      * This method is overloaded to take either a string value for the button title or a jQuery promise that is resolved with
881      * text most commonly from a Str.get_string call.
882      *
883      * @param {DOMString} action The action of the button
884      * @param {(String|object)} value The button text, or a promise which will resolve to it
885      * @returns {Promise}
886      */
887     Modal.prototype.setButtonText = function(action, value) {
888         const button = this.getFooter().find(this.getActionSelector(action));
890         if (!button) {
891             throw new Error("Unable to find the '" + action + "' button");
892         }
894         return this.asyncSet(value, button.text.bind(button));
895     };
897     /**
898      * Get the Selector for an action.
899      *
900      * @param {String} action
901      * @returns {DOMString}
902      */
903     Modal.prototype.getActionSelector = function(action) {
904         return "[data-action='" + action + "']";
905     };
907     /**
908      * Set the flag to remove the modal from the DOM on close.
909      *
910      * @param {Boolean} remove
911      */
912     Modal.prototype.setRemoveOnClose = function(remove) {
913         this.removeOnClose = remove;
914     };
916     return Modal;
917 });