diff --git a/cohort/tests/behat/upload_cohort_users.feature b/cohort/tests/behat/upload_cohort_users.feature
index 1b1edb4086a..cc940811cb5 100644
--- a/cohort/tests/behat/upload_cohort_users.feature
+++ b/cohort/tests/behat/upload_cohort_users.feature
@@ -18,9 +18,11 @@ Feature: Upload users to a cohort
And I am on "Course 1" course homepage
And I add "Cohort sync" enrolment method with:
| Cohort | Cohort 1 |
+ And I should see "Cohort sync (Cohort 1 - Student)"
And I am on "Course 2" course homepage
And I add "Cohort sync" enrolment method with:
| Cohort | Cohort 2 |
+ And I should see "Cohort sync (Cohort 2 - Student)"
When I navigate to "Upload users" node in "Site administration > Users > Accounts"
And I upload "lib/tests/fixtures/upload_users_cohorts.csv" file to "File" filemanager
And I press "Upload users"
diff --git a/lib/amd/build/form-autocomplete.min.js b/lib/amd/build/form-autocomplete.min.js
index e66e5ff27cd..830d6fbc362 100644
--- a/lib/amd/build/form-autocomplete.min.js
+++ b/lib/amd/build/form-autocomplete.min.js
@@ -1 +1 @@
-define(["jquery","core/log","core/str","core/templates","core/notification"],function(a,b,c,d,e){var f={DOWN:40,ENTER:13,SPACE:32,ESCAPE:27,COMMA:44,UP:38},g=a.now(),h=function(b,c){var d=a(document.getElementById(c.selectionId)),e=d.children("[aria-selected=true]").length;for(b%=e;b<0;)b+=e;var f=a(d.children("[aria-selected=true]").get(b)),g=c.selectionId+"-"+b;d.children().attr("data-active-selection",!1).attr("id",""),f.attr("data-active-selection",!0).attr("id",g),d.attr("aria-activedescendant",g)},i=function(b,c,f){var g=[],i=a(document.getElementById(c.selectionId)),j=i.attr("aria-activedescendant"),k=!1;j&&(k=a(document.getElementById(j)).attr("data-value")),f.children("option").each(function(b,c){if(a(c).prop("selected")){var d;d=a(c).data("html")?a(c).data("html"):a(c).html(),g.push({label:d,value:a(c).attr("value")})}});var l=a.extend({items:g},b,c);d.render("core/form_autocomplete_selection",l).done(function(b){i.empty().append(a(b).html()),k!==!1&&i.children("[aria-selected=true]").each(function(b,d){a(d).attr("data-value")===k&&h(b,c)})}).fail(e.exception)},j=function(a){"undefined"!=typeof M.core_formchangechecker&&M.core_formchangechecker.set_form_changed(),a.change()},k=function(b,c,d,e){var f=a(d).attr("data-value");b.multiple&&e.children("option").each(function(b,c){a(c).attr("value")==f&&(a(c).prop("selected",!1),a(c).attr("data-iscustom")&&a(c).remove())}),i(b,c,e),j(e)},l=function(b,c){var d=a(document.getElementById(c.inputId)),e=a(document.getElementById(c.suggestionsId)),f=e.children("[aria-hidden=false]").length;for(b%=f;b<0;)b+=f;var g=a(e.children("[aria-hidden=false]").get(b)),h=a(e.children("[role=option]")).index(g),i=c.suggestionsId+"-"+h;e.children().attr("aria-selected",!1).attr("id",""),g.attr("aria-selected",!0).attr("id",i),d.attr("aria-activedescendant",i);var j=g.offset().top-e.offset().top+e.scrollTop()-e.height()/2;e.animate({scrollTop:j},100)},m=function(b){var c=a(document.getElementById(b.suggestionsId)),d=c.children("[aria-selected=true]"),e=c.children("[aria-hidden=false]").index(d);l(e+1,b)},n=function(b){var c=a(document.getElementById(b.selectionId)),d=c.children("[data-active-selection=true]");if(!d)return void h(0,b);var e=c.children("[aria-selected=true]").index(d);h(e-1,b)},o=function(b){var c=a(document.getElementById(b.selectionId)),d=c.children("[data-active-selection=true]");if(!d)return void h(0,b);var e=c.children("[aria-selected=true]").index(d);h(e+1,b)},p=function(b){var c=a(document.getElementById(b.suggestionsId)),d=c.children("[aria-selected=true]"),e=c.children("[aria-hidden=false]").index(d);l(e-1,b)},q=function(b){var c=a(document.getElementById(b.inputId)),d=a(document.getElementById(b.suggestionsId));c.attr("aria-expanded",!1).attr("aria-activedescendant",b.selectionId),d.hide().attr("aria-hidden",!0)},r=function(b,f,g,h){var i=a(document.getElementById(f.inputId)),j=a(document.getElementById(f.suggestionsId)),k=!1,m=[];h.children("option").each(function(b,c){a(c).prop("selected")!==!0&&(m[m.length]={label:c.innerHTML,value:a(c).attr("value")})});var n=f.caseSensitive?g:g.toLocaleLowerCase(),o=a.extend({options:m},b,f);d.render("core/form_autocomplete_suggestions",o).done(function(d){j.replaceWith(d),j=a(document.getElementById(f.suggestionsId)),j.show().attr("aria-hidden",!1),j.children().each(function(c,d){d=a(d),b.caseSensitive&&d.text().indexOf(n)>-1||!b.caseSensitive&&d.text().toLocaleLowerCase().indexOf(n)>-1?(d.show().attr("aria-hidden",!1),k=!0):d.hide().attr("aria-hidden",!0)}),i.attr("aria-expanded",!0),h.attr("data-notice")?j.html(h.attr("data-notice")):k?b.tags||l(0,f):c.get_string("nosuggestions","form").done(function(a){j.html(a)})}).fail(e.exception)},s=function(b,c,d){var e=a(document.getElementById(c.inputId)),f=e.val(),g=f.split(","),h=!1;a.each(g,function(c,e){if(e=e.trim(),""!==e&&(b.multiple||d.children("option").prop("selected",!1),d.children("option").each(function(b,c){a(c).attr("value")==e&&(h=!0,a(c).prop("selected",!0))}),!h)){var f=a("");f.append(document.createTextNode(e)),f.attr("value",e),d.append(f),f.prop("selected",!0),f.attr("data-iscustom",!0)}}),i(b,c,d),j(d),e.val(""),q(c)},t=function(b,c,d){var e=a(document.getElementById(c.inputId)),f=a(document.getElementById(c.suggestionsId)),g=f.children("[aria-selected=true]").attr("data-value");b.multiple||d.children("option").prop("selected",!1),d.children("option").each(function(b,c){a(c).attr("value")==g&&a(c).prop("selected",!0)}),i(b,c,d),j(d),b.closeSuggestionsOnSelect?(e.val(""),q(c)):(e.focus(),r(b,c,e.val(),d))},u=function(b,c,d,f,g){var h="form-autocomplete-updateajax";M.util.js_pending(h);var i=a(b.currentTarget).val();g.transport(c.selector,i,function(b){var e=g.processResults(c.selector,b),i=[];if(f.children("option").each(function(b,c){c=a(c),c.prop("selected")?i.push(String(c.attr("value"))):c.remove()}),!c.multiple&&0===f.children("option").length){var j=a(" ");f.append(j)}a.isArray(e)?(a.each(e,function(b,c){if(i.indexOf(String(c.value))===-1){var d=a(" ");d.append(c.label),d.attr("value",c.value),f.append(d)}}),f.attr("data-notice","")):f.attr("data-notice",e),r(c,d,"",f),M.util.js_complete(h)},function(a){M.util.js_complete(h),e.exception(a)})},v=function(b,c,d){var e=a(document.getElementById(c.inputId));if(e.on("keydown",function(g){var h="form-autocomplete-addnav-"+c.inputId+"-"+g.keyCode;switch(M.util.js_pending(h),g.keyCode){case f.DOWN:return b.showSuggestions?("true"===e.attr("aria-expanded")?m(c):!e.val()&&b.ajax?require([b.ajax],function(a){u(g,b,c,d,a)}):r(b,c,e.val(),d),g.preventDefault(),M.util.js_complete(h),!1):(M.util.js_complete(h),!0);case f.UP:return p(c),g.preventDefault(),M.util.js_complete(h),!1;case f.ENTER:var i=a(document.getElementById(c.suggestionsId));return"true"===e.attr("aria-expanded")&&i.children("[aria-selected=true]").length>0?t(b,c,d):b.tags&&s(b,c,d),g.preventDefault(),M.util.js_complete(h),!1;case f.ESCAPE:return"true"===e.attr("aria-expanded")&&q(c),g.preventDefault(),M.util.js_complete(h),!1}return M.util.js_complete(h),!0}),e.on("keypress",function(a){var e="form-autocomplete-keypress-"+a.keyCode;return M.util.js_pending(e),a.keyCode===f.COMMA?(b.tags&&s(b,c,d),a.preventDefault(),M.util.js_complete(e),!1):(M.util.js_complete(e),!0)}),e.on("behat:set-value",function(){var f=a(document.getElementById(c.suggestionsId)),g="form-autocomplete-behat";M.util.js_pending(g),"true"===e.attr("aria-expanded")&&f.children("[aria-selected=true]").length>0?t(b,c,d):b.tags&&s(b,c,d),M.util.js_complete(g)}),e.on("blur",function(){var f="form-autocomplete-blur";M.util.js_pending(f),window.setTimeout(function(){var g=a(document.activeElement);g.attr("id")!=e.attr("id")&&(b.tags&&s(b,c,d),q(c)),M.util.js_complete(f)},500)}),b.showSuggestions){var g=a(document.getElementById(c.downArrowId));g.on("click",function(a){var f="form-autocomplete-show-suggestions";M.util.js_pending(f),e.focus(),!e.val()&&b.ajax?require([b.ajax],function(e){u(a,b,c,d,e)}):r(b,c,e.val(),d),M.util.js_complete(f)})}var h=a(document.getElementById(c.suggestionsId));h.parent().prop("onclick",null).off("click"),h.parent().on("click","[role=option]",function(e){var f="form-autocomplete-parent";M.util.js_pending(f);var g=a(e.currentTarget).closest("[role=option]"),h=a(document.getElementById(c.suggestionsId)),i=h.children("[aria-hidden=false]").index(g);l(i,c),t(b,c,d),M.util.js_complete(f)});var i=a(document.getElementById(c.selectionId));i.on("click","[role=listitem]",function(e){var f="form-autocomplete-clicks";M.util.js_pending(f);var g=a(e.currentTarget);k(b,c,g,d),M.util.js_complete(f)}),i.on("keydown",function(e){var g="form-autocomplete-keydown-"+e.keyCode;switch(M.util.js_pending(g),e.keyCode){case f.DOWN:return o(c),e.preventDefault(),M.util.js_complete(g),!1;case f.UP:return n(c),e.preventDefault(),M.util.js_complete(g),!1;case f.SPACE:case f.ENTER:var h=a(document.getElementById(c.selectionId)).children("[data-active-selection=true]");return h&&(k(b,c,h,d),e.preventDefault()),M.util.js_complete(g),!1}return M.util.js_complete(g),!0}),b.showSuggestions&&(b.ajax?require([b.ajax],function(a){var f=null,g="autocomplete-throttledhandler",h=function(e){u(e,b,c,d,a),M.util.js_complete(g)},i=function(a){null!==f?(window.clearTimeout(f),f=null):M.util.js_pending(g),f=window.setTimeout(h.bind(this,a),300)};e.on("input",i)}):e.on("input",function(e){var f=a(e.currentTarget).val(),g=a(e.currentTarget).data("last-value");g!==f&&r(b,c,f,d),a(e.currentTarget).data("last-value",f)}))};return{enhance:function(f,h,j,k,l,m,n,o){var p={selector:f,tags:!1,ajax:!1,placeholder:k,caseSensitive:!1,showSuggestions:!0,noSelectionString:n},q="autocomplete-setup-"+f;M.util.js_pending(q),"undefined"!=typeof h&&(p.tags=h),"undefined"!=typeof j&&(p.ajax=j),"undefined"!=typeof l&&(p.caseSensitive=l),"undefined"!=typeof m&&(p.showSuggestions=m),"undefined"==typeof n&&c.get_string("noselection","form").done(function(a){p.noSelectionString=a}).fail(e.exception);var r=a(f);if(!r)return b.debug("Selector not found: "+f),M.util.js_complete(q),!1;r.css("visibility","hidden").attr("aria-hidden",!0);var s={selectId:r.attr("id"),inputId:"form_autocomplete_input-"+g,suggestionsId:"form_autocomplete_suggestions-"+g,selectionId:"form_autocomplete_selection-"+g,downArrowId:"form_autocomplete_downarrow-"+g};g++,p.multiple=r.attr("multiple"),"undefined"!=typeof o?p.closeSuggestionsOnSelect=o:p.closeSuggestionsOnSelect=!p.multiple;var t=a("[for="+s.selectId+"]"),u=[];r.children("option").each(function(b,c){u[b]={label:c.innerHTML,value:a(c).attr("value")}});var w=a.extend({},p,s);w.options=u,w.items=[];var x="",y=d.render("core/form_autocomplete_input",w).then(function(a,b){return x+=b,a}),z=d.render("core/form_autocomplete_suggestions",w).then(function(a,b){return x+=b,a}),A=d.render("core/form_autocomplete_selection",w).then(function(a,b){return x+=b,a});return a.when(y,z,A).then(function(b,c,e){r.hide(),r.after(c),r.after(b),r.after(e),d.runTemplateJS(x),t.attr("for",s.inputId),v(p,s,r);var f=a(document.getElementById(s.suggestionsId));return f.hide().attr("aria-hidden",!0),i(p,s,r),M.util.js_complete(q),!0}).fail(function(a){M.util.js_complete(q),e.exception(a)})}}});
\ No newline at end of file
+define(["jquery","core/log","core/str","core/templates","core/notification"],function(a,b,c,d,e){var f={DOWN:40,ENTER:13,SPACE:32,ESCAPE:27,COMMA:44,UP:38},g=a.now(),h=function(b,c){var d=a(document.getElementById(c.selectionId)),e=d.children("[aria-selected=true]").length;for(b%=e;b<0;)b+=e;var f=a(d.children("[aria-selected=true]").get(b)),g=c.selectionId+"-"+b;return d.children().attr("data-active-selection",!1).attr("id",""),f.attr("data-active-selection",!0).attr("id",g),d.attr("aria-activedescendant",g),a.Deferred().resolve()},i=function(b,c,f){var g="form-autocomplete-updateSelectionList-"+c.inputId;M.util.js_pending(g);var i=[],j=a(document.getElementById(c.selectionId)),k=j.attr("aria-activedescendant"),l=!1;k&&(l=a(document.getElementById(k)).attr("data-value")),f.children("option").each(function(b,c){if(a(c).prop("selected")){var d;d=a(c).data("html")?a(c).data("html"):a(c).html(),i.push({label:d,value:a(c).attr("value")})}});var m=a.extend({items:i},b,c);return d.render("core/form_autocomplete_selection",m).then(function(b,e){return d.replaceNodeContents(j,b,e),l!==!1&&j.children("[aria-selected=true]").each(function(b,d){a(d).attr("data-value")===l&&h(b,c)}),l}).then(function(){return M.util.js_complete(g)})["catch"](e.exception)},j=function(a){"undefined"!=typeof M.core_formchangechecker&&M.core_formchangechecker.set_form_changed(),a.change()},k=function(b,c,d,e){var f=a(d).attr("data-value");return b.multiple&&e.children("option").each(function(b,c){a(c).attr("value")==f&&(a(c).prop("selected",!1),a(c).attr("data-iscustom")&&a(c).remove())}),i(b,c,e).then(function(){j(e)})},l=function(b,c){var d=a(document.getElementById(c.inputId)),e=a(document.getElementById(c.suggestionsId)),f=e.children("[aria-hidden=false]").length;for(b%=f;b<0;)b+=f;var g=a(e.children("[aria-hidden=false]").get(b)),h=a(e.children("[role=option]")).index(g),i=c.suggestionsId+"-"+h;e.children().attr("aria-selected",!1).attr("id",""),g.attr("aria-selected",!0).attr("id",i),d.attr("aria-activedescendant",i);var j=g.offset().top-e.offset().top+e.scrollTop()-e.height()/2;return e.animate({scrollTop:j},100).promise()},m=function(b){var c=a(document.getElementById(b.suggestionsId)),d=c.children("[aria-selected=true]"),e=c.children("[aria-hidden=false]").index(d);return l(e+1,b)},n=function(b){var c=a(document.getElementById(b.selectionId)),d=c.children("[data-active-selection=true]");if(!d)return h(0,b);var e=c.children("[aria-selected=true]").index(d);return h(e-1,b)},o=function(b){var c=a(document.getElementById(b.selectionId)),d=c.children("[data-active-selection=true]"),e=0;return d?(e=c.children("[aria-selected=true]").index(d),e+=1):e=0,h(e,b)},p=function(b){var c=a(document.getElementById(b.suggestionsId)),d=c.children("[aria-selected=true]"),e=c.children("[aria-hidden=false]").index(d);return l(e-1,b)},q=function(b){var c=a(document.getElementById(b.inputId)),d=a(document.getElementById(b.suggestionsId));return c.attr("aria-expanded",!1).attr("aria-activedescendant",b.selectionId),d.hide().attr("aria-hidden",!0),a.Deferred().resolve()},r=function(b,f,g,h){var i="form-autocomplete-updateSuggestions-"+f.inputId;M.util.js_pending(i);var j=a(document.getElementById(f.inputId)),k=a(document.getElementById(f.suggestionsId)),m=!1,n=[];h.children("option").each(function(b,c){a(c).prop("selected")!==!0&&(n[n.length]={label:c.innerHTML,value:a(c).attr("value")})});var o=f.caseSensitive?g:g.toLocaleLowerCase(),p=a.extend({options:n},b,f),q=d.render("core/form_autocomplete_suggestions",p).then(function(e,g){return d.replaceNode(k,e,g),k=a(document.getElementById(f.suggestionsId)),k.show().attr("aria-hidden",!1),k.children().each(function(c,d){d=a(d),b.caseSensitive&&d.text().indexOf(o)>-1||!b.caseSensitive&&d.text().toLocaleLowerCase().indexOf(o)>-1?(d.show().attr("aria-hidden",!1),m=!0):d.hide().attr("aria-hidden",!0)}),j.attr("aria-expanded",!0),h.attr("data-notice")?k.html(h.attr("data-notice")):m?b.tags||l(0,f):c.get_string("nosuggestions","form").done(function(a){k.html(a)}),k}).then(function(){return M.util.js_complete(i)})["catch"](e.exception);return q},s=function(b,c,d){var e=a(document.getElementById(c.inputId)),f=e.val(),g=f.split(","),h=!1;return a.each(g,function(c,e){if(e=e.trim(),""!==e&&(b.multiple||d.children("option").prop("selected",!1),d.children("option").each(function(b,c){a(c).attr("value")==e&&(h=!0,a(c).prop("selected",!0))}),!h)){var f=a(" ");f.append(document.createTextNode(e)),f.attr("value",e),d.append(f),f.prop("selected",!0),f.attr("data-iscustom",!0)}}),i(b,c,d).then(function(){j(d)}).then(function(){e.val("")}).then(function(){return q(c)})},t=function(b,c,d){var e=a(document.getElementById(c.inputId)),f=a(document.getElementById(c.suggestionsId)),g=f.children("[aria-selected=true]").attr("data-value");return b.multiple||d.children("option").prop("selected",!1),d.children("option").each(function(b,c){a(c).attr("value")==g&&a(c).prop("selected",!0)}),i(b,c,d).then(function(){j(d)}).then(function(){return b.closeSuggestionsOnSelect?(e.val(""),q(c)):(e.focus(),r(b,c,e.val(),d))})},u=function(b,c,d,e,f){var g=w("updateAjax"),h=a(b.currentTarget).val();return f.transport(c.selector,h,function(b){var h=f.processResults(c.selector,b),i=[];if(e.children("option").each(function(b,c){c=a(c),c.prop("selected")?i.push(String(c.attr("value"))):c.remove()}),!c.multiple&&0===e.children("option").length){var j=a(" ");e.append(j)}a.isArray(h)?(a.each(h,function(b,c){if(i.indexOf(String(c.value))===-1){var d=a(" ");d.append(c.label),d.attr("value",c.value),e.append(d)}}),e.attr("data-notice","")):e.attr("data-notice",h),g.resolve(r(c,d,"",e))},function(a){g.reject(a)}),g},v=function(b,c,d){var g=a(document.getElementById(c.inputId));if(g.on("keydown",function(e){var h=w("addNavigation-"+c.inputId+"-"+e.keyCode);switch(e.keyCode){case f.DOWN:return b.showSuggestions?("true"===g.attr("aria-expanded")?h.resolve(m(c)):!g.val()&&b.ajax?require([b.ajax],function(a){h.resolve(u(e,b,c,d,a))}):h.resolve(r(b,c,g.val(),d)),e.preventDefault(),!1):(h.resolve(),!0);case f.UP:return h.resolve(p(c)),e.preventDefault(),!1;case f.ENTER:var i=a(document.getElementById(c.suggestionsId));return"true"===g.attr("aria-expanded")&&i.children("[aria-selected=true]").length>0?h.resolve(t(b,c,d)):b.tags?h.resolve(s(b,c,d)):h.resolve(),e.preventDefault(),!1;case f.ESCAPE:return"true"===g.attr("aria-expanded")?h.resolve(q(c)):h.resolve(),e.preventDefault(),!1}return h.resolve(),!0}),g.on("keypress",function(a){return a.keyCode!==f.COMMA||(b.tags&&w("keypress-"+a.keyCode).resolve(s(b,c,d)),a.preventDefault(),!1)}),g.on("blur",function(){var e=w("form-autocomplete-blur");window.setTimeout(function(){var f=a(document.activeElement);f.attr("id")!=g.attr("id")&&(b.tags&&e.then(function(){return s(b,c,d)})["catch"](),e.then(function(){return q(c)})["catch"]()),e.resolve()},500)}),b.showSuggestions){var h=a(document.getElementById(c.downArrowId));h.on("click",function(a){var e=w("form-autocomplete-show-suggestions");g.focus(),!g.val()&&b.ajax?require([b.ajax],function(f){e.resolve(u(a,b,c,d,f))}):e.resolve(r(b,c,g.val(),d))})}var i=a(document.getElementById(c.suggestionsId));i.parent().prop("onclick",null).off("click"),i.parent().on("click","[role=option]",function(e){var f=w("form-autocomplete-parent"),g=a(e.currentTarget).closest("[role=option]"),h=a(document.getElementById(c.suggestionsId)),i=h.children("[aria-hidden=false]").index(g);l(i,c).then(function(){return t(b,c,d)}).then(function(){return f.resolve()})["catch"]()});var j=a(document.getElementById(c.selectionId));j.on("click","[role=listitem]",function(e){var f=w("form-autocomplete-clicks");f.resolve(k(b,c,a(e.currentTarget),d))}),j.on("keydown",function(e){var g=w("form-autocomplete-keydown-"+e.keyCode);switch(e.keyCode){case f.DOWN:return e.preventDefault(),g.resolve(o(c)),!1;case f.UP:return e.preventDefault(),g.resolve(n(c)),!1;case f.SPACE:case f.ENTER:var h=a(document.getElementById(c.selectionId)).children("[data-active-selection=true]");return h&&(e.preventDefault(),g.resolve(k(b,c,h,d))),!1}return g.resolve(),!0}),b.showSuggestions&&(b.ajax?require([b.ajax],function(a){var f=null,h=!1,i="autocomplete-throttledhandler",j=function(g){f=null,h=!0,u(g,b,c,d,a).then(function(){return null===f&&M.util.js_complete(i),h=!1,arguments[0]})["catch"](e.exception)},k=function(a){return window.clearTimeout(f),h?void(f=window.setTimeout(k.bind(this,a),100)):(null===f&&M.util.js_pending(i),void(f=window.setTimeout(j.bind(this,a),300)))};g.on("input",k)}):g.on("input",function(e){var f=a(e.currentTarget).val(),g=a(e.currentTarget).data("last-value");g!==f&&r(b,c,f,d),a(e.currentTarget).data("last-value",f)}))},w=function(b){var c="form-autocomplete:"+b;M.util.js_pending(c);var d=a.Deferred();return d.then(function(){return M.util.js_complete(c),arguments[0]})["catch"](e.exception),d};return{enhance:function(f,h,j,k,l,m,n,o){var p={selector:f,tags:!1,ajax:!1,placeholder:k,caseSensitive:!1,showSuggestions:!0,noSelectionString:n},q="autocomplete-setup-"+f;M.util.js_pending(q),"undefined"!=typeof h&&(p.tags=h),"undefined"!=typeof j&&(p.ajax=j),"undefined"!=typeof l&&(p.caseSensitive=l),"undefined"!=typeof m&&(p.showSuggestions=m),"undefined"==typeof n&&c.get_string("noselection","form").done(function(a){p.noSelectionString=a}).fail(e.exception);var r=a(f);if(!r)return b.debug("Selector not found: "+f),M.util.js_complete(q),!1;r.css("visibility","hidden").attr("aria-hidden",!0);var s={selectId:r.attr("id"),inputId:"form_autocomplete_input-"+g,suggestionsId:"form_autocomplete_suggestions-"+g,selectionId:"form_autocomplete_selection-"+g,downArrowId:"form_autocomplete_downarrow-"+g};g++,p.multiple=r.attr("multiple"),"undefined"!=typeof o?p.closeSuggestionsOnSelect=o:p.closeSuggestionsOnSelect=!p.multiple;var t=a("[for="+s.selectId+"]"),u=[];r.children("option").each(function(b,c){u[b]={label:c.innerHTML,value:a(c).attr("value")}});var w=a.extend({},p,s);w.options=u,w.items=[];var x="",y=d.render("core/form_autocomplete_input",w).then(function(a,b){return x+=b,a}),z=d.render("core/form_autocomplete_suggestions",w).then(function(a,b){return x+=b,a}),A=d.render("core/form_autocomplete_selection",w).then(function(a,b){return x+=b,a});return a.when(y,z,A).then(function(b,c,e){r.hide(),r.after(c),r.after(b),r.after(e),d.runTemplateJS(x),t.attr("for",s.inputId),v(p,s,r);var f=a(document.getElementById(s.suggestionsId));f.hide().attr("aria-hidden",!0)}).then(function(){return i(p,s,r)}).then(function(){return M.util.js_complete(q)})["catch"](function(a){M.util.js_complete(q),e.exception(a)})}}});
\ No newline at end of file
diff --git a/lib/amd/build/inplace_editable.min.js b/lib/amd/build/inplace_editable.min.js
index 84f656b8532..ad0ff95d43d 100644
--- a/lib/amd/build/inplace_editable.min.js
+++ b/lib/amd/build/inplace_editable.min.js
@@ -1 +1 @@
-define(["jquery","core/ajax","core/templates","core/notification","core/str","core/config","core/url","core/form-autocomplete"],function(a,b,c,d,e,f,g,h){return a("body").on("click keypress","[data-inplaceeditable] [data-inplaceeditablelink]",function(i){if("keypress"!==i.type||13===i.keyCode){i.stopImmediatePropagation(),i.preventDefault();var j=a(this),k=j.closest("[data-inplaceeditable]"),l=function(b){b.addClass("updating");var c=b.find("img.spinner");c.length?c.show():(c=a(" ").attr("src",g.imageUrl("i/loading_small")).addClass("spinner").addClass("smallicon"),b.append(c))},m=function(a){a.removeClass("updating"),a.find("img.spinner").hide()},n=function(e,f){var g=[e.attr("data-itemid"),e.attr("data-component"),e.attr("data-itemtype")].join("-");M.util.js_pending(g),l(e),b.call([{methodname:"core_update_inplace_editable",args:{itemid:e.attr("data-itemid"),component:e.attr("data-component"),itemtype:e.attr("data-itemtype"),value:f},done:function(b){var d=e.attr("data-value");c.render("core/inplace_editable",b).done(function(f,h){var i=a(f);c.replaceNode(e,i,h),i.find("[data-inplaceeditablelink]").focus(),i.trigger({type:"updated",ajaxreturn:b,oldvalue:d}),M.util.js_complete(g)})},fail:function(b){var c=a.Event("updatefailed",{exception:b,newvalue:f});m(e),M.util.js_complete(g),e.trigger(c),c.isDefaultPrevented()||d.exception(b)}}],!0)},o=function(a){a.find("input").off(),a.find("select").off(),a.html(a.attr("data-oldcontent")),a.removeAttr("data-oldcontent"),a.removeClass("inplaceeditingon"),a.find("[data-inplaceeditablelink]").focus()},p=function(){a("span.inplaceeditable.inplaceeditingon").each(function(){o(a(this))})},q=function(b,c){var d,e=b;for(d=0;d'+c+"").attr("id",q("id_editinstructions_",20)),e=a(' ').attr("id",q("id_inplacevalue_",20)).attr("value",b.attr("data-value")).attr("aria-describedby",d.attr("id")).addClass("ignoredirty").addClass("form-control"),g=a(''+k.attr("data-editlabel")+" ").attr("for",e.attr("id"));b.html("").append(d).append(g).append(e),e.focus(),e.select(),e.on("keyup keypress focusout",function(a){if(!f.behatsiterunning||"focusout"!==a.type){if("keypress"===a.type&&13===a.keyCode){var c=e.val();o(b),n(b,c)}("keyup"===a.type&&27===a.keyCode||"focusout"===a.type)&&o(b)}})})},s=function(a,b){o(a),n(a,b)},t=function(b,c){var d,e=a(" ").attr("id",q("id_inplacevalue_",20)).addClass("custom-select"),g=a(''+k.attr("data-editlabel")+" ").attr("for",e.attr("id"));for(d in c)e.append(a("").attr("value",c[d].key).html(c[d].value));e.val(b.attr("data-value")),b.html("").append(g).append(e),e.focus(),e.select(),e.on("keyup change focusout",function(a){if(!f.behatsiterunning||"focusout"!==a.type){if("change"===a.type){var c=e.val();o(b),n(b,c)}("keyup"===a.type&&27===a.keyCode||"focusout"===a.type)&&o(b)}})},u=function(b,f){var g,i=a(" ").attr("id",q("id_inplacevalue_",20)).addClass("form-autocomplete-original-select").addClass("custom-select"),j=a(''+k.attr("data-editlabel")+" ").attr("for",i.attr("id")),l=f.options,m=f.attributes,p=a(' '),r=a(' ');for(g in l)i.append(a(" ").attr("value",l[g].key).html(l[g].value));m.multiple&&i.attr("multiple","true"),i.val(JSON.parse(b.attr("data-value"))),e.get_string("savechanges","core").then(function(a){return c.renderPix("e/save","core",a)}).then(function(a){p.append(a)}).fail(d.exception),e.get_string("cancel","core").then(function(a){return c.renderPix("e/cancel","core",a)}).then(function(a){r.append(a)}).fail(d.exception),b.html("").append(j).append(i).append(p).append(r),i.focus(),i.select(),h.enhance(i,m.tags,m.ajax,m.placeholder,m.caseSensitive,m.showSuggestions,m.noSelectionString).then(function(){b.find("[role=combobox]").focus()}).fail(d.exception),i.on("keyup",function(a){("keyup"===a.type&&27===a.keyCode||"focusout"===a.type)&&o(b)}),p.on("click",function(a){var c=JSON.stringify(i.val());i.empty(),o(b),n(b,c),a.preventDefault()}),r.on("click",function(a){i.empty(),o(b),a.preventDefault()})},v=function(b){b.addClass("inplaceeditingon"),b.attr("data-oldcontent",b.html());var c=b.attr("data-type"),d=b.attr("data-options");"toggle"===c?s(b,d):"select"===c?t(b,a.parseJSON(d)):"autocomplete"===c?u(b,a.parseJSON(d)):r(b)};p(),v(k)}}),{}});
\ No newline at end of file
+define(["jquery","core/ajax","core/templates","core/notification","core/str","core/config","core/url","core/form-autocomplete","core/pending"],function(a,b,c,d,e,f,g,h,i){return a("body").on("click keypress","[data-inplaceeditable] [data-inplaceeditablelink]",function(j){if("keypress"!==j.type||13===j.keyCode){var k=new i("autocomplete-start-editing");j.stopImmediatePropagation(),j.preventDefault();var l=a(this),m=l.closest("[data-inplaceeditable]"),n=function(b){b.addClass("updating");var c=b.find("img.spinner");c.length?c.show():(c=a(" ").attr("src",g.imageUrl("i/loading_small")).addClass("spinner").addClass("smallicon"),b.append(c))},o=function(a){a.removeClass("updating"),a.find("img.spinner").hide()},p=function(e,f){var g=[e.attr("data-itemid"),e.attr("data-component"),e.attr("data-itemtype")].join("-"),h=new i(g);n(e),b.call([{methodname:"core_update_inplace_editable",args:{itemid:e.attr("data-itemid"),component:e.attr("data-component"),itemtype:e.attr("data-itemtype"),value:f}}])[0].then(function(b){return c.render("core/inplace_editable",b).then(function(d,f){var g=e.attr("data-value"),h=a(d);c.replaceNode(e,h,f),h.find("[data-inplaceeditablelink]").focus(),h.trigger({type:"updated",ajaxreturn:b,oldvalue:g})})}).then(function(){return h.resolve()}).fail(function(b){var c=a.Event("updatefailed",{exception:b,newvalue:f});o(e),M.util.js_complete(g),e.trigger(c),c.isDefaultPrevented()||d.exception(b)})},q=function(a){a.find("input").off(),a.find("select").off(),a.html(a.attr("data-oldcontent")),a.removeAttr("data-oldcontent"),a.removeClass("inplaceeditingon"),a.find("[data-inplaceeditablelink]").focus()},r=function(){a("span.inplaceeditable.inplaceeditingon").each(function(){q(a(this))})},s=function(b,c){var d,e=b;for(d=0;d'+c+"").attr("id",s("id_editinstructions_",20)),e=a(' ').attr("id",s("id_inplacevalue_",20)).attr("value",b.attr("data-value")).attr("aria-describedby",d.attr("id")).addClass("ignoredirty").addClass("form-control"),g=a(''+m.attr("data-editlabel")+" ").attr("for",e.attr("id"));b.html("").append(d).append(g).append(e),e.focus(),e.select(),e.on("keyup keypress focusout",function(a){if(!f.behatsiterunning||"focusout"!==a.type){if("keypress"===a.type&&13===a.keyCode){var c=e.val();q(b),p(b,c)}("keyup"===a.type&&27===a.keyCode||"focusout"===a.type)&&q(b)}})})},u=function(a,b){q(a),p(a,b)},v=function(b,c){var d,e=a(" ").attr("id",s("id_inplacevalue_",20)).addClass("custom-select"),g=a(''+m.attr("data-editlabel")+" ").attr("for",e.attr("id"));for(d in c)e.append(a("").attr("value",c[d].key).html(c[d].value));e.val(b.attr("data-value")),b.html("").append(g).append(e),e.focus(),e.select(),e.on("keyup change focusout",function(a){if(!f.behatsiterunning||"focusout"!==a.type){if("change"===a.type){var c=e.val();q(b),p(b,c)}("keyup"===a.type&&27===a.keyCode||"focusout"===a.type)&&q(b)}})},w=function(b,f){var g,i=a(" ").attr("id",s("id_inplacevalue_",20)).addClass("form-autocomplete-original-select").addClass("custom-select"),j=a(''+m.attr("data-editlabel")+" ").attr("for",i.attr("id")),k=f.options,l=f.attributes,n=a(' '),o=a(' ');for(g in k)i.append(a(" ").attr("value",k[g].key).html(k[g].value));l.multiple&&i.attr("multiple","true"),i.val(JSON.parse(b.attr("data-value"))),e.get_string("savechanges","core").then(function(a){return c.renderPix("e/save","core",a)}).then(function(a){n.append(a)}).fail(d.exception),e.get_string("cancel","core").then(function(a){return c.renderPix("e/cancel","core",a)}).then(function(a){o.append(a)}).fail(d.exception),b.html("").append(j).append(i).append(n).append(o),i.focus(),i.select(),h.enhance(i,l.tags,l.ajax,l.placeholder,l.caseSensitive,l.showSuggestions,l.noSelectionString).then(function(){b.find("[role=combobox]").focus()}).fail(d.exception),i.on("keyup",function(a){("keyup"===a.type&&27===a.keyCode||"focusout"===a.type)&&q(b)}),n.on("click",function(a){var c=JSON.stringify(i.val());i.empty(),q(b),p(b,c),a.preventDefault()}),o.on("click",function(a){i.empty(),q(b),a.preventDefault()})},x=function(b){b.addClass("inplaceeditingon"),b.attr("data-oldcontent",b.html());var c=b.attr("data-type"),d=b.attr("data-options");"toggle"===c?u(b,d):"select"===c?v(b,a.parseJSON(d)):"autocomplete"===c?w(b,a.parseJSON(d)):t(b)};r(),x(m),k.resolve()}}),{}});
\ No newline at end of file
diff --git a/lib/amd/src/form-autocomplete.js b/lib/amd/src/form-autocomplete.js
index c63e5617ac1..90275cbdb95 100644
--- a/lib/amd/src/form-autocomplete.js
+++ b/lib/amd/src/form-autocomplete.js
@@ -46,6 +46,7 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification']
* @private
* @param {Number} index The index in the current (visible) list of selection.
* @param {Object} state State variables for this autocomplete element.
+ * @return {Promise}
*/
var activateSelection = function(index, state) {
// Find the elements in the DOM.
@@ -69,6 +70,8 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification']
element.attr('data-active-selection', true).attr('id', itemId);
// Tell the input field it has a new active descendant so the item is announced.
selectionElement.attr('aria-activedescendant', itemId);
+
+ return $.Deferred().resolve();
};
/**
@@ -79,8 +82,12 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification']
* @param {Object} options Original options for this autocomplete element.
* @param {Object} state State variables for this autocomplete element.
* @param {JQuery} originalSelect The JQuery object matching the hidden select list.
+ * @return {Promise}
*/
var updateSelectionList = function(options, state, originalSelect) {
+ var pendingKey = 'form-autocomplete-updateSelectionList-' + state.inputId;
+ M.util.js_pending(pendingKey);
+
// Build up a valid context to re-render the template.
var items = [];
var newSelection = $(document.getElementById(state.selectionId));
@@ -104,9 +111,10 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification']
var context = $.extend({items: items}, options, state);
// Render the template.
- templates.render('core/form_autocomplete_selection', context).done(function(newHTML) {
+ return templates.render('core/form_autocomplete_selection', context)
+ .then(function(html, js) {
// Add it to the page.
- newSelection.empty().append($(newHTML).html());
+ templates.replaceNodeContents(newSelection, html, js);
if (activeValue !== false) {
// Reselect any previously selected item.
@@ -116,7 +124,13 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification']
}
});
}
- }).fail(notification.exception);
+
+ return activeValue;
+ })
+ .then(function() {
+ return M.util.js_complete(pendingKey);
+ })
+ .catch(notification.exception);
};
/**
@@ -140,6 +154,7 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification']
* @param {Object} state State variables for this autocomplete element.
* @param {Element} item The item to be deselected.
* @param {Element} originalSelect The original select list.
+ * @return {Promise}
*/
var deselectItem = function(options, state, item, originalSelect) {
var selectedItemValue = $(item).attr('data-value');
@@ -158,9 +173,13 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification']
});
}
// Rerender the selection list.
- updateSelectionList(options, state, originalSelect);
- // Notifiy that the selection changed.
- notifyChange(originalSelect);
+ return updateSelectionList(options, state, originalSelect)
+ .then(function() {
+ // Notify that the selection changed.
+ notifyChange(originalSelect);
+
+ return;
+ });
};
/**
@@ -170,6 +189,7 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification']
* @private
* @param {Number} index The index in the current (visible) list of suggestions.
* @param {Object} state State variables for this instance of autocomplete.
+ * @return {Promise}
*/
var activateItem = function(index, state) {
// Find the elements in the DOM.
@@ -202,9 +222,9 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification']
- suggestionsElement.offset().top
+ suggestionsElement.scrollTop()
- (suggestionsElement.height() / 2);
- suggestionsElement.animate({
+ return suggestionsElement.animate({
scrollTop: scrollPos
- }, 100);
+ }, 100).promise();
};
/**
@@ -213,6 +233,7 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification']
* @method activateNextItem
* @private
* @param {Object} state State variable for this auto complete element.
+ * @return {Promise}
*/
var activateNextItem = function(state) {
// Find the list of suggestions.
@@ -222,7 +243,7 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification']
// Find it's index.
var current = suggestionsElement.children('[aria-hidden=false]').index(element);
// Activate the next one.
- activateItem(current + 1, state);
+ return activateItem(current + 1, state);
};
/**
@@ -231,6 +252,7 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification']
* @method activatePreviousSelection
* @private
* @param {Object} state State variables for this instance of autocomplete.
+ * @return {Promise}
*/
var activatePreviousSelection = function(state) {
// Find the list of selections.
@@ -238,34 +260,40 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification']
// Find the active one.
var element = selectionsElement.children('[data-active-selection=true]');
if (!element) {
- activateSelection(0, state);
- return;
+ return activateSelection(0, state);
}
// Find it's index.
var current = selectionsElement.children('[aria-selected=true]').index(element);
// Activate the next one.
- activateSelection(current - 1, state);
+ return activateSelection(current - 1, state);
};
+
/**
* Find the index of the current active selection, and activate the next one.
*
* @method activateNextSelection
* @private
* @param {Object} state State variables for this instance of autocomplete.
+ * @return {Promise}
*/
var activateNextSelection = function(state) {
// Find the list of selections.
var selectionsElement = $(document.getElementById(state.selectionId));
+
// Find the active one.
var element = selectionsElement.children('[data-active-selection=true]');
- if (!element) {
- activateSelection(0, state);
- return;
+ var current = 0;
+
+ if (element) {
+ // The element was found. Determine the index and move to the next one.
+ current = selectionsElement.children('[aria-selected=true]').index(element);
+ current = current + 1;
+ } else {
+ // No selected item found. Move to the first.
+ current = 0;
}
- // Find it's index.
- var current = selectionsElement.children('[aria-selected=true]').index(element);
- // Activate the next one.
- activateSelection(current + 1, state);
+
+ return activateSelection(current, state);
};
/**
@@ -274,16 +302,20 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification']
* @method activatePreviousItem
* @private
* @param {Object} state State variables for this autocomplete element.
+ * @return {Promise}
*/
var activatePreviousItem = function(state) {
// Find the list of suggestions.
var suggestionsElement = $(document.getElementById(state.suggestionsId));
+
// Find the active one.
var element = suggestionsElement.children('[aria-selected=true]');
+
// Find it's index.
var current = suggestionsElement.children('[aria-hidden=false]').index(element);
- // Activate the next one.
- activateItem(current - 1, state);
+
+ // Activate the previous one.
+ return activateItem(current - 1, state);
};
/**
@@ -292,6 +324,7 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification']
* @method closeSuggestions
* @private
* @param {Object} state State variables for this autocomplete element.
+ * @return {Promise}
*/
var closeSuggestions = function(state) {
// Find the elements in the DOM.
@@ -300,8 +333,11 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification']
// Announce the list of suggestions was closed, and read the current list of selections.
inputElement.attr('aria-expanded', false).attr('aria-activedescendant', state.selectionId);
+
// Hide the suggestions list (from screen readers too).
suggestionsElement.hide().attr('aria-hidden', true);
+
+ return $.Deferred().resolve();
};
/**
@@ -313,8 +349,12 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification']
* @param {Object} state The state variables for this autocomplete.
* @param {String} query The current text for the search string.
* @param {JQuery} originalSelect The JQuery object matching the hidden select list.
+ * @return {Promise}
*/
var updateSuggestions = function(options, state, query, originalSelect) {
+ var pendingKey = 'form-autocomplete-updateSuggestions-' + state.inputId;
+ M.util.js_pending(pendingKey);
+
// Find the elements in the DOM.
var inputElement = $(document.getElementById(state.inputId));
var suggestionsElement = $(document.getElementById(state.suggestionsId));
@@ -332,12 +372,14 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification']
// Re-render the list of suggestions.
var searchquery = state.caseSensitive ? query : query.toLocaleLowerCase();
var context = $.extend({options: suggestions}, options, state);
- templates.render(
+ var returnVal = templates.render(
'core/form_autocomplete_suggestions',
context
- ).done(function(newHTML) {
+ )
+ .then(function(html, js) {
// We have the new template, insert it in the page.
- suggestionsElement.replaceWith(newHTML);
+ templates.replaceNode(suggestionsElement, html, js);
+
// Get the element again.
suggestionsElement = $(document.getElementById(state.suggestionsId));
// Show it if it is hidden.
@@ -371,8 +413,15 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification']
suggestionsElement.html(nosuggestionsstr);
});
}
- }).fail(notification.exception);
+ return suggestionsElement;
+ })
+ .then(function() {
+ return M.util.js_complete(pendingKey);
+ })
+ .catch(notification.exception);
+
+ return returnVal;
};
/**
@@ -383,6 +432,7 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification']
* @param {Object} options The original options for the autocomplete.
* @param {Object} state State variables for the autocomplete.
* @param {JQuery} originalSelect The JQuery object matching the hidden select list.
+ * @return {Promise}
*/
var createItem = function(options, state, originalSelect) {
// Find the element in the DOM.
@@ -419,13 +469,23 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification']
}
});
- updateSelectionList(options, state, originalSelect);
- // Notifiy that the selection changed.
- notifyChange(originalSelect);
- // Clear the input field.
- inputElement.val('');
- // Close the suggestions list.
- closeSuggestions(state);
+ return updateSelectionList(options, state, originalSelect)
+ .then(function() {
+ // Notify that the selection changed.
+ notifyChange(originalSelect);
+
+ return;
+ })
+ .then(function() {
+ // Clear the input field.
+ inputElement.val('');
+
+ return;
+ })
+ .then(function() {
+ // Close the suggestions list.
+ return closeSuggestions(state);
+ });
};
/**
@@ -436,6 +496,7 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification']
* @param {Object} options The original options for the autocomplete.
* @param {Object} state State variables for the autocomplete.
* @param {JQuery} originalSelect The JQuery object matching the hidden select list.
+ * @return {Promise}
*/
var selectCurrentItem = function(options, state, originalSelect) {
// Find the elements in the page.
@@ -458,22 +519,26 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification']
}
});
- // Rerender the selection list.
- updateSelectionList(options, state, originalSelect);
- // Notifiy that the selection changed.
- notifyChange(originalSelect);
+ return updateSelectionList(options, state, originalSelect)
+ .then(function() {
+ // Notify that the selection changed.
+ notifyChange(originalSelect);
- if (options.closeSuggestionsOnSelect) {
- // Clear the input element.
- inputElement.val('');
- // Close the list of suggestions.
- closeSuggestions(state);
- } else {
- // Focus on the input element so the suggestions does not auto-close.
- inputElement.focus();
- // Remove the last selected item from the suggestions list.
- updateSuggestions(options, state, inputElement.val(), originalSelect);
- }
+ return;
+ })
+ .then(function() {
+ if (options.closeSuggestionsOnSelect) {
+ // Clear the input element.
+ inputElement.val('');
+ // Close the list of suggestions.
+ return closeSuggestions(state);
+ } else {
+ // Focus on the input element so the suggestions does not auto-close.
+ inputElement.focus();
+ // Remove the last selected item from the suggestions list.
+ return updateSuggestions(options, state, inputElement.val(), originalSelect);
+ }
+ });
};
/**
@@ -486,10 +551,11 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification']
* @param {Object} state The state variables for the autocomplete.
* @param {JQuery} originalSelect The JQuery object matching the hidden select list.
* @param {Object} ajaxHandler This is a module that does the ajax fetch and translates the results.
+ * @return {Promise}
*/
var updateAjax = function(e, options, state, originalSelect, ajaxHandler) {
- var pendingKey = 'form-autocomplete-updateajax';
- M.util.js_pending(pendingKey);
+ var pendingPromise = addPendingJSPromise('updateAjax');
+
// Get the query to pass to the ajax function.
var query = $(e.currentTarget).val();
// Call the transport function to do the ajax (name taken from Select2).
@@ -531,12 +597,12 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification']
originalSelect.attr('data-notice', processedResults);
}
// Update the list of suggestions now from the new values in the select list.
- updateSuggestions(options, state, '', originalSelect);
- M.util.js_complete(pendingKey);
+ pendingPromise.resolve(updateSuggestions(options, state, '', originalSelect));
}, function(error) {
- M.util.js_complete(pendingKey);
- notification.exception(error);
+ pendingPromise.reject(error);
});
+
+ return pendingPromise;
};
/**
@@ -553,132 +619,123 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification']
var inputElement = $(document.getElementById(state.inputId));
// Add keyboard nav with keydown.
inputElement.on('keydown', function(e) {
- var pendingKey = 'form-autocomplete-addnav-' + state.inputId + '-' + e.keyCode;
- M.util.js_pending(pendingKey);
+ var pendingJsPromise = addPendingJSPromise('addNavigation-' + state.inputId + '-' + e.keyCode);
switch (e.keyCode) {
case KEYS.DOWN:
// If the suggestion list is open, move to the next item.
if (!options.showSuggestions) {
// Do not consume this event.
- M.util.js_complete(pendingKey);
+ pendingJsPromise.resolve();
return true;
} else if (inputElement.attr('aria-expanded') === "true") {
- activateNextItem(state);
+ pendingJsPromise.resolve(activateNextItem(state));
} else {
// Handle ajax population of suggestions.
if (!inputElement.val() && options.ajax) {
require([options.ajax], function(ajaxHandler) {
- updateAjax(e, options, state, originalSelect, ajaxHandler);
+ pendingJsPromise.resolve(updateAjax(e, options, state, originalSelect, ajaxHandler));
});
} else {
// Open the suggestions list.
- updateSuggestions(options, state, inputElement.val(), originalSelect);
+ pendingJsPromise.resolve(updateSuggestions(options, state, inputElement.val(), originalSelect));
}
}
// We handled this event, so prevent it.
e.preventDefault();
- M.util.js_complete(pendingKey);
return false;
case KEYS.UP:
// Choose the previous active item.
- activatePreviousItem(state);
+ pendingJsPromise.resolve(activatePreviousItem(state));
+
// We handled this event, so prevent it.
e.preventDefault();
- M.util.js_complete(pendingKey);
return false;
case KEYS.ENTER:
var suggestionsElement = $(document.getElementById(state.suggestionsId));
if ((inputElement.attr('aria-expanded') === "true") &&
(suggestionsElement.children('[aria-selected=true]').length > 0)) {
// If the suggestion list has an active item, select it.
- selectCurrentItem(options, state, originalSelect);
+ pendingJsPromise.resolve(selectCurrentItem(options, state, originalSelect));
} else if (options.tags) {
// If tags are enabled, create a tag.
- createItem(options, state, originalSelect);
+ pendingJsPromise.resolve(createItem(options, state, originalSelect));
+ } else {
+ pendingJsPromise.resolve();
}
+
// We handled this event, so prevent it.
e.preventDefault();
- M.util.js_complete(pendingKey);
return false;
case KEYS.ESCAPE:
if (inputElement.attr('aria-expanded') === "true") {
// If the suggestion list is open, close it.
- closeSuggestions(state);
+ pendingJsPromise.resolve(closeSuggestions(state));
+ } else {
+ pendingJsPromise.resolve();
}
// We handled this event, so prevent it.
e.preventDefault();
- M.util.js_complete(pendingKey);
return false;
}
- M.util.js_complete(pendingKey);
+ pendingJsPromise.resolve();
return true;
});
// Support multi lingual COMMA keycode (44).
inputElement.on('keypress', function(e) {
- var pendingKey = 'form-autocomplete-keypress-' + e.keyCode;
- M.util.js_pending(pendingKey);
+
if (e.keyCode === KEYS.COMMA) {
if (options.tags) {
// If we are allowing tags, comma should create a tag (or enter).
- createItem(options, state, originalSelect);
+ addPendingJSPromise('keypress-' + e.keyCode)
+ .resolve(createItem(options, state, originalSelect));
}
// We handled this event, so prevent it.
e.preventDefault();
- M.util.js_complete(pendingKey);
return false;
}
- M.util.js_complete(pendingKey);
return true;
});
- // Handler used to force set the value from behat.
- inputElement.on('behat:set-value', function() {
- var suggestionsElement = $(document.getElementById(state.suggestionsId));
- var pendingKey = 'form-autocomplete-behat';
- M.util.js_pending(pendingKey);
- if ((inputElement.attr('aria-expanded') === "true") &&
- (suggestionsElement.children('[aria-selected=true]').length > 0)) {
- // If the suggestion list has an active item, select it.
- selectCurrentItem(options, state, originalSelect);
- } else if (options.tags) {
- // If tags are enabled, create a tag.
- createItem(options, state, originalSelect);
- }
- M.util.js_complete(pendingKey);
- });
inputElement.on('blur', function() {
- var pendingKey = 'form-autocomplete-blur';
- M.util.js_pending(pendingKey);
+ var pendingPromise = addPendingJSPromise('form-autocomplete-blur');
window.setTimeout(function() {
// Get the current element with focus.
var focusElement = $(document.activeElement);
+
// Only close the menu if the input hasn't regained focus.
if (focusElement.attr('id') != inputElement.attr('id')) {
if (options.tags) {
- createItem(options, state, originalSelect);
+ pendingPromise.then(function() {
+ return createItem(options, state, originalSelect);
+ })
+ .catch();
}
- closeSuggestions(state);
+ pendingPromise.then(function() {
+ return closeSuggestions(state);
+ })
+ .catch();
}
- M.util.js_complete(pendingKey);
+
+ pendingPromise.resolve();
}, 500);
});
if (options.showSuggestions) {
var arrowElement = $(document.getElementById(state.downArrowId));
arrowElement.on('click', function(e) {
- var pendingKey = 'form-autocomplete-show-suggestions';
- M.util.js_pending(pendingKey);
+ var pendingPromise = addPendingJSPromise('form-autocomplete-show-suggestions');
+
// Prevent the close timer, or we will open, then close the suggestions.
inputElement.focus();
+
// Handle ajax population of suggestions.
if (!inputElement.val() && options.ajax) {
require([options.ajax], function(ajaxHandler) {
- updateAjax(e, options, state, originalSelect, ajaxHandler);
+ pendingPromise.resolve(updateAjax(e, options, state, originalSelect, ajaxHandler));
});
} else {
// Else - open the suggestions list.
- updateSuggestions(options, state, inputElement.val(), originalSelect);
+ pendingPromise.resolve(updateSuggestions(options, state, inputElement.val(), originalSelect));
}
- M.util.js_complete(pendingKey);
});
}
@@ -686,63 +743,65 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification']
// Remove any click handler first.
suggestionsElement.parent().prop("onclick", null).off("click");
suggestionsElement.parent().on('click', '[role=option]', function(e) {
- var pendingKey = 'form-autocomplete-parent';
- M.util.js_pending(pendingKey);
+ var pendingPromise = addPendingJSPromise('form-autocomplete-parent');
// Handle clicks on suggestions.
var element = $(e.currentTarget).closest('[role=option]');
var suggestionsElement = $(document.getElementById(state.suggestionsId));
// Find the index of the clicked on suggestion.
var current = suggestionsElement.children('[aria-hidden=false]').index(element);
+
// Activate it.
- activateItem(current, state);
- // And select it.
- selectCurrentItem(options, state, originalSelect);
- M.util.js_complete(pendingKey);
+ activateItem(current, state)
+ .then(function() {
+ // And select it.
+ return selectCurrentItem(options, state, originalSelect);
+ })
+ .then(function() {
+ return pendingPromise.resolve();
+ })
+ .catch();
});
var selectionElement = $(document.getElementById(state.selectionId));
// Handle clicks on the selected items (will unselect an item).
selectionElement.on('click', '[role=listitem]', function(e) {
- var pendingKey = 'form-autocomplete-clicks';
- M.util.js_pending(pendingKey);
- // Get the item that was clicked.
- var item = $(e.currentTarget);
+ var pendingPromise = addPendingJSPromise('form-autocomplete-clicks');
+
// Remove it from the selection.
- deselectItem(options, state, item, originalSelect);
- M.util.js_complete(pendingKey);
+ pendingPromise.resolve(deselectItem(options, state, $(e.currentTarget), originalSelect));
});
// Keyboard navigation for the selection list.
selectionElement.on('keydown', function(e) {
- var pendingKey = 'form-autocomplete-keydown-' + e.keyCode;
- M.util.js_pending(pendingKey);
+ var pendingPromise = addPendingJSPromise('form-autocomplete-keydown-' + e.keyCode);
switch (e.keyCode) {
case KEYS.DOWN:
- // Choose the next selection item.
- activateNextSelection(state);
// We handled this event, so prevent it.
e.preventDefault();
- M.util.js_complete(pendingKey);
+
+ // Choose the next selection item.
+ pendingPromise.resolve(activateNextSelection(state));
return false;
case KEYS.UP:
- // Choose the previous selection item.
- activatePreviousSelection(state);
// We handled this event, so prevent it.
e.preventDefault();
- M.util.js_complete(pendingKey);
+
+ // Choose the previous selection item.
+ pendingPromise.resolve(activatePreviousSelection(state));
return false;
case KEYS.SPACE:
case KEYS.ENTER:
// Get the item that is currently selected.
var selectedItem = $(document.getElementById(state.selectionId)).children('[data-active-selection=true]');
if (selectedItem) {
- // Unselect this item.
- deselectItem(options, state, selectedItem, originalSelect);
- // We handled this event, so prevent it.
e.preventDefault();
+
+ // Unselect this item.
+ pendingPromise.resolve(deselectItem(options, state, selectedItem, originalSelect));
}
- M.util.js_complete(pendingKey);
return false;
}
- M.util.js_complete(pendingKey);
+
+ // Not handled. Resolve the promise.
+ pendingPromise.resolve();
return true;
});
// Whenever the input field changes, update the suggestion list.
@@ -750,24 +809,61 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification']
// If this field uses ajax, set it up.
if (options.ajax) {
require([options.ajax], function(ajaxHandler) {
+ // Creating throttled handlers free of race conditions, and accurate.
+ // This code keeps track of a throttleTimeout, which is periodically polled.
+ // Once the throttled function is executed, the fact that it is running is noted.
+ // If a subsequent request comes in whilst it is running, this request is re-applied.
var throttleTimeout = null;
+ var inProgress = false;
var pendingKey = 'autocomplete-throttledhandler';
var handler = function(e) {
- updateAjax(e, options, state, originalSelect, ajaxHandler);
- M.util.js_complete(pendingKey);
+ // Empty the current timeout.
+ throttleTimeout = null;
+
+ // Mark this request as in-progress.
+ inProgress = true;
+
+ // Process the request.
+ updateAjax(e, options, state, originalSelect, ajaxHandler)
+ .then(function() {
+ // Check if the throttleTimeout is still empty.
+ // There's a potential condition whereby the JS request takes long enough to complete that
+ // another task has been queued.
+ // In this case another task will be kicked off and we must wait for that before marking htis as
+ // complete.
+ if (null === throttleTimeout) {
+ // Mark this task as complete.
+ M.util.js_complete(pendingKey);
+ }
+ inProgress = false;
+
+ return arguments[0];
+ })
+ .catch(notification.exception);
};
// For input events, we do not want to trigger many, many updates.
var throttledHandler = function(e) {
- if (throttleTimeout !== null) {
- window.clearTimeout(throttleTimeout);
- throttleTimeout = null;
- } else {
- // No existing timeout handler, so this is the start of a throttling check.
+ window.clearTimeout(throttleTimeout);
+ if (inProgress) {
+ // A request is currently ongoing.
+ // Delay this request another 100ms.
+ throttleTimeout = window.setTimeout(throttledHandler.bind(this, e), 100);
+ return;
+ }
+
+ if (throttleTimeout === null) {
+ // There is currently no existing timeout handler, and it has not been recently cleared, so
+ // this is the start of a throttling check.
M.util.js_pending(pendingKey);
}
+
+ // There is currently no existing timeout handler, and it has not been recently cleared, so this
+ // is the start of a throttling check.
+ // Queue a call to the handler.
throttleTimeout = window.setTimeout(handler.bind(this, e), 300);
};
+
// Trigger an ajax update after the text field value changes.
inputElement.on("input", throttledHandler);
});
@@ -789,6 +885,30 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification']
}
};
+ /**
+ * Create and return an unresolved Promise for some pending JS.
+ *
+ * @param {String} key The unique identifier for this promise
+ * @return {Promise}
+ */
+ var addPendingJSPromise = function(key) {
+ var pendingKey = 'form-autocomplete:' + key;
+
+ M.util.js_pending(pendingKey);
+
+ var pendingPromise = $.Deferred();
+
+ pendingPromise
+ .then(function() {
+ M.util.js_complete(pendingKey);
+
+ return arguments[0];
+ })
+ .catch(notification.exception);
+
+ return pendingPromise;
+ };
+
return /** @alias module:core/form-autocomplete */ {
// Public variables and functions.
/**
@@ -902,7 +1022,8 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification']
return html;
});
- return $.when(renderInput, renderDatalist, renderSelection).then(function(input, suggestions, selection) {
+ return $.when(renderInput, renderDatalist, renderSelection)
+ .then(function(input, suggestions, selection) {
originalSelect.hide();
originalSelect.after(suggestions);
originalSelect.after(input);
@@ -919,11 +1040,16 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification']
// Hide the suggestions by default.
suggestionsElement.hide().attr('aria-hidden', true);
+ return;
+ })
+ .then(function() {
// Show the current values in the selection list.
- updateSelectionList(options, state, originalSelect);
- M.util.js_complete(pendingKey);
- return true;
- }).fail(function(error) {
+ return updateSelectionList(options, state, originalSelect);
+ })
+ .then(function() {
+ return M.util.js_complete(pendingKey);
+ })
+ .catch(function(error) {
M.util.js_complete(pendingKey);
notification.exception(error);
});
diff --git a/lib/amd/src/inplace_editable.js b/lib/amd/src/inplace_editable.js
index 90ddf2d6f33..1021c332ba9 100644
--- a/lib/amd/src/inplace_editable.js
+++ b/lib/amd/src/inplace_editable.js
@@ -35,13 +35,16 @@ define(['jquery',
'core/str',
'core/config',
'core/url',
- 'core/form-autocomplete'],
- function($, ajax, templates, notification, str, cfg, url, autocomplete) {
+ 'core/form-autocomplete',
+ 'core/pending',
+ ],
+ function($, ajax, templates, notification, str, cfg, url, autocomplete, Pending) {
$('body').on('click keypress', '[data-inplaceeditable] [data-inplaceeditablelink]', function(e) {
if (e.type === 'keypress' && e.keyCode !== 13) {
return;
}
+ var editingEnabledPromise = new Pending('autocomplete-start-editing');
e.stopImmediatePropagation();
e.preventDefault();
var target = $(this),
@@ -72,41 +75,49 @@ define(['jquery',
mainelement.attr('data-component'),
mainelement.attr('data-itemtype'),
].join('-');
- M.util.js_pending(pendingId);
+ var pendingPromise = new Pending(pendingId);
addSpinner(mainelement);
- ajax
- .call([{
- methodname: 'core_update_inplace_editable',
- args: {
- itemid: mainelement.attr('data-itemid'),
- component: mainelement.attr('data-component'),
- itemtype: mainelement.attr('data-itemtype'),
- value: value
- },
- done: function(data) {
- var oldvalue = mainelement.attr('data-value');
- templates.render('core/inplace_editable', data).done(function(html, js) {
- var newelement = $(html);
- templates.replaceNode(mainelement, newelement, js);
- newelement.find('[data-inplaceeditablelink]').focus();
- newelement.trigger({type: 'updated', ajaxreturn: data, oldvalue: oldvalue});
- M.util.js_complete(pendingId);
- });
- },
- fail: function(ex) {
- var e = $.Event('updatefailed', {
- exception: ex,
- newvalue: value
- });
- removeSpinner(mainelement);
- M.util.js_complete(pendingId);
- mainelement.trigger(e);
- if (!e.isDefaultPrevented()) {
- notification.exception(ex);
- }
- }
- }], true);
+ ajax.call([{
+ methodname: 'core_update_inplace_editable',
+ args: {
+ itemid: mainelement.attr('data-itemid'),
+ component: mainelement.attr('data-component'),
+ itemtype: mainelement.attr('data-itemtype'),
+ value: value,
+ },
+ }])[0]
+ .then(function(data) {
+ return templates.render('core/inplace_editable', data)
+ .then(function(html, js) {
+ var oldvalue = mainelement.attr('data-value');
+ var newelement = $(html);
+ templates.replaceNode(mainelement, newelement, js);
+ newelement.find('[data-inplaceeditablelink]').focus();
+ newelement.trigger({
+ type: 'updated',
+ ajaxreturn: data,
+ oldvalue: oldvalue,
+ });
+
+ return;
+ });
+ })
+ .then(function() {
+ return pendingPromise.resolve();
+ })
+ .fail(function(ex) {
+ var e = $.Event('updatefailed', {
+ exception: ex,
+ newvalue: value
+ });
+ removeSpinner(mainelement);
+ M.util.js_complete(pendingId);
+ mainelement.trigger(e);
+ if (!e.isDefaultPrevented()) {
+ notification.exception(ex);
+ }
+ });
};
var turnEditingOff = function(el) {
@@ -319,6 +330,7 @@ define(['jquery',
// Turn editing on for the current element and register handler for Enter/Esc keys.
turnEditingOffEverywhere();
turnEditingOn(mainelement);
+ editingEnabledPromise.resolve();
});
diff --git a/lib/behat/behat_base.php b/lib/behat/behat_base.php
index e7c631fada5..185d901b2e8 100644
--- a/lib/behat/behat_base.php
+++ b/lib/behat/behat_base.php
@@ -28,10 +28,11 @@
// NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php.
-use Behat\Mink\Exception\DriverException,
- Behat\Mink\Exception\ExpectationException as ExpectationException,
- Behat\Mink\Exception\ElementNotFoundException as ElementNotFoundException,
- Behat\Mink\Element\NodeElement as NodeElement;
+use Behat\Mink\Exception\DriverException;
+use Behat\Mink\Exception\ExpectationException;
+use Behat\Mink\Exception\ElementNotFoundException;
+use Behat\Mink\Element\NodeElement;
+use Behat\Mink\Session;
/**
* Steps definitions base class.
@@ -709,23 +710,30 @@ class behat_base extends Behat\MinkExtension\Context\RawMinkContext {
/**
* Waits for all the JS to be loaded.
*
- * @throws \Exception
- * @throws NoSuchWindow
- * @throws UnknownError
- * @return bool True or false depending whether all the JS is loaded or not.
+ * @return bool Whether any JS is still pending completion.
*/
public function wait_for_pending_js() {
- // Waiting for JS is only valid for JS scenarios.
if (!$this->running_javascript()) {
- return;
+ // JS is not available therefore there is nothing to wait for.
+ return false;
}
+ return static::wait_for_pending_js_in_session($this->getSession());
+ }
+
+ /**
+ * Waits for all the JS to be loaded.
+ *
+ * @param Session $session The Mink Session where JS can be run
+ * @return bool Whether any JS is still pending completion.
+ */
+ public static function wait_for_pending_js_in_session(Session $session) {
// We don't use behat_base::spin() here as we don't want to end up with an exception
// if the page & JSs don't finish loading properly.
for ($i = 0; $i < self::EXTENDED_TIMEOUT * 10; $i++) {
$pending = '';
try {
- $jscode = '
+ $jscode = trim(preg_replace('/\s+/', ' ', '
return (function() {
if (typeof M === "undefined") {
if (document.readyState === "complete") {
@@ -740,8 +748,8 @@ class behat_base extends Behat\MinkExtension\Context\RawMinkContext {
} else {
return "incomplete"
}
- }());';
- $pending = $this->getSession()->evaluateScript($jscode);
+ }());'));
+ $pending = $session->evaluateScript($jscode);
} catch (NoSuchWindow $nsw) {
// We catch an exception here, in case we just closed the window we were interacting with.
// No javascript is running if there is no window right?
@@ -762,7 +770,7 @@ class behat_base extends Behat\MinkExtension\Context\RawMinkContext {
usleep(100000);
}
- // Timeout waiting for JS to complete. It will be catched and forwarded to behat_hooks::i_look_for_exceptions().
+ // Timeout waiting for JS to complete. It will be caught and forwarded to behat_hooks::i_look_for_exceptions().
// It is unlikely that Javascript code of a page or an AJAX request needs more than self::EXTENDED_TIMEOUT seconds
// to be loaded, although when pages contains Javascript errors M.util.js_complete() can not be executed, so the
// number of JS pending code and JS completed code will not match and we will reach this point.
diff --git a/lib/behat/form_field/behat_form_autocomplete.php b/lib/behat/form_field/behat_form_autocomplete.php
index 2453d7abc24..873695a8d88 100644
--- a/lib/behat/form_field/behat_form_autocomplete.php
+++ b/lib/behat/form_field/behat_form_autocomplete.php
@@ -47,13 +47,45 @@ class behat_form_autocomplete extends behat_form_text {
if (!$this->running_javascript()) {
throw new coding_exception('Setting the valid of an autocomplete field requires javascript.');
}
- $this->field->setValue($value);
- // After the value is set, there is a 400ms throttle and then search. So adding 2 sec. delay to ensure both
- // throttle + search finishes.
- sleep(2);
- $id = $this->field->getAttribute('id');
- $js = ' require(["jquery"], function($) { $(document.getElementById("'.$id.'")).trigger("behat:set-value"); }); ';
- $this->session->executeScript($js);
- $this->key_press(27);
+
+ // Set the value of the autocomplete's input.
+ // If this autocomplete offers suggestions then these should be fetched by setting the value and waiting for the
+ // JS to finish fetching those suggestions.
+
+ $istagelement = $this->field->hasAttribute('data-tags') && $this->field->getAttribute('data-tags');
+
+ if ($istagelement && false !== strpos($value, ',')) {
+ // Commas have a special meaning as a value separator in 'tag' autocomplete elements.
+ // To handle this we break the value up by comma, and enter it in chunks.
+ $values = explode(',', $value);
+
+ while ($value = array_shift($values)) {
+ $this->set_value($value);
+ }
+ } else {
+ $this->field->setValue($value);
+ $this->wait_for_pending_js();
+
+ // If the autocomplete found suggestions, then it will have:
+ // 1) marked itself as expanded; and
+ // 2) have an aria-selected suggestion in the list.
+ $expanded = $this->field->getAttribute('aria-expanded');
+ $suggestion = $this->field->getParent()->find('css', '.form-autocomplete-suggestions > [aria-selected="true"]');
+
+ if ($expanded && null !== $suggestion) {
+ // A suggestion was found.
+ // Click on the first item in the list.
+ $suggestion->click();
+ } else {
+ // Press the return key to create a new tag.
+ // Note: We cannot use $this->key_press() because the keyPress action, in combination with the keyDown
+ // submits the form.
+ $this->field->keyDown(13);
+ $this->field->keyUp(13);
+ }
+
+ $this->wait_for_pending_js();
+ $this->key_press(27);
+ }
}
}
diff --git a/lib/behat/form_field/behat_form_field.php b/lib/behat/form_field/behat_form_field.php
index 56d7124f117..d6da75c63c4 100644
--- a/lib/behat/form_field/behat_form_field.php
+++ b/lib/behat/form_field/behat_form_field.php
@@ -172,6 +172,20 @@ class behat_form_field {
return get_class($this->session->getDriver()) !== 'Behat\Mink\Driver\GoutteDriver';
}
+ /**
+ * Waits for all the JS activity to be completed.
+ *
+ * @return bool Whether any JS is still pending completion.
+ */
+ protected function wait_for_pending_js() {
+ if (!$this->running_javascript()) {
+ // JS is not available therefore there is nothing to wait for.
+ return false;
+ }
+
+ return behat_base::wait_for_pending_js_in_session($this->session);
+ }
+
/**
* Gets the field internal id used by selenium wire protocol.
*
diff --git a/lib/templates/form_autocomplete_input.mustache b/lib/templates/form_autocomplete_input.mustache
index 7bf50328ced..38249a88a13 100644
--- a/lib/templates/form_autocomplete_input.mustache
+++ b/lib/templates/form_autocomplete_input.mustache
@@ -36,7 +36,7 @@
{ "inputID": 1, "suggestionsId": 2, "selectionId": 3, "downArrowId": 4, "placeholder": "Select something" }
}}
{{#showSuggestions}}
-▼
+▼
{{/showSuggestions}}
{{^showSuggestions}}
diff --git a/lib/testing/generator/data_generator.php b/lib/testing/generator/data_generator.php
index b52fa58da65..c97e5be2578 100644
--- a/lib/testing/generator/data_generator.php
+++ b/lib/testing/generator/data_generator.php
@@ -343,7 +343,7 @@ EOD;
}
if (!isset($record['description'])) {
- $record['description'] = "Test cohort $i\n$this->loremipsum";
+ $record['description'] = "Description for '{$record['name']}' \n$this->loremipsum";
}
if (!isset($record['descriptionformat'])) {
diff --git a/lib/testing/tests/generator_test.php b/lib/testing/tests/generator_test.php
index 33bdd99dfa1..d22a9101a4a 100644
--- a/lib/testing/tests/generator_test.php
+++ b/lib/testing/tests/generator_test.php
@@ -156,7 +156,7 @@ class core_test_generator_testcase extends advanced_testcase {
$this->assertEquals(context_system::instance()->id, $cohort->contextid);
$this->assertRegExp('/^Cohort \d/', $cohort->name);
$this->assertSame('', $cohort->idnumber);
- $this->assertRegExp('/^Test cohort \d/', $cohort->description);
+ $this->assertRegExp("/^Description for '{$cohort->name}' \\n/", $cohort->description);
$this->assertSame(FORMAT_MOODLE, $cohort->descriptionformat);
$this->assertSame('', $cohort->component);
$this->assertLessThanOrEqual(time(), $cohort->timecreated);
diff --git a/mod/feedback/tests/behat/coursemapping.feature b/mod/feedback/tests/behat/coursemapping.feature
index c3b84d3eb30..540efe8d2f2 100644
--- a/mod/feedback/tests/behat/coursemapping.feature
+++ b/mod/feedback/tests/behat/coursemapping.feature
@@ -138,10 +138,6 @@ Feature: Mapping courses in a feedback
And I follow "Map feedback to courses"
And I set the field "Courses" to "Course 2"
And I set the field "Courses" to "Course 3"
- # Weird solution to make the editable field to lose the focus
- # but with the focus, "save changes" uses to fail because of
- # the suggestions hiding the button.
- And I press key "27" in the field "Courses"
And I press "Save changes"
And I should see "Course mapping has been changed"
And I log out
diff --git a/theme/boost/templates/core/form_autocomplete_input.mustache b/theme/boost/templates/core/form_autocomplete_input.mustache
index 0d2b9664c94..20e6430793a 100644
--- a/theme/boost/templates/core/form_autocomplete_input.mustache
+++ b/theme/boost/templates/core/form_autocomplete_input.mustache
@@ -36,7 +36,7 @@
{ "inputID": 1, "suggestionsId": 2, "selectionId": 3, "downArrowId": 4, "placeholder": "Select something" }
}}
{{#showSuggestions}}
-▼
+▼
{{/showSuggestions}}
{{^showSuggestions}}
diff --git a/theme/upgrade.txt b/theme/upgrade.txt
index 64fdd873bd4..e2b22eaf779 100644
--- a/theme/upgrade.txt
+++ b/theme/upgrade.txt
@@ -1,6 +1,9 @@
This files describes API changes in /theme/* themes,
information provided here is intended especially for theme designer.
+=== 3.5.4 ===
+* The core/form_autocomplete_input template now has a `data-tags` attribute.
+
=== 3.4 ===
* There was a mustache template login.mustache in /lib/templates/ rendering the login form