on-demand release 3.9dev+
[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
AN
35 'core/pending',
36], function($, Templates, Notification, KeyCodes, CustomEvents, ModalBackdrop, Event, ModalEvents, FocusLock, Pending) {
2bcef559
RW
37
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]',
cdc73904 47 FORM: 'form',
2bcef559
RW
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 };
52
53 var TEMPLATES = {
54 LOADING: 'core/loading',
55 BACKDROP: 'core/modal_backdrop',
56 };
57
58 /**
59 * Module singleton for the backdrop to be reused by all Modal instances.
60 */
61 var backdropPromise;
62
946f9d0a
RW
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;
68
2bcef559
RW
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);
e6a52983 78 this.headerPromise = $.Deferred();
2bcef559 79 this.title = this.header.find(SELECTORS.TITLE);
e6a52983 80 this.titlePromise = $.Deferred();
2bcef559 81 this.body = this.modal.find(SELECTORS.BODY);
e6a52983 82 this.bodyPromise = $.Deferred();
2bcef559 83 this.footer = this.modal.find(SELECTORS.FOOTER);
e6a52983 84 this.footerPromise = $.Deferred();
2bcef559
RW
85 this.hiddenSiblings = [];
86 this.isAttached = false;
87 this.bodyJS = null;
88 this.footerJS = null;
946f9d0a 89 this.modalCount = modalCounter++;
2bcef559
RW
90
91 if (!this.root.is(SELECTORS.CONTAINER)) {
92 Notification.exception({message: 'Element is not a modal container'});
93 }
94
95 if (!this.modal.length) {
96 Notification.exception({message: 'Container does not contain a modal'});
97 }
98
99 if (!this.header.length) {
100 Notification.exception({message: 'Modal is missing a header region'});
101 }
102
103 if (!this.title.length) {
104 Notification.exception({message: 'Modal header is missing a title region'});
105 }
106
107 if (!this.body.length) {
108 Notification.exception({message: 'Modal is missing a body region'});
109 }
110
111 if (!this.footer.length) {
112 Notification.exception({message: 'Modal is missing a footer region'});
113 }
114
115 this.registerEventListeners();
116 };
117
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 }
128
129 $('body').append(this.root);
c50bc1bf 130 FocusLock.trapFocus(this.root[0]);
2bcef559
RW
131
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 }
138
139 if (this.footerJS) {
140 Templates.runTemplateJS(this.footerJS);
141 this.footerJS = null;
142 }
143
144 this.isAttached = true;
145 };
146
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);
157
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));
163
164 return count;
165 };
166
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);
178
179 return new ModalBackdrop(element);
180 })
181 .fail(Notification.exception);
182 }
183
184 return backdropPromise;
185 };
186
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 };
196
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 };
206
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 };
216
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 };
226
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 };
236
e6a52983
MM
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 };
246
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 };
256
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 };
266
946f9d0a
RW
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 };
276
2bcef559
RW
277 /**
278 * Set the modal title element.
279 *
e2b50304
AN
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 *
2bcef559 283 * @method setTitle
e2b50304 284 * @param {(string|object)} value The title string or jQuery promise which resolves to the title.
2bcef559
RW
285 */
286 Modal.prototype.setTitle = function(value) {
287 var title = this.getTitle();
e6a52983 288 this.titlePromise = $.Deferred();
e2b50304 289
e6a52983
MM
290 this.asyncSet(value, title.html.bind(title))
291 .then(function() {
292 this.titlePromise.resolve(title);
293 }.bind(this))
294 .catch(Notification.exception);
2bcef559
RW
295 };
296
297 /**
298 * Set the modal body element.
299 *
e2b50304
AN
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.
2bcef559
RW
302 *
303 * @method setBody
e2b50304 304 * @param {(string|object)} value The body string or jQuery promise which resolves to the body.
2bcef559
RW
305 */
306 Modal.prototype.setBody = function(value) {
e6a52983
MM
307 this.bodyPromise = $.Deferred();
308
2bcef559
RW
309 var body = this.getBody();
310
311 if (typeof value === 'string') {
312 // Just set the value if it's a string.
313 body.html(value);
f02e119a 314 Event.notifyFilterContentUpdated(body);
97c4a29d 315 this.getRoot().trigger(ModalEvents.bodyRendered, this);
e6a52983 316 this.bodyPromise.resolve(body);
2bcef559 317 } else {
946f9d0a
RW
318 var jsPendingId = 'amd-modal-js-pending-id-' + this.getModalCount();
319 M.util.js_pending(jsPendingId);
2bcef559
RW
320 // Otherwise we assume it's a promise to be resolved with
321 // html and javascript.
946f9d0a
RW
322 var contentPromise = null;
323 body.css('overflow', 'hidden');
324
325 if (value.state() == 'pending') {
326 // We're still waiting for the body promise to resolve so
327 // let's show a loading icon.
2328bccc
RW
328 var height = body.innerHeight();
329 if (height < 100) {
330 height = 100;
331 }
332
333 body.animate({height: height + 'px'}, 150);
946f9d0a
RW
334
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);
341
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 }
2bcef559 361
946f9d0a
RW
362 // Now we can actually display the content.
363 contentPromise.then(function(html, js) {
364 var result = null;
2bcef559 365
946f9d0a
RW
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 }
388
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;
2bcef559 396 }
946f9d0a 397 }
035bd996
RW
398
399 return result;
400 }.bind(this))
401 .then(function(result) {
946f9d0a
RW
402 Event.notifyFilterContentUpdated(body);
403 this.getRoot().trigger(ModalEvents.bodyRendered, this);
946f9d0a
RW
404 return result;
405 }.bind(this))
e6a52983
MM
406 .then(function() {
407 this.bodyPromise.resolve(body);
408 return;
409 }.bind(this))
946f9d0a
RW
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);
418
419 return;
4c250a5b
AN
420 })
421 .fail(Notification.exception);
2bcef559
RW
422 }
423 };
424
425 /**
368832d5
RW
426 * Set the modal footer element. The footer element is made visible, if it
427 * isn't already.
2bcef559
RW
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) {
368832d5
RW
437 // Make sure the footer is visible.
438 this.showFooter();
e6a52983 439 this.footerPromise = $.Deferred();
368832d5 440
2bcef559
RW
441 var footer = this.getFooter();
442
443 if (typeof value === 'string') {
444 // Just set the value if it's a string.
445 footer.html(value);
e6a52983 446 this.footerPromise.resolve(footer);
2bcef559
RW
447 } else {
448 // Otherwise we assume it's a promise to be resolved with
449 // html and javascript.
e6a52983
MM
450 Templates.render(TEMPLATES.LOADING, {})
451 .then(function(html) {
2bcef559
RW
452 footer.html(html);
453
e6a52983
MM
454 return value;
455 })
456 .then(function(html, js) {
457 footer.html(html);
458
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;
2bcef559 466 }
e6a52983
MM
467 }
468
469 return footer;
470 }.bind(this))
471 .then(function(footer) {
472 this.footerPromise.resolve(footer);
473 return;
474 }.bind(this))
475 .catch(Notification.exception);
2bcef559
RW
476 }
477 };
478
368832d5
RW
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 };
488
489 /**
490 * Hide the footer element.
491 *
492 * @method hideFooter
493 */
494 Modal.prototype.hideFooter = function() {
495 this.getFooter().addClass('hidden');
496 };
497
498 /**
499 * Show the footer element.
500 *
501 * @method showFooter
502 */
503 Modal.prototype.showFooter = function() {
504 this.getFooter().removeClass('hidden');
505 };
506
2bcef559
RW
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 }
516
4defa05f 517 this.getModal().addClass('modal-lg');
2bcef559
RW
518 };
519
520 /**
521 * Check if the modal is a large modal.
522 *
523 * @method isLarge
524 * @return {bool}
525 */
526 Modal.prototype.isLarge = function() {
4defa05f 527 return this.getModal().hasClass('modal-lg');
2bcef559
RW
528 };
529
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 }
539
4defa05f 540 this.getModal().removeClass('modal-lg');
2bcef559
RW
541 };
542
543 /**
544 * Check if the modal is a small modal.
545 *
546 * @method isSmall
547 * @return {bool}
548 */
549 Modal.prototype.isSmall = function() {
4defa05f 550 return !this.getModal().hasClass('modal-lg');
2bcef559
RW
551 };
552
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'));
562
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;
568
569 if (itemZIndex > zIndex) {
570 zIndex = itemZIndex;
571 }
572 });
573
574 return zIndex;
575 };
576
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 };
586
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 };
597
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 };
607
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 }
618
604887ce
AN
619 var pendingPromise = new Pending('core/modal:show');
620
368832d5
RW
621 if (this.hasFooterContent()) {
622 this.showFooter();
623 } else {
624 this.hideFooter();
625 }
626
2bcef559
RW
627 if (!this.isAttached) {
628 this.attachToDOM();
629 }
630
604887ce
AN
631 this.getBackdrop()
632 .then(function(backdrop) {
2bcef559
RW
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();
639
640 this.root.removeClass('hide').addClass('show');
641 this.accessibilityShow();
ae0629d2 642 this.getModal().focus();
2bcef559
RW
643 $('body').addClass('modal-open');
644 this.root.trigger(ModalEvents.shown, this);
604887ce
AN
645
646 return;
647 }.bind(this))
648 .then(pendingPromise.resolve);
2bcef559
RW
649 };
650
cdc73904
DW
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 };
662
2bcef559
RW
663 /**
664 * Hide this modal.
665 *
666 * @method hide
667 */
668 Modal.prototype.hide = function() {
2bcef559 669 this.getBackdrop().done(function(backdrop) {
c50bc1bf 670 FocusLock.untrapFocus();
2bcef559
RW
671 if (!this.countOtherVisibleModals()) {
672 // Hide the backdrop if we're the last open modal.
673 backdrop.hide();
674 $('body').removeClass('modal-open');
675 }
676
677 var currentIndex = parseInt(this.root.css('z-index'));
678 this.root.css('z-index', '');
679 backdrop.setZIndex(currentIndex - 3);
680
681 this.accessibilityHide();
682
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 }
691
692 this.root.trigger(ModalEvents.hidden, this);
693 }.bind(this));
694 };
695
696 /**
697 * Remove this modal from the DOM.
698 *
699 * @method destroy
700 */
701 Modal.prototype.destroy = function() {
702 this.root.remove();
703 this.root.trigger(ModalEvents.destroyed, this);
704 };
705
706 /**
707 * Sets the appropriate aria attributes on this dialogue and the other
708 * elements in the DOM to ensure that screen readers are able to navigate
709 * the dialogue popup correctly.
710 *
711 * @method accessibilityShow
712 */
713 Modal.prototype.accessibilityShow = function() {
714 // We need to get a list containing each sibling element and the shallowest
715 // non-ancestral nodes in the DOM. We can shortcut this a little by leveraging
716 // the fact that this dialogue is always appended to the document body therefore
717 // it's siblings are the shallowest non-ancestral nodes. If that changes then
718 // this code should also be updated.
719 $('body').children().each(function(index, child) {
720 // Skip the current modal.
721 if (!this.root.is(child)) {
722 child = $(child);
723 var hidden = child.attr('aria-hidden');
724 // If they are already hidden we can ignore them.
725 if (hidden !== 'true') {
726 // Save their current state.
727 child.data('previous-aria-hidden', hidden);
728 this.hiddenSiblings.push(child);
729
730 // Hide this node from screen readers.
731 child.attr('aria-hidden', 'true');
732 }
733 }
734 }.bind(this));
735
736 // Make us visible to screen readers.
737 this.root.attr('aria-hidden', 'false');
738 };
739
740 /**
741 * Restores the aria visibility on the DOM elements changed when displaying
742 * the dialogue popup and makes the dialogue aria hidden to allow screen
743 * readers to navigate the main page correctly when the dialogue is closed.
744 *
745 * @method accessibilityHide
746 */
747 Modal.prototype.accessibilityHide = function() {
748 this.root.attr('aria-hidden', 'true');
749
750 // Restore the sibling nodes back to their original values.
751 $.each(this.hiddenSiblings, function(index, sibling) {
752 sibling = $(sibling);
753 var previousValue = sibling.data('previous-aria-hidden');
754 // If the element didn't previously have an aria-hidden attribute
755 // then we can just remove the one we set.
756 if (typeof previousValue == 'undefined') {
757 sibling.removeAttr('aria-hidden');
758 } else {
759 // Otherwise set it back to the old value (which will be false).
760 sibling.attr('aria-hidden', previousValue);
761 }
762 });
763
764 // Clear the cache. No longer need to store these.
765 this.hiddenSiblings = [];
766 };
767
2bcef559
RW
768 /**
769 * Set up all of the event handling for the modal.
770 *
771 * @method registerEventListeners
772 */
773 Modal.prototype.registerEventListeners = function() {
774 this.getRoot().on('keydown', function(e) {
775 if (!this.isVisible()) {
776 return;
777 }
778
c50bc1bf 779 if (e.keyCode == KeyCodes.escape) {
2bcef559
RW
780 this.hide();
781 }
782 }.bind(this));
783
30e1f5a0
TQ
784 // Listen for clicks on the modal container.
785 this.getRoot().click(function(e) {
786 // If the click wasn't inside the modal element then we should
787 // hide the modal.
788 if (!$(e.target).closest(SELECTORS.MODAL).length) {
b6ece79d
SR
789 // The check above fails to detect the click was inside the modal when the DOM tree is already changed.
790 // So, we check if we can still find the container element or not. If not, then the DOM tree is changed.
791 // It's best not to hide the modal in that case.
792 if ($(e.target).closest(SELECTORS.CONTAINER).length) {
cdc73904 793 this.hideIfNotForm();
b6ece79d 794 }
30e1f5a0
TQ
795 }
796 }.bind(this));
797
2bcef559
RW
798 CustomEvents.define(this.getModal(), [CustomEvents.events.activate]);
799 this.getModal().on(CustomEvents.events.activate, SELECTORS.HIDE, function(e, data) {
800 this.hide();
801 data.originalEvent.preventDefault();
802 }.bind(this));
803 };
804
e2b50304
AN
805 /**
806 * Set or resolve and set the value using the function.
807 *
808 * @method asyncSet
809 * @param {(string|object)} value The string or jQuery promise.
810 * @param {function} setFunction The setter
811 * @return {Promise}
812 */
813 Modal.prototype.asyncSet = function(value, setFunction) {
814 var p = value;
e5bdf51c 815 if (typeof value !== 'object' || !value.hasOwnProperty('then')) {
e2b50304
AN
816 p = $.Deferred();
817 p.resolve(value);
818 }
819
820 p.then(function(content) {
821 setFunction(content);
822
823 return;
4c250a5b
AN
824 })
825 .fail(Notification.exception);
e2b50304
AN
826
827 return p;
828 };
829
2bcef559
RW
830 return Modal;
831});