Proyectos de Subversion Moodle

Rev

Rev 1 | | Comparar con el anterior | Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 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
 * @copyright  2016 Ryan Wyllie <ryan@moodle.com>
21
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
22
 */
23
 
24
import $ from 'jquery';
25
import * as Templates from 'core/templates';
26
import * as Notification from 'core/notification';
27
import * as KeyCodes from 'core/key_codes';
28
import ModalBackdrop from 'core/modal_backdrop';
29
import ModalEvents from 'core/modal_events';
30
import * as ModalRegistry from 'core/modal_registry';
31
import Pending from 'core/pending';
32
import * as CustomEvents from 'core/custom_interaction_events';
33
import * as FilterEvents from 'core_filters/events';
34
import * as FocusLock from 'core/local/aria/focuslock';
35
import * as Aria from 'core/aria';
36
import * as Fullscreen from 'core/fullscreen';
37
import {removeToastRegion} from './toast';
38
 
39
/**
40
 * A configuration to provide to the modal.
41
 *
42
 * @typedef {Object} ModalConfig
43
 *
44
 * @property {string} [type] The type of modal to create.
45
 * @property {string|Promise<string>} [title] The title of the modal.
46
 * @property {string|Promise<string>} [body] The body of the modal.
47
 * @property {string|Promise<string>} [footer] The footer of the modal.
48
 * @property {boolean} [show=false] Whether to show the modal immediately.
49
 * @property {boolean} [scrollable=true] Whether the modal should be scrollable.
50
 * @property {boolean} [removeOnClose=true] Whether the modal should be removed from the DOM when it is closed.
51
 * @property {Element|jQuery} [returnElement] The element to focus when closing the modal.
52
 * @property {boolean} [large=false] Whether the modal should be a large modal.
53
 * @property {boolean} [isVerticallyCentered=false] Whether the modal should be vertically centered.
54
 * @property {object} [buttons={}] The buttons to display in the footer as a key => title pair.
55
 */
56
 
57
const SELECTORS = {
58
    CONTAINER: '[data-region="modal-container"]',
59
    MODAL: '[data-region="modal"]',
60
    HEADER: '[data-region="header"]',
61
    TITLE: '[data-region="title"]',
62
    BODY: '[data-region="body"]',
63
    FOOTER: '[data-region="footer"]',
64
    HIDE: '[data-action="hide"]',
65
    DIALOG: '[role=dialog]',
66
    FORM: 'form',
67
    MENU_BAR: '[role=menubar]',
68
    HAS_Z_INDEX: '.moodle-has-zindex',
69
    CAN_RECEIVE_FOCUS: 'input:not([type="hidden"]), a[href], button, textarea, select, [tabindex]',
70
};
71
 
72
const TEMPLATES = {
73
    LOADING: 'core/loading',
74
    BACKDROP: 'core/modal_backdrop',
75
};
76
 
77
export default class Modal {
78
    /** @var {string} The type of modal */
79
    static TYPE = 'default';
80
 
81
    /** @var {string} The template to use for this modal */
82
    static TEMPLATE = 'core/modal';
83
 
84
    /** @var {Promise} Module singleton for the backdrop to be reused by all Modal instances */
85
    static backdropPromise = null;
86
 
87
    /**
88
     * @var {Number} A counter that gets incremented for each modal created.
89
     * This can be used to generate unique values for the modals.
90
     */
91
    static modalCounter = 0;
92
 
93
    /**
11 efrain 94
     * Getter method for .root element.
95
     * @return {object} jQuery object
96
     */
97
    get root() {
98
        return $(this._root.filter(SELECTORS.CONTAINER));
99
    }
100
 
101
    /**
102
     * Setter method for .root element.
103
     * @param {object} root jQuery object
104
     */
105
    set root(root) {
106
        this._root = root;
107
    }
108
 
109
    /**
1 efrain 110
     * Constructor for the Modal.
111
     *
112
     * @param {HTMLElement} root The HTMLElement at the root of the Modal content
113
     */
114
    constructor(root) {
115
        this.root = $(root);
116
 
117
        this.modal = this.root.find(SELECTORS.MODAL);
118
        this.header = this.modal.find(SELECTORS.HEADER);
119
        this.headerPromise = $.Deferred();
120
        this.title = this.header.find(SELECTORS.TITLE);
121
        this.titlePromise = $.Deferred();
122
        this.body = this.modal.find(SELECTORS.BODY);
123
        this.bodyPromise = $.Deferred();
124
        this.footer = this.modal.find(SELECTORS.FOOTER);
125
        this.footerPromise = $.Deferred();
126
        this.hiddenSiblings = [];
127
        this.isAttached = false;
128
        this.bodyJS = null;
129
        this.footerJS = null;
130
        this.modalCount = Modal.modalCounter++;
131
        this.attachmentPoint = document.createElement('div');
132
        document.body.append(this.attachmentPoint);
133
        this.focusOnClose = null;
134
 
135
        if (!this.root.is(SELECTORS.CONTAINER)) {
136
            Notification.exception({message: 'Element is not a modal container'});
137
        }
138
 
139
        if (!this.modal.length) {
140
            Notification.exception({message: 'Container does not contain a modal'});
141
        }
142
 
143
        if (!this.header.length) {
144
            Notification.exception({message: 'Modal is missing a header region'});
145
        }
146
 
147
        if (!this.title.length) {
148
            Notification.exception({message: 'Modal header is missing a title region'});
149
        }
150
 
151
        if (!this.body.length) {
152
            Notification.exception({message: 'Modal is missing a body region'});
153
        }
154
 
155
        if (!this.footer.length) {
156
            Notification.exception({message: 'Modal is missing a footer region'});
157
        }
158
 
159
        this.registerEventListeners();
160
    }
161
 
162
    /**
163
     * Register a modal with the legacy modal registry.
164
     *
165
     * This is provided to allow backwards-compatibility with existing code that uses the legacy modal registry.
166
     * It is not necessary to register modals for code only present in Moodle 4.3 and later.
167
     */
168
    static registerModalType() {
169
        if (!this.TYPE) {
170
            throw new Error(`Unknown modal type`, this);
171
        }
172
 
173
        if (!this.TEMPLATE) {
174
            throw new Error(`Unknown modal template`, this);
175
        }
176
        ModalRegistry.register(
177
            this.TYPE,
178
            this,
179
            this.TEMPLATE,
180
        );
181
    }
182
 
183
    /**
184
     * Create a new modal using the ModalFactory.
185
     * This is a shortcut to creating the modal.
186
     * Create a new modal using the supplied configuration.
187
     *
188
     * @param {ModalConfig} modalConfig
189
     * @returns {Promise<Modal>}
190
     */
191
    static async create(modalConfig = {}) {
192
        const pendingModalPromise = new Pending('core/modal_factory:create');
193
        modalConfig.type = this.TYPE;
194
 
195
        const templateName = this._getTemplateName(modalConfig);
196
        const templateContext = modalConfig.templateContext || {};
197
        const {html} = await Templates.renderForPromise(templateName, templateContext);
198
 
199
        const modal = new this(html);
200
        modal.configure(modalConfig);
201
 
202
        pendingModalPromise.resolve();
203
 
204
        return modal;
205
    }
206
 
207
    /**
208
     * A helper to get the template name for this modal.
209
     *
210
     * @param {ModalConfig} modalConfig
211
     * @returns {string}
212
     * @protected
213
     */
214
    static _getTemplateName(modalConfig) {
215
        if (modalConfig.template) {
216
            return modalConfig.template;
217
        }
218
 
219
        if (this.TEMPLATE) {
220
            return this.TEMPLATE;
221
        }
222
 
223
        if (ModalRegistry.has(this.TYPE)) {
224
            // Note: This is provided as an interim backwards-compatability layer and will be removed four releases after 4.3.
225
            window.console.warning(
226
                'Use of core/modal_registry is deprecated. ' +
227
                'Please define your modal template in a new static TEMPLATE property on your modal class.',
228
            );
229
            const config = ModalRegistry.get(this.TYPE);
230
            return config.template;
231
        }
232
 
233
        throw new Error(`Unable to determine template name for modal ${this.TYPE}`);
234
    }
235
 
236
    /**
237
     * Configure the modal.
238
     *
239
     * @param {ModalConfig} param0 The configuration options
240
     */
241
    configure({
242
        show = false,
243
        large = false,
244
        isVerticallyCentered = false,
245
        removeOnClose = false,
246
        scrollable = true,
247
        returnElement,
248
        title,
249
        body,
250
        footer,
251
        buttons = {},
252
    } = {}) {
253
        if (large) {
254
            this.setLarge();
255
        }
256
 
257
        if (isVerticallyCentered) {
258
            this.setVerticallyCentered();
259
        }
260
 
261
        // If configured remove the modal when hiding it.
262
        // Ideally this should be true, but we need to identify places that this breaks first.
263
        this.setRemoveOnClose(removeOnClose);
264
        this.setReturnElement(returnElement);
265
        this.setScrollable(scrollable);
266
 
267
        if (title !== undefined) {
268
            this.setTitle(title);
269
        }
270
 
271
        if (body !== undefined) {
272
            this.setBody(body);
273
        }
274
 
275
        if (footer !== undefined) {
276
            this.setFooter(footer);
277
        }
278
 
279
        Object.entries(buttons).forEach(([key, value]) => this.setButtonText(key, value));
280
 
281
        // If configured show the modal.
282
        if (show) {
283
            this.show();
284
        }
285
    }
286
 
287
    /**
288
     * Attach the modal to the correct part of the page.
289
     *
290
     * If it hasn't already been added it runs any
291
     * javascript that has been cached until now.
292
     *
293
     * @method attachToDOM
294
     */
295
    attachToDOM() {
11 efrain 296
        this.getAttachmentPoint().append(this._root);
1 efrain 297
 
298
        if (this.isAttached) {
299
            return;
300
        }
301
 
302
        FocusLock.trapFocus(this.root[0]);
303
 
304
        // If we'd cached any JS then we can run it how that the modal is
305
        // attached to the DOM.
306
        if (this.bodyJS) {
307
            Templates.runTemplateJS(this.bodyJS);
308
            this.bodyJS = null;
309
        }
310
 
311
        if (this.footerJS) {
312
            Templates.runTemplateJS(this.footerJS);
313
            this.footerJS = null;
314
        }
315
 
316
        this.isAttached = true;
317
    }
318
 
319
    /**
320
     * Count the number of other visible modals (not including this one).
321
     *
322
     * @method countOtherVisibleModals
323
     * @return {int}
324
     */
325
    countOtherVisibleModals() {
326
        let count = 0;
327
        $('body').find(SELECTORS.CONTAINER).each((index, element) => {
328
            element = $(element);
329
 
330
            // If we haven't found ourself and the element is visible.
331
            if (!this.root.is(element) && element.hasClass('show')) {
332
                count++;
333
            }
334
        });
335
 
336
        return count;
337
    }
338
 
339
    /**
340
     * Get the modal backdrop.
341
     *
342
     * @method getBackdrop
343
     * @return {object} jQuery promise
344
     */
345
    getBackdrop() {
346
        if (!Modal.backdropPromise) {
347
            Modal.backdropPromise = Templates.render(TEMPLATES.BACKDROP, {})
348
                .then((html) => new ModalBackdrop($(html)))
349
                .catch(Notification.exception);
350
        }
351
 
352
        return Modal.backdropPromise;
353
    }
354
 
355
    /**
356
     * Get the root element of this modal.
357
     *
358
     * @method getRoot
359
     * @return {object} jQuery object
360
     */
361
    getRoot() {
362
        return this.root;
363
    }
364
 
365
    /**
366
     * Get the modal element of this modal.
367
     *
368
     * @method getModal
369
     * @return {object} jQuery object
370
     */
371
    getModal() {
372
        return this.modal;
373
    }
374
 
375
    /**
376
     * Get the modal title element.
377
     *
378
     * @method getTitle
379
     * @return {object} jQuery object
380
     */
381
    getTitle() {
382
        return this.title;
383
    }
384
 
385
    /**
386
     * Get the modal body element.
387
     *
388
     * @method getBody
389
     * @return {object} jQuery object
390
     */
391
    getBody() {
392
        return this.body;
393
    }
394
 
395
    /**
396
     * Get the modal footer element.
397
     *
398
     * @method getFooter
399
     * @return {object} jQuery object
400
     */
401
    getFooter() {
402
        return this.footer;
403
    }
404
 
405
    /**
406
     * Get a promise resolving to the title region.
407
     *
408
     * @method getTitlePromise
409
     * @return {Promise}
410
     */
411
    getTitlePromise() {
412
        return this.titlePromise;
413
    }
414
 
415
    /**
416
     * Get a promise resolving to the body region.
417
     *
418
     * @method getBodyPromise
419
     * @return {object} jQuery object
420
     */
421
    getBodyPromise() {
422
        return this.bodyPromise;
423
    }
424
 
425
    /**
426
     * Get a promise resolving to the footer region.
427
     *
428
     * @method getFooterPromise
429
     * @return {object} jQuery object
430
     */
431
    getFooterPromise() {
432
        return this.footerPromise;
433
    }
434
 
435
    /**
436
     * Get the unique modal count.
437
     *
438
     * @method getModalCount
439
     * @return {int}
440
     */
441
    getModalCount() {
442
        return this.modalCount;
443
    }
444
 
445
    /**
446
     * Set the modal title element.
447
     *
448
     * This method is overloaded to take either a string value for the title or a jQuery promise that is resolved with
449
     * HTML most commonly from a Str.get_string call.
450
     *
451
     * @method setTitle
452
     * @param {(string|object)} value The title string or jQuery promise which resolves to the title.
453
     */
454
    setTitle(value) {
455
        const title = this.getTitle();
456
        this.titlePromise = $.Deferred();
457
 
458
        this.asyncSet(value, title.html.bind(title))
459
        .then(() => {
460
            this.titlePromise.resolve(title);
461
            return;
462
        })
463
        .catch(Notification.exception);
464
    }
465
 
466
    /**
467
     * Set the modal body element.
468
     *
469
     * This method is overloaded to take either a string value for the body or a jQuery promise that is resolved with
470
     * HTML and Javascript most commonly from a Templates.render call.
471
     *
472
     * @method setBody
473
     * @param {(string|object)} value The body string or jQuery promise which resolves to the body.
474
     * @fires event:filterContentUpdated
475
     */
476
    setBody(value) {
477
        this.bodyPromise = $.Deferred();
478
 
479
        const body = this.getBody();
480
 
481
        if (typeof value === 'string') {
482
            // Just set the value if it's a string.
483
            body.html(value);
484
            FilterEvents.notifyFilterContentUpdated(body);
485
            this.getRoot().trigger(ModalEvents.bodyRendered, this);
486
            this.bodyPromise.resolve(body);
487
        } else {
488
            const modalPromise = new Pending(`amd-modal-js-pending-id-${this.getModalCount()}`);
489
            // Otherwise we assume it's a promise to be resolved with
490
            // html and javascript.
491
            let contentPromise = null;
492
            body.css('overflow', 'hidden');
493
 
494
            // Ensure that the `value` is a jQuery Promise.
495
            value = $.when(value);
496
 
497
            if (value.state() == 'pending') {
498
                // We're still waiting for the body promise to resolve so
499
                // let's show a loading icon.
500
                let height = body.innerHeight();
501
                if (height < 100) {
502
                    height = 100;
503
                }
504
 
505
                body.animate({height: `${height}px`}, 150);
506
 
507
                body.html('');
508
                contentPromise = Templates.render(TEMPLATES.LOADING, {})
509
                    .then((html) => {
510
                        const loadingIcon = $(html).hide();
511
                        body.html(loadingIcon);
512
                        loadingIcon.fadeIn(150);
513
 
514
                        // We only want the loading icon to fade out
515
                        // when the content for the body has finished
516
                        // loading.
517
                        return $.when(loadingIcon.promise(), value);
518
                    })
519
                    .then((loadingIcon) => {
520
                        // Once the content has finished loading and
521
                        // the loading icon has been shown then we can
522
                        // fade the icon away to reveal the content.
523
                        return loadingIcon.fadeOut(100).promise();
524
                    })
525
                    .then(() => {
526
                        return value;
527
                    });
528
            } else {
529
                // The content is already loaded so let's just display
530
                // it to the user. No need for a loading icon.
531
                contentPromise = value;
532
            }
533
 
534
            // Now we can actually display the content.
535
            contentPromise.then((html, js) => {
536
                let result = null;
537
 
538
                if (this.isVisible()) {
539
                    // If the modal is visible then we should display
540
                    // the content gracefully for the user.
541
                    body.css('opacity', 0);
542
                    const currentHeight = body.innerHeight();
543
                    body.html(html);
544
                    // We need to clear any height values we've set here
545
                    // in order to measure the height of the content being
546
                    // added. This then allows us to animate the height
547
                    // transition.
548
                    body.css('height', '');
549
                    const newHeight = body.innerHeight();
550
                    body.css('height', `${currentHeight}px`);
551
                    result = body.animate(
552
                        {height: `${newHeight}px`, opacity: 1},
553
                        {duration: 150, queue: false}
554
                    ).promise();
555
                } else {
556
                    // Since the modal isn't visible we can just immediately
557
                    // set the content. No need to animate it.
558
                    body.html(html);
559
                }
560
 
561
                if (js) {
562
                    if (this.isAttached) {
563
                        // If we're in the DOM then run the JS immediately.
564
                        Templates.runTemplateJS(js);
565
                    } else {
566
                        // Otherwise cache it to be run when we're attached.
567
                        this.bodyJS = js;
568
                    }
569
                }
570
 
571
                return result;
572
            })
573
            .then((result) => {
574
                FilterEvents.notifyFilterContentUpdated(body);
575
                this.getRoot().trigger(ModalEvents.bodyRendered, this);
576
                return result;
577
            })
578
            .then(() => {
579
                this.bodyPromise.resolve(body);
580
                return;
581
            })
582
            .catch(Notification.exception)
583
            .always(() => {
584
                // When we're done displaying all of the content we need
585
                // to clear the custom values we've set here.
586
                body.css('height', '');
587
                body.css('overflow', '');
588
                body.css('opacity', '');
589
                modalPromise.resolve();
590
 
591
                return;
592
            });
593
        }
594
    }
595
 
596
    /**
597
     * Alternative to setBody() that can be used from non-Jquery modules
598
     *
599
     * @param {Promise} promise promise that returns {html, js} object
600
     * @return {Promise}
601
     */
602
    setBodyContent(promise) {
603
        // Call the leegacy API for now and pass it a jQuery Promise.
604
        // This is a non-spec feature of jQuery and cannot be produced with spec promises.
605
        // We can encourage people to migrate to this approach, and in future we can swap
606
        // it so that setBody() calls setBodyPromise().
607
        return promise.then(({html, js}) => this.setBody($.when(html, js)))
608
            .catch(exception => {
609
                this.hide();
610
                throw exception;
611
            });
612
    }
613
 
614
    /**
615
     * Set the modal footer element. The footer element is made visible, if it
616
     * isn't already.
617
     *
618
     * This method is overloaded to take either a string
619
     * value for the body or a jQuery promise that is resolved with HTML and Javascript
620
     * most commonly from a Templates.render call.
621
     *
622
     * @method setFooter
623
     * @param {(string|object)} value The footer string or jQuery promise
624
     */
625
    setFooter(value) {
626
        // Make sure the footer is visible.
627
        this.showFooter();
628
        this.footerPromise = $.Deferred();
629
 
630
        const footer = this.getFooter();
631
 
632
        if (typeof value === 'string') {
633
            // Just set the value if it's a string.
634
            footer.html(value);
635
            this.footerPromise.resolve(footer);
636
        } else {
637
            // Otherwise we assume it's a promise to be resolved with
638
            // html and javascript.
639
            Templates.render(TEMPLATES.LOADING, {})
640
            .then((html) => {
641
                footer.html(html);
642
 
643
                return value;
644
            })
645
            .then((html, js) => {
646
                footer.html(html);
647
 
648
                if (js) {
649
                    if (this.isAttached) {
650
                        // If we're in the DOM then run the JS immediately.
651
                        Templates.runTemplateJS(js);
652
                    } else {
653
                        // Otherwise cache it to be run when we're attached.
654
                        this.footerJS = js;
655
                    }
656
                }
657
 
658
                return footer;
659
            })
660
            .then((footer) => {
661
                this.footerPromise.resolve(footer);
662
                this.showFooter();
663
                return;
664
            })
665
            .catch(Notification.exception);
666
        }
667
    }
668
 
669
    /**
670
     * Check if the footer has any content in it.
671
     *
672
     * @method hasFooterContent
673
     * @return {bool}
674
     */
675
    hasFooterContent() {
676
        return this.getFooter().children().length ? true : false;
677
    }
678
 
679
    /**
680
     * Hide the footer element.
681
     *
682
     * @method hideFooter
683
     */
684
    hideFooter() {
685
        this.getFooter().addClass('hidden');
686
    }
687
 
688
    /**
689
     * Show the footer element.
690
     *
691
     * @method showFooter
692
     */
693
    showFooter() {
694
        this.getFooter().removeClass('hidden');
695
    }
696
 
697
    /**
698
     * Mark the modal as a large modal.
699
     *
700
     * @method setLarge
701
     */
702
    setLarge() {
703
        if (this.isLarge()) {
704
            return;
705
        }
706
 
707
        this.getModal().addClass('modal-lg');
708
    }
709
 
710
    /**
711
     * Mark the modal as a centered modal.
712
     *
713
     * @method setVerticallyCentered
714
     */
715
    setVerticallyCentered() {
716
        if (this.isVerticallyCentered()) {
717
            return;
718
        }
719
        this.getModal().addClass('modal-dialog-centered');
720
    }
721
 
722
    /**
723
     * Check if the modal is a large modal.
724
     *
725
     * @method isLarge
726
     * @return {bool}
727
     */
728
    isLarge() {
729
        return this.getModal().hasClass('modal-lg');
730
    }
731
 
732
    /**
733
     * Check if the modal is vertically centered.
734
     *
735
     * @method isVerticallyCentered
736
     * @return {bool}
737
     */
738
    isVerticallyCentered() {
739
        return this.getModal().hasClass('modal-dialog-centered');
740
    }
741
 
742
    /**
743
     * Mark the modal as a small modal.
744
     *
745
     * @method setSmall
746
     */
747
    setSmall() {
748
        if (this.isSmall()) {
749
            return;
750
        }
751
 
752
        this.getModal().removeClass('modal-lg');
753
    }
754
 
755
    /**
756
     * Check if the modal is a small modal.
757
     *
758
     * @method isSmall
759
     * @return {bool}
760
     */
761
    isSmall() {
762
        return !this.getModal().hasClass('modal-lg');
763
    }
764
 
765
    /**
766
     * Set this modal to be scrollable or not.
767
     *
768
     * @method setScrollable
769
     * @param {bool} value Whether the modal is scrollable or not
770
     */
771
    setScrollable(value) {
772
        if (!value) {
773
            this.getModal()[0].classList.remove('modal-dialog-scrollable');
774
            return;
775
        }
776
 
777
        this.getModal()[0].classList.add('modal-dialog-scrollable');
778
    }
779
 
780
 
781
    /**
782
     * Determine the highest z-index value currently on the page.
783
     *
784
     * @method calculateZIndex
785
     * @return {int}
786
     */
787
    calculateZIndex() {
788
        const items = $(`${SELECTORS.DIALOG}, ${SELECTORS.MENU_BAR}, ${SELECTORS.HAS_Z_INDEX}`);
789
        let zIndex = parseInt(this.root.css('z-index'));
790
 
791
        items.each((index, item) => {
792
            item = $(item);
793
            if (!item.is(':visible')) {
794
                // Do not include items which are not visible in the z-index calculation.
795
                // This is important because some dialogues are not removed from the DOM.
796
                return;
797
            }
798
            // Note that webkit browsers won't return the z-index value from the CSS stylesheet
799
            // if the element doesn't have a position specified. Instead it'll return "auto".
800
            const itemZIndex = item.css('z-index') ? parseInt(item.css('z-index')) : 0;
801
 
802
            if (itemZIndex > zIndex) {
803
                zIndex = itemZIndex;
804
            }
805
        });
806
 
807
        return zIndex;
808
    }
809
 
810
    /**
811
     * Check if this modal is visible.
812
     *
813
     * @method isVisible
814
     * @return {bool}
815
     */
816
    isVisible() {
817
        return this.root.hasClass('show');
818
    }
819
 
820
    /**
821
     * Check if this modal has focus.
822
     *
823
     * @method hasFocus
824
     * @return {bool}
825
     */
826
    hasFocus() {
827
        const target = $(document.activeElement);
828
        return this.root.is(target) || this.root.has(target).length;
829
    }
830
 
831
    /**
832
     * Check if this modal has CSS transitions applied.
833
     *
834
     * @method hasTransitions
835
     * @return {bool}
836
     */
837
    hasTransitions() {
838
        return this.getRoot().hasClass('fade');
839
    }
840
 
841
    /**
842
     * Gets the jQuery wrapped node that the Modal should be attached to.
843
     *
844
     * @returns {jQuery}
845
     */
846
    getAttachmentPoint() {
847
        return $(Fullscreen.getElement() || this.attachmentPoint);
848
    }
849
 
850
    /**
851
     * Display this modal. The modal will be attached to the DOM if it hasn't
852
     * already been.
853
     *
854
     * @method show
855
     * @returns {Promise}
856
     */
857
    show() {
858
        if (this.isVisible()) {
859
            return $.Deferred().resolve();
860
        }
861
 
862
        const pendingPromise = new Pending('core/modal:show');
863
 
864
        if (this.hasFooterContent()) {
865
            this.showFooter();
866
        } else {
867
            this.hideFooter();
868
        }
869
 
870
        this.attachToDOM();
871
 
872
        // If the focusOnClose was not set. Set the focus back to triggered element.
873
        if (!this.focusOnClose && document.activeElement) {
874
            this.focusOnClose = document.activeElement;
875
        }
876
 
877
        return this.getBackdrop()
878
        .then((backdrop) => {
879
            const currentIndex = this.calculateZIndex();
880
            const newIndex = currentIndex + 2;
881
            const newBackdropIndex = newIndex - 1;
882
            this.root.css('z-index', newIndex);
883
            backdrop.setZIndex(newBackdropIndex);
884
            backdrop.show();
885
 
886
            this.root.removeClass('hide').addClass('show');
887
            this.accessibilityShow();
888
            this.getModal().focus();
889
            $('body').addClass('modal-open');
890
            this.root.trigger(ModalEvents.shown, this);
891
 
892
            return;
893
        })
894
        .then(pendingPromise.resolve);
895
    }
896
 
897
    /**
898
     * Hide this modal if it does not contain a form.
899
     *
900
     * @method hideIfNotForm
901
     */
902
    hideIfNotForm() {
903
        const formElement = this.modal.find(SELECTORS.FORM);
904
        if (formElement.length == 0) {
905
            this.hide();
906
        }
907
    }
908
 
909
    /**
910
     * Hide this modal.
911
     *
912
     * @method hide
913
     */
914
    hide() {
915
        this.getBackdrop().done((backdrop) => {
916
            FocusLock.untrapFocus();
917
 
918
            if (!this.countOtherVisibleModals()) {
919
                // Hide the backdrop if we're the last open modal.
920
                backdrop.hide();
921
                $('body').removeClass('modal-open');
922
            }
923
 
924
            const currentIndex = parseInt(this.root.css('z-index'));
925
            this.root.css('z-index', '');
926
            backdrop.setZIndex(currentIndex - 3);
927
 
928
            this.accessibilityHide();
929
 
930
            if (this.hasTransitions()) {
931
                // Wait for CSS transitions to complete before hiding the element.
932
                this.getRoot().one('transitionend webkitTransitionEnd oTransitionEnd', () => {
933
                    this.getRoot().removeClass('show').addClass('hide');
934
                });
935
            } else {
936
                this.getRoot().removeClass('show').addClass('hide');
937
            }
938
 
939
            // Ensure the modal is moved onto the body node if it is still attached to the DOM.
940
            if ($(document.body).find(this.getRoot()).length) {
941
                $(document.body).append(this.getRoot());
942
            }
943
 
11 efrain 944
            // Closes popover elements that are inside the modal at the time the modal is closed.
945
            this.getRoot().find('[data-toggle="popover"]').each(function() {
946
                document.getElementById(this.getAttribute('aria-describedby'))?.remove();
947
            });
948
 
1 efrain 949
            this.root.trigger(ModalEvents.hidden, this);
950
        });
951
    }
952
 
953
    /**
954
     * Remove this modal from the DOM.
955
     *
956
     * @method destroy
957
     */
958
    destroy() {
959
        this.hide();
960
        removeToastRegion(this.getBody().get(0));
961
        this.root.remove();
962
        this.root.trigger(ModalEvents.destroyed, this);
963
        this.attachmentPoint.remove();
964
    }
965
 
966
    /**
967
     * Sets the appropriate aria attributes on this dialogue and the other
968
     * elements in the DOM to ensure that screen readers are able to navigate
969
     * the dialogue popup correctly.
970
     *
971
     * @method accessibilityShow
972
     */
973
    accessibilityShow() {
974
        // Make us visible to screen readers.
975
        Aria.unhide(this.root.get());
976
 
977
        // Hide siblings.
978
        Aria.hideSiblings(this.root.get()[0]);
979
    }
980
 
981
    /**
982
     * Restores the aria visibility on the DOM elements changed when displaying
983
     * the dialogue popup and makes the dialogue aria hidden to allow screen
984
     * readers to navigate the main page correctly when the dialogue is closed.
985
     *
986
     * @method accessibilityHide
987
     */
988
    accessibilityHide() {
989
        // Unhide siblings.
990
        Aria.unhideSiblings(this.root.get()[0]);
991
 
992
        // Hide this modal.
993
        Aria.hide(this.root.get());
994
    }
995
 
996
    /**
997
     * Set up all of the event handling for the modal.
998
     *
999
     * @method registerEventListeners
1000
     */
1001
    registerEventListeners() {
1002
        this.getRoot().on('keydown', (e) => {
1003
            if (!this.isVisible()) {
1004
                return;
1005
            }
1006
 
1007
            if (e.keyCode == KeyCodes.escape) {
1008
                if (this.removeOnClose) {
1009
                    this.destroy();
1010
                } else {
1011
                    this.hide();
1012
                }
1013
            }
1014
        });
1015
 
1016
        // Listen for clicks on the modal container.
1017
        this.getRoot().click((e) => {
1018
            // If the click wasn't inside the modal element then we should
1019
            // hide the modal.
1020
            if (!$(e.target).closest(SELECTORS.MODAL).length) {
1021
                // The check above fails to detect the click was inside the modal when the DOM tree is already changed.
1022
                // So, we check if we can still find the container element or not. If not, then the DOM tree is changed.
1023
                // It's best not to hide the modal in that case.
1024
                if ($(e.target).closest(SELECTORS.CONTAINER).length) {
1025
                    const outsideClickEvent = $.Event(ModalEvents.outsideClick);
1026
                    this.getRoot().trigger(outsideClickEvent, this);
1027
 
1028
                    if (!outsideClickEvent.isDefaultPrevented()) {
1029
                        this.hideIfNotForm();
1030
                    }
1031
                }
1032
            }
1033
        });
1034
 
1035
        CustomEvents.define(this.getModal(), [CustomEvents.events.activate]);
1036
        this.getModal().on(CustomEvents.events.activate, SELECTORS.HIDE, (e, data) => {
1037
            if (this.removeOnClose) {
1038
                this.destroy();
1039
            } else {
1040
                this.hide();
1041
            }
1042
            data.originalEvent.preventDefault();
1043
        });
1044
 
1045
        this.getRoot().on(ModalEvents.hidden, () => {
1046
            if (this.focusOnClose) {
1047
                // Focus on the element that actually triggers the modal.
1048
                this.focusOnClose.focus();
1049
            }
1050
        });
1051
    }
1052
 
1053
    /**
1054
     * Register a listener to close the dialogue when the cancel button is pressed.
1055
     *
1056
     * @method registerCloseOnCancel
1057
     */
1058
    registerCloseOnCancel() {
1059
        // Handle the clicking of the Cancel button.
1060
        this.getModal().on(CustomEvents.events.activate, this.getActionSelector('cancel'), (e, data) => {
1061
            const cancelEvent = $.Event(ModalEvents.cancel);
1062
            this.getRoot().trigger(cancelEvent, this);
1063
 
1064
            if (!cancelEvent.isDefaultPrevented()) {
1065
                data.originalEvent.preventDefault();
1066
 
1067
                if (this.removeOnClose) {
1068
                    this.destroy();
1069
                } else {
1070
                    this.hide();
1071
                }
1072
            }
1073
        });
1074
    }
1075
 
1076
    /**
1077
     * Register a listener to close the dialogue when the save button is pressed.
1078
     *
1079
     * @method registerCloseOnSave
1080
     */
1081
    registerCloseOnSave() {
1082
        // Handle the clicking of the Cancel button.
1083
        this.getModal().on(CustomEvents.events.activate, this.getActionSelector('save'), (e, data) => {
1084
            const saveEvent = $.Event(ModalEvents.save);
1085
            this.getRoot().trigger(saveEvent, this);
1086
 
1087
            if (!saveEvent.isDefaultPrevented()) {
1088
                data.originalEvent.preventDefault();
1089
 
1090
                if (this.removeOnClose) {
1091
                    this.destroy();
1092
                } else {
1093
                    this.hide();
1094
                }
1095
            }
1096
        });
1097
    }
1098
 
1099
 
1100
    /**
1101
     * Register a listener to close the dialogue when the delete button is pressed.
1102
     *
1103
     * @method registerCloseOnDelete
1104
     */
1105
    registerCloseOnDelete() {
1106
        // Handle the clicking of the Cancel button.
1107
        this.getModal().on(CustomEvents.events.activate, this.getActionSelector('delete'), (e, data) => {
1108
            const deleteEvent = $.Event(ModalEvents.delete);
1109
            this.getRoot().trigger(deleteEvent, this);
1110
 
1111
            if (!deleteEvent.isDefaultPrevented()) {
1112
                data.originalEvent.preventDefault();
1113
 
1114
                if (this.removeOnClose) {
1115
                    this.destroy();
1116
                } else {
1117
                    this.hide();
1118
                }
1119
            }
1120
        });
1121
    }
1122
 
1123
    /**
1124
     * Set or resolve and set the value using the function.
1125
     *
1126
     * @method asyncSet
1127
     * @param {(string|object)} value The string or jQuery promise.
1128
     * @param {function} setFunction The setter
1129
     * @return {Promise}
1130
     */
1131
    asyncSet(value, setFunction) {
1132
        const getWrappedValue = (value) => {
1133
            if (value instanceof Promise) {
1134
                return $.when(value);
1135
            }
1136
 
1137
            if (typeof value !== 'object' || !value.hasOwnProperty('then')) {
1138
                return $.Deferred().resolve(value);
1139
            }
1140
 
1141
            return value;
1142
        };
1143
 
1144
        return getWrappedValue(value)
1145
            .then((content) => setFunction(content))
1146
            .catch(Notification.exception);
1147
    }
1148
 
1149
    /**
1150
     * Set the title text of a button.
1151
     *
1152
     * This method is overloaded to take either a string value for the button title or a jQuery promise that is resolved with
1153
     * text most commonly from a Str.get_string call.
1154
     *
1155
     * @param {DOMString} action The action of the button
1156
     * @param {(String|object)} value The button text, or a promise which will resolve to it
1157
     * @returns {Promise}
1158
     */
1159
    setButtonText(action, value) {
1160
        const button = this.getFooter().find(this.getActionSelector(action));
1161
 
1162
        if (!button) {
1163
            throw new Error("Unable to find the '" + action + "' button");
1164
        }
1165
 
1166
        return this.asyncSet(value, button.text.bind(button));
1167
    }
1168
 
1169
    /**
1170
     * Get the Selector for an action.
1171
     *
1172
     * @param {String} action
1173
     * @returns {DOMString}
1174
     */
1175
    getActionSelector(action) {
1176
        return "[data-action='" + action + "']";
1177
    }
1178
 
1179
    /**
1180
     * Set the flag to remove the modal from the DOM on close.
1181
     *
1182
     * @param {Boolean} remove
1183
     */
1184
    setRemoveOnClose(remove) {
1185
        this.removeOnClose = remove;
1186
    }
1187
 
1188
    /**
1189
     * Set the return element for the modal.
1190
     *
1191
     * @param {Element|jQuery} element Element to focus when the modal is closed
1192
     */
1193
    setReturnElement(element) {
1194
        this.focusOnClose = element;
1195
    }
1196
 
1197
    /**
1198
     * Set the a button enabled or disabled.
1199
     *
1200
     * @param {DOMString} action The action of the button
1201
     * @param {Boolean} disabled the new disabled value
1202
     */
1203
    setButtonDisabled(action, disabled) {
1204
        const button = this.getFooter().find(this.getActionSelector(action));
1205
 
1206
        if (!button) {
1207
            throw new Error("Unable to find the '" + action + "' button");
1208
        }
1209
        if (disabled) {
1210
            button.attr('disabled', '');
1211
        } else {
1212
            button.removeAttr('disabled');
1213
        }
1214
    }
1215
}