diff --git a/lib/amd/build/modal.min.js b/lib/amd/build/modal.min.js index 52153245f82..06323fb1cc0 100644 --- a/lib/amd/build/modal.min.js +++ b/lib/amd/build/modal.min.js @@ -1 +1 @@ -define(["jquery","core/templates","core/notification","core/key_codes","core/custom_interaction_events","core/modal_backdrop","core/modal_events"],function(a,b,c,d,e,f,g){var h,i={CONTAINER:'[data-region="modal-container"]',MODAL:'[data-region="modal"]',HEADER:'[data-region="header"]',TITLE:'[data-region="title"]',BODY:'[data-region="body"]',FOOTER:'[data-region="footer"]',HIDE:'[data-action="hide"]',DIALOG:"[role=dialog]",MENU_BAR:"[role=menubar]",HAS_Z_INDEX:".moodle-has-zindex",CAN_RECEIVE_FOCUS:'input:not([type="hidden"]), a[href], button, textarea, select, [tabindex]'},j={LOADING:"core/loading",BACKDROP:"core/modal_backdrop"},k=function(b){this.root=a(b),this.modal=this.root.find(i.MODAL),this.header=this.modal.find(i.HEADER),this.title=this.header.find(i.TITLE),this.body=this.modal.find(i.BODY),this.footer=this.modal.find(i.FOOTER),this.hiddenSiblings=[],this.isAttached=!1,this.bodyJS=null,this.footerJS=null,this.root.is(i.CONTAINER)||c.exception({message:"Element is not a modal container"}),this.modal.length||c.exception({message:"Container does not contain a modal"}),this.header.length||c.exception({message:"Modal is missing a header region"}),this.title.length||c.exception({message:"Modal header is missing a title region"}),this.body.length||c.exception({message:"Modal is missing a body region"}),this.footer.length||c.exception({message:"Modal is missing a footer region"}),this.registerEventListeners()};return k.prototype.attachToDOM=function(){this.isAttached||(a("body").append(this.root),this.bodyJS&&(b.runTemplateJS(this.bodyJS),this.bodyJS=null),this.footerJS&&(b.runTemplateJS(this.footerJS),this.footerJS=null),this.isAttached=!0)},k.prototype.countOtherVisibleModals=function(){var b=0;return a("body").find(i.CONTAINER).each(function(c,d){d=a(d),!this.root.is(d)&&d.hasClass("show")&&b++}.bind(this)),b},k.prototype.getBackdrop=function(){return h||(h=b.render(j.BACKDROP,{}).then(function(b){var c=a(b);return new f(c)}).fail(c.exception)),h},k.prototype.getRoot=function(){return this.root},k.prototype.getModal=function(){return this.modal},k.prototype.getTitle=function(){return this.title},k.prototype.getBody=function(){return this.body},k.prototype.getFooter=function(){return this.footer},k.prototype.setTitle=function(b){var c=this.getTitle(),d=b;"object"==typeof b&&b.hasOwnProperty("then")||(d=a.Deferred(),d.resolve(b)),d.then(function(a){c.html(a)})},k.prototype.setBody=function(a){var c=this.getBody();"string"==typeof a?c.html(a):b.render(j.LOADING,{}).done(function(d){c.html(d),a.done(function(a,d){c.html(a),d&&(this.isAttached?b.runTemplateJS(d):this.bodyJS=d)}.bind(this))}.bind(this))},k.prototype.setFooter=function(a){var c=this.getFooter();"string"==typeof a?c.html(a):b.render(j.LOADING,{}).done(function(d){c.html(d),a.done(function(a,d){c.html(a),d&&(this.isAttached?b.runTemplateJS(d):this.footerJS=d)}.bind(this))}.bind(this))},k.prototype.setLarge=function(){this.isLarge()||this.getModal().addClass("modal-lg")},k.prototype.isLarge=function(){return this.getModal().hasClass("modal-lg")},k.prototype.setSmall=function(){this.isSmall()||this.getModal().removeClass("modal-lg")},k.prototype.isSmall=function(){return!this.getModal().hasClass("modal-lg")},k.prototype.calculateZIndex=function(){var b=a(i.DIALOG+", "+i.MENU_BAR+", "+i.HAS_Z_INDEX),c=parseInt(this.root.css("z-index"));return b.each(function(b,d){d=a(d);var e=d.css("z-index")?parseInt(d.css("z-index")):0;e>c&&(c=e)}),c},k.prototype.isVisible=function(){return this.root.hasClass("show")},k.prototype.hasFocus=function(){var b=a(document.activeElement);return this.root.is(b)||this.root.has(b).length},k.prototype.hasTransitions=function(){return this.getRoot().hasClass("fade")},k.prototype.show=function(){this.isVisible()||(this.isAttached||this.attachToDOM(),this.getBackdrop().done(function(b){var c=this.calculateZIndex(),d=c+2,e=d-1;this.root.css("z-index",d),b.setZIndex(e),b.show(),this.root.removeClass("hide").addClass("show"),this.accessibilityShow(),this.getTitle().focus(),a("body").addClass("modal-open"),this.root.trigger(g.shown,this)}.bind(this)))},k.prototype.hide=function(){this.isVisible()&&this.getBackdrop().done(function(b){this.countOtherVisibleModals()||(b.hide(),a("body").removeClass("modal-open"));var c=parseInt(this.root.css("z-index"));this.root.css("z-index",""),b.setZIndex(c-3),this.accessibilityHide(),this.hasTransitions()?this.getRoot().one("transitionend webkitTransitionEnd oTransitionEnd",function(){this.getRoot().removeClass("show").addClass("hide")}.bind(this)):this.getRoot().removeClass("show").addClass("hide"),this.root.trigger(g.hidden,this)}.bind(this))},k.prototype.destroy=function(){this.root.remove(),this.root.trigger(g.destroyed,this)},k.prototype.accessibilityShow=function(){a("body").children().each(function(b,c){if(!this.root.is(c)){c=a(c);var d=c.attr("aria-hidden");"true"!==d&&(c.data("previous-aria-hidden",d),this.hiddenSiblings.push(c),c.attr("aria-hidden","true"))}}.bind(this)),this.root.attr("aria-hidden","false")},k.prototype.accessibilityHide=function(){this.root.attr("aria-hidden","true"),a.each(this.hiddenSiblings,function(b,c){c=a(c);var d=c.data("previous-aria-hidden");"undefined"==typeof d?c.removeAttr("aria-hidden"):c.attr("aria-hidden",d)}),this.hiddenSiblings=[]},k.prototype.handleTabLock=function(b){if(this.hasFocus()){var c=a(document.activeElement),d=this.modal.find(i.CAN_RECEIVE_FOCUS),e=d.first(),f=d.last();c.is(e)&&b.shiftKey?(f.focus(),b.preventDefault()):c.is(f)&&!b.shiftKey&&(e.focus(),b.preventDefault())}},k.prototype.registerEventListeners=function(){this.getRoot().on("keydown",function(a){this.isVisible()&&(a.keyCode==d.tab?this.handleTabLock(a):a.keyCode==d.escape&&this.hide())}.bind(this)),e.define(this.getModal(),[e.events.activate]),this.getModal().on(e.events.activate,i.HIDE,function(a,b){this.hide(),b.originalEvent.preventDefault()}.bind(this))},k}); \ No newline at end of file +define(["jquery","core/templates","core/notification","core/key_codes","core/custom_interaction_events","core/modal_backdrop","core/modal_events"],function(a,b,c,d,e,f,g){var h,i={CONTAINER:'[data-region="modal-container"]',MODAL:'[data-region="modal"]',HEADER:'[data-region="header"]',TITLE:'[data-region="title"]',BODY:'[data-region="body"]',FOOTER:'[data-region="footer"]',HIDE:'[data-action="hide"]',DIALOG:"[role=dialog]",MENU_BAR:"[role=menubar]",HAS_Z_INDEX:".moodle-has-zindex",CAN_RECEIVE_FOCUS:'input:not([type="hidden"]), a[href], button, textarea, select, [tabindex]'},j={LOADING:"core/loading",BACKDROP:"core/modal_backdrop"},k=0,l=function(b){this.root=a(b),this.modal=this.root.find(i.MODAL),this.header=this.modal.find(i.HEADER),this.title=this.header.find(i.TITLE),this.body=this.modal.find(i.BODY),this.footer=this.modal.find(i.FOOTER),this.hiddenSiblings=[],this.isAttached=!1,this.bodyJS=null,this.footerJS=null,this.modalCount=k++,this.root.is(i.CONTAINER)||c.exception({message:"Element is not a modal container"}),this.modal.length||c.exception({message:"Container does not contain a modal"}),this.header.length||c.exception({message:"Modal is missing a header region"}),this.title.length||c.exception({message:"Modal header is missing a title region"}),this.body.length||c.exception({message:"Modal is missing a body region"}),this.footer.length||c.exception({message:"Modal is missing a footer region"}),this.registerEventListeners()};return l.prototype.attachToDOM=function(){this.isAttached||(a("body").append(this.root),this.bodyJS&&(b.runTemplateJS(this.bodyJS),this.bodyJS=null),this.footerJS&&(b.runTemplateJS(this.footerJS),this.footerJS=null),this.isAttached=!0)},l.prototype.countOtherVisibleModals=function(){var b=0;return a("body").find(i.CONTAINER).each(function(c,d){d=a(d),!this.root.is(d)&&d.hasClass("show")&&b++}.bind(this)),b},l.prototype.getBackdrop=function(){return h||(h=b.render(j.BACKDROP,{}).then(function(b){var c=a(b);return new f(c)}).fail(c.exception)),h},l.prototype.getRoot=function(){return this.root},l.prototype.getModal=function(){return this.modal},l.prototype.getTitle=function(){return this.title},l.prototype.getBody=function(){return this.body},l.prototype.getFooter=function(){return this.footer},l.prototype.getModalCount=function(){return this.modalCount},l.prototype.setTitle=function(b){var c=this.getTitle(),d=b;"object"==typeof b&&b.hasOwnProperty("then")||(d=a.Deferred(),d.resolve(b)),d.then(function(a){c.html(a)})},l.prototype.setBody=function(d){var e=this.getBody();if("string"==typeof d)e.html(d);else{var f="amd-modal-js-pending-id-"+this.getModalCount();M.util.js_pending(f);var g=null;e.css("overflow","hidden"),"pending"==d.state()?(e.animate({height:"100px"},150),e.html(""),g=b.render(j.LOADING,{}).then(function(b){var c=a(b).hide();return e.html(c),c.fadeIn(150),a.when(c.promise(),d)}).then(function(a){return a.fadeOut(100).promise()}).then(function(){return d})):g=d,g.then(function(a,c){var d=null;if(this.isVisible()){e.css("opacity",0);var f=e.innerHeight();e.html(a),e.css("height","");var g=e.innerHeight();e.css("height",f+"px"),d=e.animate({height:g+"px",opacity:1},{duration:150,queue:!1}).promise()}else e.html(a);return c&&(this.isAttached?b.runTemplateJS(c):this.bodyJS=c),d}.bind(this)).fail(c.exception).always(function(){e.css("height",""),e.css("overflow",""),e.css("opacity",""),M.util.js_complete(f)})}},l.prototype.setFooter=function(a){var c=this.getFooter();"string"==typeof a?c.html(a):b.render(j.LOADING,{}).done(function(d){c.html(d),a.done(function(a,d){c.html(a),d&&(this.isAttached?b.runTemplateJS(d):this.footerJS=d)}.bind(this))}.bind(this))},l.prototype.setLarge=function(){this.isLarge()||this.getModal().addClass("modal-lg")},l.prototype.isLarge=function(){return this.getModal().hasClass("modal-lg")},l.prototype.setSmall=function(){this.isSmall()||this.getModal().removeClass("modal-lg")},l.prototype.isSmall=function(){return!this.getModal().hasClass("modal-lg")},l.prototype.calculateZIndex=function(){var b=a(i.DIALOG+", "+i.MENU_BAR+", "+i.HAS_Z_INDEX),c=parseInt(this.root.css("z-index"));return b.each(function(b,d){d=a(d);var e=d.css("z-index")?parseInt(d.css("z-index")):0;e>c&&(c=e)}),c},l.prototype.isVisible=function(){return this.root.hasClass("show")},l.prototype.hasFocus=function(){var b=a(document.activeElement);return this.root.is(b)||this.root.has(b).length},l.prototype.hasTransitions=function(){return this.getRoot().hasClass("fade")},l.prototype.show=function(){this.isVisible()||(this.isAttached||this.attachToDOM(),this.getBackdrop().done(function(b){var c=this.calculateZIndex(),d=c+2,e=d-1;this.root.css("z-index",d),b.setZIndex(e),b.show(),this.root.removeClass("hide").addClass("show"),this.accessibilityShow(),this.getTitle().focus(),a("body").addClass("modal-open"),this.root.trigger(g.shown,this)}.bind(this)))},l.prototype.hide=function(){this.isVisible()&&this.getBackdrop().done(function(b){this.countOtherVisibleModals()||(b.hide(),a("body").removeClass("modal-open"));var c=parseInt(this.root.css("z-index"));this.root.css("z-index",""),b.setZIndex(c-3),this.accessibilityHide(),this.hasTransitions()?this.getRoot().one("transitionend webkitTransitionEnd oTransitionEnd",function(){this.getRoot().removeClass("show").addClass("hide")}.bind(this)):this.getRoot().removeClass("show").addClass("hide"),this.root.trigger(g.hidden,this)}.bind(this))},l.prototype.destroy=function(){this.root.remove(),this.root.trigger(g.destroyed,this)},l.prototype.accessibilityShow=function(){a("body").children().each(function(b,c){if(!this.root.is(c)){c=a(c);var d=c.attr("aria-hidden");"true"!==d&&(c.data("previous-aria-hidden",d),this.hiddenSiblings.push(c),c.attr("aria-hidden","true"))}}.bind(this)),this.root.attr("aria-hidden","false")},l.prototype.accessibilityHide=function(){this.root.attr("aria-hidden","true"),a.each(this.hiddenSiblings,function(b,c){c=a(c);var d=c.data("previous-aria-hidden");"undefined"==typeof d?c.removeAttr("aria-hidden"):c.attr("aria-hidden",d)}),this.hiddenSiblings=[]},l.prototype.handleTabLock=function(b){if(this.hasFocus()){var c=a(document.activeElement),d=this.modal.find(i.CAN_RECEIVE_FOCUS),e=d.first(),f=d.last();c.is(e)&&b.shiftKey?(f.focus(),b.preventDefault()):c.is(f)&&!b.shiftKey&&(e.focus(),b.preventDefault())}},l.prototype.registerEventListeners=function(){this.getRoot().on("keydown",function(a){this.isVisible()&&(a.keyCode==d.tab?this.handleTabLock(a):a.keyCode==d.escape&&this.hide())}.bind(this)),e.define(this.getModal(),[e.events.activate]),this.getModal().on(e.events.activate,i.HIDE,function(a,b){this.hide(),b.originalEvent.preventDefault()}.bind(this))},l}); \ No newline at end of file diff --git a/lib/amd/src/modal.js b/lib/amd/src/modal.js index b0a570c4cae..2971d9fce3a 100644 --- a/lib/amd/src/modal.js +++ b/lib/amd/src/modal.js @@ -50,6 +50,12 @@ define(['jquery', 'core/templates', 'core/notification', 'core/key_codes', */ var backdropPromise; + /** + * A counter that gets incremented for each modal created. This can be + * used to generate unique values for the modals. + */ + var modalCounter = 0; + /** * Constructor for the Modal. * @@ -66,6 +72,7 @@ define(['jquery', 'core/templates', 'core/notification', 'core/key_codes', this.isAttached = false; this.bodyJS = null; this.footerJS = null; + this.modalCount = modalCounter++; if (!this.root.is(SELECTORS.CONTAINER)) { Notification.exception({message: 'Element is not a modal container'}); @@ -212,6 +219,16 @@ define(['jquery', 'core/templates', 'core/notification', 'core/key_codes', return this.footer; }; + /** + * Get the unique modal count. + * + * @method getModalCount + * @return {int} + */ + Modal.prototype.getModalCount = function() { + return this.modalCount; + }; + /** * Set the modal title element. * @@ -253,25 +270,95 @@ define(['jquery', 'core/templates', 'core/notification', 'core/key_codes', // Just set the value if it's a string. body.html(value); } else { + var jsPendingId = 'amd-modal-js-pending-id-' + this.getModalCount(); + M.util.js_pending(jsPendingId); // Otherwise we assume it's a promise to be resolved with // html and javascript. - Templates.render(TEMPLATES.LOADING, {}).done(function(html) { - body.html(html); + var contentPromise = null; + body.css('overflow', 'hidden'); - value.done(function(html, js) { + if (value.state() == 'pending') { + // We're still waiting for the body promise to resolve so + // let's show a loading icon. + body.animate({height: '100px'}, 150); + + body.html(''); + contentPromise = Templates.render(TEMPLATES.LOADING, {}) + .then(function(html) { + var loadingIcon = $(html).hide(); + body.html(loadingIcon); + loadingIcon.fadeIn(150); + + // We only want the loading icon to fade out + // when the content for the body has finished + // loading. + return $.when(loadingIcon.promise(), value); + }) + .then(function(loadingIcon) { + // Once the content has finished loading and + // the loading icon has been shown then we can + // fade the icon away to reveal the content. + return loadingIcon.fadeOut(100).promise(); + }) + .then(function() { + return value; + }); + } else { + // The content is already loaded so let's just display + // it to the user. No need for a loading icon. + contentPromise = value; + } + + // Now we can actually display the content. + contentPromise.then(function(html, js) { + var result = null; + + if (this.isVisible()) { + // If the modal is visible then we should display + // the content gracefully for the user. + body.css('opacity', 0); + var currentHeight = body.innerHeight(); body.html(html); + // We need to clear any height values we've set here + // in order to measure the height of the content being + // added. This then allows us to animate the height + // transition. + body.css('height', ''); + var newHeight = body.innerHeight(); + body.css('height', currentHeight + 'px'); + result = body.animate( + {height: newHeight + 'px', opacity: 1}, + {duration: 150, queue: false} + ).promise(); + } else { + // Since the modal isn't visible we can just immediately + // set the content. No need to animate it. + body.html(html); + } - if (js) { - if (this.isAttached) { - // If we're in the DOM then run the JS immediately. - Templates.runTemplateJS(js); - } else { - // Otherwise cache it to be run when we're attached. - this.bodyJS = js; - } + if (js) { + if (this.isAttached) { + // If we're in the DOM then run the JS immediately. + Templates.runTemplateJS(js); + } else { + // Otherwise cache it to be run when we're attached. + this.bodyJS = js; } - }.bind(this)); - }.bind(this)); + } + + return result; + }.bind(this)) + .fail(Notification.exception) + .always(function() { + // When we're done displaying all of the content we need + // to clear the custom values we've set here. + body.css('height', ''); + body.css('overflow', ''); + body.css('opacity', ''); + M.util.js_complete(jsPendingId); + + return; + }); } }; diff --git a/theme/boost/scss/moodle.scss b/theme/boost/scss/moodle.scss index 6b2b93b1782..19c89c33f7c 100644 --- a/theme/boost/scss/moodle.scss +++ b/theme/boost/scss/moodle.scss @@ -46,3 +46,4 @@ $breadcrumb-divider-rtl: "◀" !default; @import "moodle/responsive-tabs"; @import "moodle/bs2-compat"; @import "moodle/print"; +@import "moodle/modal"; diff --git a/theme/boost/scss/moodle/modal.scss b/theme/boost/scss/moodle/modal.scss new file mode 100644 index 00000000000..7399bfa2402 --- /dev/null +++ b/theme/boost/scss/moodle/modal.scss @@ -0,0 +1,16 @@ +.modal-body { + & > .loading-icon { + display: block; + position: relative; + width: 100%; + height: 100%; + + .icon { + position: absolute; + top: 50%; + /*rtl:ignore*/ + left: 50%; + transform: translate(-50%, -50%); + } + } +} diff --git a/theme/bootstrapbase/less/moodle/modal.less b/theme/bootstrapbase/less/moodle/modal.less index ecf0b0cd15d..1accb3dffb6 100644 --- a/theme/bootstrapbase/less/moodle/modal.less +++ b/theme/bootstrapbase/less/moodle/modal.less @@ -91,6 +91,21 @@ body { .modal-body { max-height: none; + + & > .loading-icon { + display: block; + position: relative; + width: 100%; + height: 100%; + + .icon { + position: absolute; + top: 50%; + /*rtl:ignore*/ + left: 50%; + transform: translate(-50%, -50%); + } + } } .modal-footer { diff --git a/theme/bootstrapbase/style/moodle.css b/theme/bootstrapbase/style/moodle.css index cf238a16c9a..15fad133b0b 100644 --- a/theme/bootstrapbase/style/moodle.css +++ b/theme/bootstrapbase/style/moodle.css @@ -17053,6 +17053,19 @@ body.modal-open { .modal-container .modal .modal-body { max-height: none; } +.modal-container .modal .modal-body > .loading-icon { + display: block; + position: relative; + width: 100%; + height: 100%; +} +.modal-container .modal .modal-body > .loading-icon .icon { + position: absolute; + top: 50%; + /*rtl:ignore*/ + left: 50%; + transform: translate(-50%, -50%); +} .modal-container .modal .modal-footer { border-top: 1px solid #bbb; text-align: center;