MDL-68390 aria: Add new core_aria module
[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     'core/aria',
37 ], function($, Templates, Notification, KeyCodes, CustomEvents, ModalBackdrop, Event, ModalEvents, FocusLock, Pending, Aria) {
39     var SELECTORS = {
40         CONTAINER: '[data-region="modal-container"]',
41         MODAL: '[data-region="modal"]',
42         HEADER: '[data-region="header"]',
43         TITLE: '[data-region="title"]',
44         BODY: '[data-region="body"]',
45         FOOTER: '[data-region="footer"]',
46         HIDE: '[data-action="hide"]',
47         DIALOG: '[role=dialog]',
48         FORM: 'form',
49         MENU_BAR: '[role=menubar]',
50         HAS_Z_INDEX: '.moodle-has-zindex',
51         CAN_RECEIVE_FOCUS: 'input:not([type="hidden"]), a[href], button, textarea, select, [tabindex]',
52     };
54     var TEMPLATES = {
55         LOADING: 'core/loading',
56         BACKDROP: 'core/modal_backdrop',
57     };
59     /**
60      * Module singleton for the backdrop to be reused by all Modal instances.
61      */
62     var backdropPromise;
64     /**
65      * A counter that gets incremented for each modal created. This can be
66      * used to generate unique values for the modals.
67      */
68     var modalCounter = 0;
70     /**
71      * Constructor for the Modal.
72      *
73      * @param {object} root The root jQuery element for the modal
74      */
75     var Modal = function(root) {
76         this.root = $(root);
77         this.modal = this.root.find(SELECTORS.MODAL);
78         this.header = this.modal.find(SELECTORS.HEADER);
79         this.headerPromise = $.Deferred();
80         this.title = this.header.find(SELECTORS.TITLE);
81         this.titlePromise = $.Deferred();
82         this.body = this.modal.find(SELECTORS.BODY);
83         this.bodyPromise = $.Deferred();
84         this.footer = this.modal.find(SELECTORS.FOOTER);
85         this.footerPromise = $.Deferred();
86         this.hiddenSiblings = [];
87         this.isAttached = false;
88         this.bodyJS = null;
89         this.footerJS = null;
90         this.modalCount = modalCounter++;
92         if (!this.root.is(SELECTORS.CONTAINER)) {
93             Notification.exception({message: 'Element is not a modal container'});
94         }
96         if (!this.modal.length) {
97             Notification.exception({message: 'Container does not contain a modal'});
98         }
100         if (!this.header.length) {
101             Notification.exception({message: 'Modal is missing a header region'});
102         }
104         if (!this.title.length) {
105             Notification.exception({message: 'Modal header is missing a title region'});
106         }
108         if (!this.body.length) {
109             Notification.exception({message: 'Modal is missing a body region'});
110         }
112         if (!this.footer.length) {
113             Notification.exception({message: 'Modal is missing a footer region'});
114         }
116         this.registerEventListeners();
117     };
119     /**
120      * Add the modal to the page, if it hasn't already been added. This includes running any
121      * javascript that has been cached until now.
122      *
123      * @method attachToDOM
124      */
125     Modal.prototype.attachToDOM = function() {
126         if (this.isAttached) {
127             return;
128         }
130         $('body').append(this.root);
131         FocusLock.trapFocus(this.root[0]);
133         // If we'd cached any JS then we can run it how that the modal is
134         // attached to the DOM.
135         if (this.bodyJS) {
136             Templates.runTemplateJS(this.bodyJS);
137             this.bodyJS = null;
138         }
140         if (this.footerJS) {
141             Templates.runTemplateJS(this.footerJS);
142             this.footerJS = null;
143         }
145         this.isAttached = true;
146     };
148     /**
149      * Count the number of other visible modals (not including this one).
150      *
151      * @method countOtherVisibleModals
152      * @return {int}
153      */
154     Modal.prototype.countOtherVisibleModals = function() {
155         var count = 0;
156         $('body').find(SELECTORS.CONTAINER).each(function(index, element) {
157             element = $(element);
159             // If we haven't found ourself and the element is visible.
160             if (!this.root.is(element) && element.hasClass('show')) {
161                 count++;
162             }
163         }.bind(this));
165         return count;
166     };
168     /**
169      * Get the modal backdrop.
170      *
171      * @method getBackdrop
172      * @return {object} jQuery promise
173      */
174     Modal.prototype.getBackdrop = function() {
175         if (!backdropPromise) {
176             backdropPromise = Templates.render(TEMPLATES.BACKDROP, {})
177                 .then(function(html) {
178                     var element = $(html);
180                     return new ModalBackdrop(element);
181                 })
182                 .fail(Notification.exception);
183         }
185         return backdropPromise;
186     };
188     /**
189      * Get the root element of this modal.
190      *
191      * @method getRoot
192      * @return {object} jQuery object
193      */
194     Modal.prototype.getRoot = function() {
195         return this.root;
196     };
198     /**
199      * Get the modal element of this modal.
200      *
201      * @method getModal
202      * @return {object} jQuery object
203      */
204     Modal.prototype.getModal = function() {
205         return this.modal;
206     };
208     /**
209      * Get the modal title element.
210      *
211      * @method getTitle
212      * @return {object} jQuery object
213      */
214     Modal.prototype.getTitle = function() {
215         return this.title;
216     };
218     /**
219      * Get the modal body element.
220      *
221      * @method getBody
222      * @return {object} jQuery object
223      */
224     Modal.prototype.getBody = function() {
225         return this.body;
226     };
228     /**
229      * Get the modal footer element.
230      *
231      * @method getFooter
232      * @return {object} jQuery object
233      */
234     Modal.prototype.getFooter = function() {
235         return this.footer;
236     };
238     /**
239      * Get a promise resolving to the title region.
240      *
241      * @method getTitlePromise
242      * @return {Promise}
243      */
244     Modal.prototype.getTitlePromise = function() {
245         return this.titlePromise;
246     };
248     /**
249      * Get a promise resolving to the body region.
250      *
251      * @method getBodyPromise
252      * @return {object} jQuery object
253      */
254     Modal.prototype.getBodyPromise = function() {
255         return this.bodyPromise;
256     };
258     /**
259      * Get a promise resolving to the footer region.
260      *
261      * @method getFooterPromise
262      * @return {object} jQuery object
263      */
264     Modal.prototype.getFooterPromise = function() {
265         return this.footerPromise;
266     };
268     /**
269      * Get the unique modal count.
270      *
271      * @method getModalCount
272      * @return {int}
273      */
274     Modal.prototype.getModalCount = function() {
275         return this.modalCount;
276     };
278     /**
279      * Set the modal title element.
280      *
281      * This method is overloaded to take either a string value for the title or a jQuery promise that is resolved with
282      * HTML most commonly from a Str.get_string call.
283      *
284      * @method setTitle
285      * @param {(string|object)} value The title string or jQuery promise which resolves to the title.
286      */
287     Modal.prototype.setTitle = function(value) {
288         var title = this.getTitle();
289         this.titlePromise = $.Deferred();
291         this.asyncSet(value, title.html.bind(title))
292         .then(function() {
293             this.titlePromise.resolve(title);
294         }.bind(this))
295         .catch(Notification.exception);
296     };
298     /**
299      * Set the modal body element.
300      *
301      * This method is overloaded to take either a string value for the body or a jQuery promise that is resolved with
302      * HTML and Javascript most commonly from a Templates.render call.
303      *
304      * @method setBody
305      * @param {(string|object)} value The body string or jQuery promise which resolves to the body.
306      */
307     Modal.prototype.setBody = function(value) {
308         this.bodyPromise = $.Deferred();
310         var body = this.getBody();
312         if (typeof value === 'string') {
313             // Just set the value if it's a string.
314             body.html(value);
315             Event.notifyFilterContentUpdated(body);
316             this.getRoot().trigger(ModalEvents.bodyRendered, this);
317             this.bodyPromise.resolve(body);
318         } else {
319             var jsPendingId = 'amd-modal-js-pending-id-' + this.getModalCount();
320             M.util.js_pending(jsPendingId);
321             // Otherwise we assume it's a promise to be resolved with
322             // html and javascript.
323             var contentPromise = null;
324             body.css('overflow', 'hidden');
326             // Ensure that the `value` is a jQuery Promise.
327             value = $.when(value);
329             if (value.state() == 'pending') {
330                 // We're still waiting for the body promise to resolve so
331                 // let's show a loading icon.
332                 var height = body.innerHeight();
333                 if (height < 100) {
334                     height = 100;
335                 }
337                 body.animate({height: height + 'px'}, 150);
339                 body.html('');
340                 contentPromise = Templates.render(TEMPLATES.LOADING, {})
341                     .then(function(html) {
342                         var loadingIcon = $(html).hide();
343                         body.html(loadingIcon);
344                         loadingIcon.fadeIn(150);
346                         // We only want the loading icon to fade out
347                         // when the content for the body has finished
348                         // loading.
349                         return $.when(loadingIcon.promise(), value);
350                     })
351                     .then(function(loadingIcon) {
352                         // Once the content has finished loading and
353                         // the loading icon has been shown then we can
354                         // fade the icon away to reveal the content.
355                         return loadingIcon.fadeOut(100).promise();
356                     })
357                     .then(function() {
358                         return value;
359                     });
360             } else {
361                 // The content is already loaded so let's just display
362                 // it to the user. No need for a loading icon.
363                 contentPromise = value;
364             }
366             // Now we can actually display the content.
367             contentPromise.then(function(html, js) {
368                 var result = null;
370                 if (this.isVisible()) {
371                     // If the modal is visible then we should display
372                     // the content gracefully for the user.
373                     body.css('opacity', 0);
374                     var currentHeight = body.innerHeight();
375                     body.html(html);
376                     // We need to clear any height values we've set here
377                     // in order to measure the height of the content being
378                     // added. This then allows us to animate the height
379                     // transition.
380                     body.css('height', '');
381                     var newHeight = body.innerHeight();
382                     body.css('height', currentHeight + 'px');
383                     result = body.animate(
384                         {height: newHeight + 'px', opacity: 1},
385                         {duration: 150, queue: false}
386                     ).promise();
387                 } else {
388                     // Since the modal isn't visible we can just immediately
389                     // set the content. No need to animate it.
390                     body.html(html);
391                 }
393                 if (js) {
394                     if (this.isAttached) {
395                         // If we're in the DOM then run the JS immediately.
396                         Templates.runTemplateJS(js);
397                     } else {
398                         // Otherwise cache it to be run when we're attached.
399                         this.bodyJS = js;
400                     }
401                 }
403                 return result;
404             }.bind(this))
405             .then(function(result) {
406                 Event.notifyFilterContentUpdated(body);
407                 this.getRoot().trigger(ModalEvents.bodyRendered, this);
408                 return result;
409             }.bind(this))
410             .then(function() {
411                 this.bodyPromise.resolve(body);
412                 return;
413             }.bind(this))
414             .fail(Notification.exception)
415             .always(function() {
416                 // When we're done displaying all of the content we need
417                 // to clear the custom values we've set here.
418                 body.css('height', '');
419                 body.css('overflow', '');
420                 body.css('opacity', '');
421                 M.util.js_complete(jsPendingId);
423                 return;
424             })
425             .fail(Notification.exception);
426         }
427     };
429     /**
430      * Set the modal footer element. The footer element is made visible, if it
431      * isn't already.
432      *
433      * This method is overloaded to take either a string
434      * value for the body or a jQuery promise that is resolved with HTML and Javascript
435      * most commonly from a Templates.render call.
436      *
437      * @method setFooter
438      * @param {(string|object)} value The footer string or jQuery promise
439      */
440     Modal.prototype.setFooter = function(value) {
441         // Make sure the footer is visible.
442         this.showFooter();
443         this.footerPromise = $.Deferred();
445         var footer = this.getFooter();
447         if (typeof value === 'string') {
448             // Just set the value if it's a string.
449             footer.html(value);
450             this.footerPromise.resolve(footer);
451         } else {
452             // Otherwise we assume it's a promise to be resolved with
453             // html and javascript.
454             Templates.render(TEMPLATES.LOADING, {})
455             .then(function(html) {
456                 footer.html(html);
458                 return value;
459             })
460             .then(function(html, js) {
461                 footer.html(html);
463                 if (js) {
464                     if (this.isAttached) {
465                         // If we're in the DOM then run the JS immediately.
466                         Templates.runTemplateJS(js);
467                     } else {
468                         // Otherwise cache it to be run when we're attached.
469                         this.footerJS = js;
470                     }
471                 }
473                 return footer;
474             }.bind(this))
475             .then(function(footer) {
476                 this.footerPromise.resolve(footer);
477                 return;
478             }.bind(this))
479             .catch(Notification.exception);
480         }
481     };
483     /**
484      * Check if the footer has any content in it.
485      *
486      * @method hasFooterContent
487      * @return {bool}
488      */
489     Modal.prototype.hasFooterContent = function() {
490         return this.getFooter().children().length ? true : false;
491     };
493     /**
494      * Hide the footer element.
495      *
496      * @method hideFooter
497      */
498     Modal.prototype.hideFooter = function() {
499         this.getFooter().addClass('hidden');
500     };
502     /**
503      * Show the footer element.
504      *
505      * @method showFooter
506      */
507     Modal.prototype.showFooter = function() {
508         this.getFooter().removeClass('hidden');
509     };
511     /**
512      * Mark the modal as a large modal.
513      *
514      * @method setLarge
515      */
516     Modal.prototype.setLarge = function() {
517         if (this.isLarge()) {
518             return;
519         }
521         this.getModal().addClass('modal-lg');
522     };
524     /**
525      * Check if the modal is a large modal.
526      *
527      * @method isLarge
528      * @return {bool}
529      */
530     Modal.prototype.isLarge = function() {
531         return this.getModal().hasClass('modal-lg');
532     };
534     /**
535      * Mark the modal as a small modal.
536      *
537      * @method setSmall
538      */
539     Modal.prototype.setSmall = function() {
540         if (this.isSmall()) {
541             return;
542         }
544         this.getModal().removeClass('modal-lg');
545     };
547     /**
548      * Check if the modal is a small modal.
549      *
550      * @method isSmall
551      * @return {bool}
552      */
553     Modal.prototype.isSmall = function() {
554         return !this.getModal().hasClass('modal-lg');
555     };
557     /**
558      * Set this modal to be scrollable or not.
559      *
560      * @method setScrollable
561      * @param {bool} value Whether the modal is scrollable or not
562      */
563     Modal.prototype.setScrollable = function(value) {
564         if (!value) {
565             this.getModal()[0].classList.remove('modal-dialog-scrollable');
566             return;
567         }
569         this.getModal()[0].classList.add('modal-dialog-scrollable');
570     };
573     /**
574      * Determine the highest z-index value currently on the page.
575      *
576      * @method calculateZIndex
577      * @return {int}
578      */
579     Modal.prototype.calculateZIndex = function() {
580         var items = $(SELECTORS.DIALOG + ', ' + SELECTORS.MENU_BAR + ', ' + SELECTORS.HAS_Z_INDEX);
581         var zIndex = parseInt(this.root.css('z-index'));
583         items.each(function(index, item) {
584             item = $(item);
585             // Note that webkit browsers won't return the z-index value from the CSS stylesheet
586             // if the element doesn't have a position specified. Instead it'll return "auto".
587             var itemZIndex = item.css('z-index') ? parseInt(item.css('z-index')) : 0;
589             if (itemZIndex > zIndex) {
590                 zIndex = itemZIndex;
591             }
592         });
594         return zIndex;
595     };
597     /**
598      * Check if this modal is visible.
599      *
600      * @method isVisible
601      * @return {bool}
602      */
603     Modal.prototype.isVisible = function() {
604         return this.root.hasClass('show');
605     };
607     /**
608      * Check if this modal has focus.
609      *
610      * @method hasFocus
611      * @return {bool}
612      */
613     Modal.prototype.hasFocus = function() {
614         var target = $(document.activeElement);
615         return this.root.is(target) || this.root.has(target).length;
616     };
618     /**
619      * Check if this modal has CSS transitions applied.
620      *
621      * @method hasTransitions
622      * @return {bool}
623      */
624     Modal.prototype.hasTransitions = function() {
625         return this.getRoot().hasClass('fade');
626     };
628     /**
629      * Display this modal. The modal will be attached to the DOM if it hasn't
630      * already been.
631      *
632      * @method show
633      * @returns {Promise}
634      */
635     Modal.prototype.show = function() {
636         if (this.isVisible()) {
637             return $.Deferred().resolve();
638         }
640         var pendingPromise = new Pending('core/modal:show');
642         if (this.hasFooterContent()) {
643             this.showFooter();
644         } else {
645             this.hideFooter();
646         }
648         if (!this.isAttached) {
649             this.attachToDOM();
650         }
652         return this.getBackdrop()
653         .then(function(backdrop) {
654             var currentIndex = this.calculateZIndex();
655             var newIndex = currentIndex + 2;
656             var newBackdropIndex = newIndex - 1;
657             this.root.css('z-index', newIndex);
658             backdrop.setZIndex(newBackdropIndex);
659             backdrop.show();
661             this.root.removeClass('hide').addClass('show');
662             this.accessibilityShow();
663             this.getModal().focus();
664             $('body').addClass('modal-open');
665             this.root.trigger(ModalEvents.shown, this);
667             return;
668         }.bind(this))
669         .then(pendingPromise.resolve);
670     };
672     /**
673      * Hide this modal if it does not contain a form.
674      *
675      * @method hideIfNotForm
676      */
677     Modal.prototype.hideIfNotForm = function() {
678         var formElement = this.modal.find(SELECTORS.FORM);
679         if (formElement.length == 0) {
680             this.hide();
681         }
682     };
684     /**
685      * Hide this modal.
686      *
687      * @method hide
688      */
689     Modal.prototype.hide = function() {
690         this.getBackdrop().done(function(backdrop) {
691             FocusLock.untrapFocus();
692             if (!this.countOtherVisibleModals()) {
693                 // Hide the backdrop if we're the last open modal.
694                 backdrop.hide();
695                 $('body').removeClass('modal-open');
696             }
698             var currentIndex = parseInt(this.root.css('z-index'));
699             this.root.css('z-index', '');
700             backdrop.setZIndex(currentIndex - 3);
702             this.accessibilityHide();
704             if (this.hasTransitions()) {
705                 // Wait for CSS transitions to complete before hiding the element.
706                 this.getRoot().one('transitionend webkitTransitionEnd oTransitionEnd', function() {
707                     this.getRoot().removeClass('show').addClass('hide');
708                 }.bind(this));
709             } else {
710                 this.getRoot().removeClass('show').addClass('hide');
711             }
713             this.root.trigger(ModalEvents.hidden, this);
714         }.bind(this));
715     };
717     /**
718      * Remove this modal from the DOM.
719      *
720      * @method destroy
721      */
722     Modal.prototype.destroy = function() {
723         this.hide();
724         this.root.remove();
725         this.root.trigger(ModalEvents.destroyed, this);
726     };
728     /**
729      * Sets the appropriate aria attributes on this dialogue and the other
730      * elements in the DOM to ensure that screen readers are able to navigate
731      * the dialogue popup correctly.
732      *
733      * @method accessibilityShow
734      */
735     Modal.prototype.accessibilityShow = function() {
736         Aria.hideSiblings(this.root.get()[0]);
738         // Make us visible to screen readers.
739         this.root.removeAttr('aria-hidden');
740     };
742     /**
743      * Restores the aria visibility on the DOM elements changed when displaying
744      * the dialogue popup and makes the dialogue aria hidden to allow screen
745      * readers to navigate the main page correctly when the dialogue is closed.
746      *
747      * @method accessibilityHide
748      */
749     Modal.prototype.accessibilityHide = function() {
750         this.root.attr('aria-hidden', 'true');
752         Aria.unhideSiblings(this.root.get()[0]);
753     };
755     /**
756      * Set up all of the event handling for the modal.
757      *
758      * @method registerEventListeners
759      */
760     Modal.prototype.registerEventListeners = function() {
761         this.getRoot().on('keydown', function(e) {
762             if (!this.isVisible()) {
763                 return;
764             }
766             if (e.keyCode == KeyCodes.escape) {
767                 this.hide();
768             }
769         }.bind(this));
771         // Listen for clicks on the modal container.
772         this.getRoot().click(function(e) {
773             // If the click wasn't inside the modal element then we should
774             // hide the modal.
775             if (!$(e.target).closest(SELECTORS.MODAL).length) {
776                 // The check above fails to detect the click was inside the modal when the DOM tree is already changed.
777                 // So, we check if we can still find the container element or not. If not, then the DOM tree is changed.
778                 // It's best not to hide the modal in that case.
779                 if ($(e.target).closest(SELECTORS.CONTAINER).length) {
780                     this.hideIfNotForm();
781                 }
782             }
783         }.bind(this));
785         CustomEvents.define(this.getModal(), [CustomEvents.events.activate]);
786         this.getModal().on(CustomEvents.events.activate, SELECTORS.HIDE, function(e, data) {
787             this.hide();
788             data.originalEvent.preventDefault();
789         }.bind(this));
790     };
792     /**
793      * Register a listener to close the dialogue when the cancel button is pressed.
794      *
795      * @method registerCloseOnCancel
796      */
797     Modal.prototype.registerCloseOnCancel = function() {
798         // Handle the clicking of the Cancel button.
799         this.getModal().on(CustomEvents.events.activate, this.getActionSelector('cancel'), function(e, data) {
800             var cancelEvent = $.Event(ModalEvents.cancel);
801             this.getRoot().trigger(cancelEvent, this);
803             if (!cancelEvent.isDefaultPrevented()) {
804                 data.originalEvent.preventDefault();
806                 if (this.removeOnClose) {
807                     this.destroy();
808                 } else {
809                     this.hide();
810                 }
811             }
812         }.bind(this));
813     };
815     /**
816      * Register a listener to close the dialogue when the save button is pressed.
817      *
818      * @method registerCloseOnSave
819      */
820     Modal.prototype.registerCloseOnSave = function() {
821         // Handle the clicking of the Cancel button.
822         this.getModal().on(CustomEvents.events.activate, this.getActionSelector('save'), function(e, data) {
823             var saveEvent = $.Event(ModalEvents.save);
824             this.getRoot().trigger(saveEvent, this);
826             if (!saveEvent.isDefaultPrevented()) {
827                 data.originalEvent.preventDefault();
829                 if (this.removeOnClose) {
830                     this.destroy();
831                 } else {
832                     this.hide();
833                 }
834             }
835         }.bind(this));
836     };
838     /**
839      * Set or resolve and set the value using the function.
840      *
841      * @method asyncSet
842      * @param {(string|object)} value The string or jQuery promise.
843      * @param {function} setFunction The setter
844      * @return {Promise}
845      */
846     Modal.prototype.asyncSet = function(value, setFunction) {
847         var p = value;
848         if (typeof value !== 'object' || !value.hasOwnProperty('then')) {
849             p = $.Deferred();
850             p.resolve(value);
851         }
853         p.then(function(content) {
854             setFunction(content);
856             return;
857         })
858         .fail(Notification.exception);
860         return p;
861     };
863     /**
864      * Set the title text of a button.
865      *
866      * This method is overloaded to take either a string value for the button title or a jQuery promise that is resolved with
867      * text most commonly from a Str.get_string call.
868      *
869      * @param {DOMString} action The action of the button
870      * @param {(String|object)} value The button text, or a promise which will resolve to it
871      * @returns {Promise}
872      */
873     Modal.prototype.setButtonText = function(action, value) {
874         const button = this.getFooter().find(this.getActionSelector(action));
876         if (!button) {
877             throw new Error("Unable to find the '" + action + "' button");
878         }
880         return this.asyncSet(value, button.text.bind(button));
881     };
883     /**
884      * Get the Selector for an action.
885      *
886      * @param {String} action
887      * @returns {DOMString}
888      */
889     Modal.prototype.getActionSelector = function(action) {
890         return "[data-action='" + action + "']";
891     };
893     /**
894      * Set the flag to remove the modal from the DOM on close.
895      *
896      * @param {Boolean} remove
897      */
898     Modal.prototype.setRemoveOnClose = function(remove) {
899         this.removeOnClose = remove;
900     };
902     return Modal;
903 });