MDL-68390 aria: Add new core_aria module
[moodle.git] / lib / amd / src / modal.js
CommitLineData
2bcef559
RW
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/>.
15
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 */
c50bc1bf
AN
25define([
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',
604887ce 35 'core/pending',
4f1c8ce7
AN
36 'core/aria',
37], function($, Templates, Notification, KeyCodes, CustomEvents, ModalBackdrop, Event, ModalEvents, FocusLock, Pending, Aria) {
2bcef559
RW
38
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]',
cdc73904 48 FORM: 'form',
2bcef559
RW
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 };
53
54 var TEMPLATES = {
55 LOADING: 'core/loading',
56 BACKDROP: 'core/modal_backdrop',
57 };
58
59 /**
60 * Module singleton for the backdrop to be reused by all Modal instances.
61 */
62 var backdropPromise;
63
946f9d0a
RW
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;
69
2bcef559
RW
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);
e6a52983 79 this.headerPromise = $.Deferred();
2bcef559 80 this.title = this.header.find(SELECTORS.TITLE);
e6a52983 81 this.titlePromise = $.Deferred();
2bcef559 82 this.body = this.modal.find(SELECTORS.BODY);
e6a52983 83 this.bodyPromise = $.Deferred();
2bcef559 84 this.footer = this.modal.find(SELECTORS.FOOTER);
e6a52983 85 this.footerPromise = $.Deferred();
2bcef559
RW
86 this.hiddenSiblings = [];
87 this.isAttached = false;
88 this.bodyJS = null;
89 this.footerJS = null;
946f9d0a 90 this.modalCount = modalCounter++;
2bcef559
RW
91
92 if (!this.root.is(SELECTORS.CONTAINER)) {
93 Notification.exception({message: 'Element is not a modal container'});
94 }
95
96 if (!this.modal.length) {
97 Notification.exception({message: 'Container does not contain a modal'});
98 }
99
100 if (!this.header.length) {
101 Notification.exception({message: 'Modal is missing a header region'});
102 }
103
104 if (!this.title.length) {
105 Notification.exception({message: 'Modal header is missing a title region'});
106 }
107
108 if (!this.body.length) {
109 Notification.exception({message: 'Modal is missing a body region'});
110 }
111
112 if (!this.footer.length) {
113 Notification.exception({message: 'Modal is missing a footer region'});
114 }
115
116 this.registerEventListeners();
117 };
118
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 }
129
130 $('body').append(this.root);
c50bc1bf 131 FocusLock.trapFocus(this.root[0]);
2bcef559
RW
132
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 }
139
140 if (this.footerJS) {
141 Templates.runTemplateJS(this.footerJS);
142 this.footerJS = null;
143 }
144
145 this.isAttached = true;
146 };
147
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);
158
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));
164
165 return count;
166 };
167
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);
179
180 return new ModalBackdrop(element);
181 })
182 .fail(Notification.exception);
183 }
184
185 return backdropPromise;
186 };
187
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 };
197
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 };
207
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 };
217
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 };
227
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 };
237
e6a52983
MM
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 };
247
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 };
257
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 };
267
946f9d0a
RW
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 };
277
2bcef559
RW
278 /**
279 * Set the modal title element.
280 *
e2b50304
AN
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 *
2bcef559 284 * @method setTitle
e2b50304 285 * @param {(string|object)} value The title string or jQuery promise which resolves to the title.
2bcef559
RW
286 */
287 Modal.prototype.setTitle = function(value) {
288 var title = this.getTitle();
e6a52983 289 this.titlePromise = $.Deferred();
e2b50304 290
e6a52983
MM
291 this.asyncSet(value, title.html.bind(title))
292 .then(function() {
293 this.titlePromise.resolve(title);
294 }.bind(this))
295 .catch(Notification.exception);
2bcef559
RW
296 };
297
298 /**
299 * Set the modal body element.
300 *
e2b50304
AN
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.
2bcef559
RW
303 *
304 * @method setBody
e2b50304 305 * @param {(string|object)} value The body string or jQuery promise which resolves to the body.
2bcef559
RW
306 */
307 Modal.prototype.setBody = function(value) {
e6a52983
MM
308 this.bodyPromise = $.Deferred();
309
2bcef559
RW
310 var body = this.getBody();
311
312 if (typeof value === 'string') {
313 // Just set the value if it's a string.
314 body.html(value);
f02e119a 315 Event.notifyFilterContentUpdated(body);
97c4a29d 316 this.getRoot().trigger(ModalEvents.bodyRendered, this);
e6a52983 317 this.bodyPromise.resolve(body);
2bcef559 318 } else {
946f9d0a
RW
319 var jsPendingId = 'amd-modal-js-pending-id-' + this.getModalCount();
320 M.util.js_pending(jsPendingId);
2bcef559
RW
321 // Otherwise we assume it's a promise to be resolved with
322 // html and javascript.
946f9d0a
RW
323 var contentPromise = null;
324 body.css('overflow', 'hidden');
325
f2d033a2
MM
326 // Ensure that the `value` is a jQuery Promise.
327 value = $.when(value);
328
946f9d0a
RW
329 if (value.state() == 'pending') {
330 // We're still waiting for the body promise to resolve so
331 // let's show a loading icon.
2328bccc
RW
332 var height = body.innerHeight();
333 if (height < 100) {
334 height = 100;
335 }
336
337 body.animate({height: height + 'px'}, 150);
946f9d0a
RW
338
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);
345
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 }
2bcef559 365
946f9d0a
RW
366 // Now we can actually display the content.
367 contentPromise.then(function(html, js) {
368 var result = null;
2bcef559 369
946f9d0a
RW
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 }
392
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;
2bcef559 400 }
946f9d0a 401 }
035bd996
RW
402
403 return result;
404 }.bind(this))
405 .then(function(result) {
946f9d0a
RW
406 Event.notifyFilterContentUpdated(body);
407 this.getRoot().trigger(ModalEvents.bodyRendered, this);
946f9d0a
RW
408 return result;
409 }.bind(this))
e6a52983
MM
410 .then(function() {
411 this.bodyPromise.resolve(body);
412 return;
413 }.bind(this))
946f9d0a
RW
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);
422
423 return;
4c250a5b
AN
424 })
425 .fail(Notification.exception);
2bcef559
RW
426 }
427 };
428
429 /**
368832d5
RW
430 * Set the modal footer element. The footer element is made visible, if it
431 * isn't already.
2bcef559
RW
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) {
368832d5
RW
441 // Make sure the footer is visible.
442 this.showFooter();
e6a52983 443 this.footerPromise = $.Deferred();
368832d5 444
2bcef559
RW
445 var footer = this.getFooter();
446
447 if (typeof value === 'string') {
448 // Just set the value if it's a string.
449 footer.html(value);
e6a52983 450 this.footerPromise.resolve(footer);
2bcef559
RW
451 } else {
452 // Otherwise we assume it's a promise to be resolved with
453 // html and javascript.
e6a52983
MM
454 Templates.render(TEMPLATES.LOADING, {})
455 .then(function(html) {
2bcef559
RW
456 footer.html(html);
457
e6a52983
MM
458 return value;
459 })
460 .then(function(html, js) {
461 footer.html(html);
462
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;
2bcef559 470 }
e6a52983
MM
471 }
472
473 return footer;
474 }.bind(this))
475 .then(function(footer) {
476 this.footerPromise.resolve(footer);
477 return;
478 }.bind(this))
479 .catch(Notification.exception);
2bcef559
RW
480 }
481 };
482
368832d5
RW
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 };
492
493 /**
494 * Hide the footer element.
495 *
496 * @method hideFooter
497 */
498 Modal.prototype.hideFooter = function() {
499 this.getFooter().addClass('hidden');
500 };
501
502 /**
503 * Show the footer element.
504 *
505 * @method showFooter
506 */
507 Modal.prototype.showFooter = function() {
508 this.getFooter().removeClass('hidden');
509 };
510
2bcef559
RW
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 }
520
4defa05f 521 this.getModal().addClass('modal-lg');
2bcef559
RW
522 };
523
524 /**
525 * Check if the modal is a large modal.
526 *
527 * @method isLarge
528 * @return {bool}
529 */
530 Modal.prototype.isLarge = function() {
4defa05f 531 return this.getModal().hasClass('modal-lg');
2bcef559
RW
532 };
533
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 }
543
4defa05f 544 this.getModal().removeClass('modal-lg');
2bcef559
RW
545 };
546
547 /**
548 * Check if the modal is a small modal.
549 *
550 * @method isSmall
551 * @return {bool}
552 */
553 Modal.prototype.isSmall = function() {
4defa05f 554 return !this.getModal().hasClass('modal-lg');
2bcef559
RW
555 };
556
e13cbf9e
MG
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 }
568
569 this.getModal()[0].classList.add('modal-dialog-scrollable');
570 };
571
572
2bcef559
RW
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'));
582
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;
588
589 if (itemZIndex > zIndex) {
590 zIndex = itemZIndex;
591 }
592 });
593
594 return zIndex;
595 };
596
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 };
606
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 };
617
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 };
627
628 /**
629 * Display this modal. The modal will be attached to the DOM if it hasn't
630 * already been.
631 *
632 * @method show
fc3d7d69 633 * @returns {Promise}
2bcef559
RW
634 */
635 Modal.prototype.show = function() {
636 if (this.isVisible()) {
fc3d7d69 637 return $.Deferred().resolve();
2bcef559
RW
638 }
639
604887ce
AN
640 var pendingPromise = new Pending('core/modal:show');
641
368832d5
RW
642 if (this.hasFooterContent()) {
643 this.showFooter();
644 } else {
645 this.hideFooter();
646 }
647
2bcef559
RW
648 if (!this.isAttached) {
649 this.attachToDOM();
650 }
651
fc3d7d69 652 return this.getBackdrop()
604887ce 653 .then(function(backdrop) {
2bcef559
RW
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();
660
661 this.root.removeClass('hide').addClass('show');
662 this.accessibilityShow();
ae0629d2 663 this.getModal().focus();
2bcef559
RW
664 $('body').addClass('modal-open');
665 this.root.trigger(ModalEvents.shown, this);
604887ce
AN
666
667 return;
668 }.bind(this))
669 .then(pendingPromise.resolve);
2bcef559
RW
670 };
671
cdc73904
DW
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 };
683
2bcef559
RW
684 /**
685 * Hide this modal.
686 *
687 * @method hide
688 */
689 Modal.prototype.hide = function() {
2bcef559 690 this.getBackdrop().done(function(backdrop) {
c50bc1bf 691 FocusLock.untrapFocus();
2bcef559
RW
692 if (!this.countOtherVisibleModals()) {
693 // Hide the backdrop if we're the last open modal.
694 backdrop.hide();
695 $('body').removeClass('modal-open');
696 }
697
698 var currentIndex = parseInt(this.root.css('z-index'));
699 this.root.css('z-index', '');
700 backdrop.setZIndex(currentIndex - 3);
701
702 this.accessibilityHide();
703
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 }
712
713 this.root.trigger(ModalEvents.hidden, this);
714 }.bind(this));
715 };
716
717 /**
718 * Remove this modal from the DOM.
719 *
720 * @method destroy
721 */
722 Modal.prototype.destroy = function() {
fa6101ba 723 this.hide();
2bcef559
RW
724 this.root.remove();
725 this.root.trigger(ModalEvents.destroyed, this);
726 };
727
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() {
4f1c8ce7 736 Aria.hideSiblings(this.root.get()[0]);
2bcef559
RW
737
738 // Make us visible to screen readers.
4f1c8ce7 739 this.root.removeAttr('aria-hidden');
2bcef559
RW
740 };
741
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');
751
4f1c8ce7 752 Aria.unhideSiblings(this.root.get()[0]);
2bcef559
RW
753 };
754
2bcef559
RW
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 }
765
c50bc1bf 766 if (e.keyCode == KeyCodes.escape) {
2bcef559
RW
767 this.hide();
768 }
769 }.bind(this));
770
30e1f5a0
TQ
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) {
b6ece79d
SR
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) {
cdc73904 780 this.hideIfNotForm();
b6ece79d 781 }
30e1f5a0
TQ
782 }
783 }.bind(this));
784
2bcef559
RW
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 };
791
fa6101ba
AN
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);
802
803 if (!cancelEvent.isDefaultPrevented()) {
804 data.originalEvent.preventDefault();
805
806 if (this.removeOnClose) {
807 this.destroy();
808 } else {
809 this.hide();
810 }
811 }
812 }.bind(this));
813 };
814
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);
825
826 if (!saveEvent.isDefaultPrevented()) {
827 data.originalEvent.preventDefault();
828
829 if (this.removeOnClose) {
830 this.destroy();
831 } else {
832 this.hide();
833 }
834 }
835 }.bind(this));
836 };
837
e2b50304
AN
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;
e5bdf51c 848 if (typeof value !== 'object' || !value.hasOwnProperty('then')) {
e2b50304
AN
849 p = $.Deferred();
850 p.resolve(value);
851 }
852
853 p.then(function(content) {
854 setFunction(content);
855
856 return;
4c250a5b
AN
857 })
858 .fail(Notification.exception);
e2b50304
AN
859
860 return p;
861 };
862
92810f7a
AN
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));
875
876 if (!button) {
877 throw new Error("Unable to find the '" + action + "' button");
878 }
879
880 return this.asyncSet(value, button.text.bind(button));
881 };
fa6101ba
AN
882
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 };
892
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 };
901
2bcef559
RW
902 return Modal;
903});