Merge branch 'MDL-62514-35' of git://github.com/andrewnicols/moodle into MOODLE_35_STABLE
This commit is contained in:
@@ -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"
|
||||
|
||||
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
@@ -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("<img/>").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;d++)e+=String(Math.floor(10*Math.random()));return 0===a("#"+e).length?e:q(b,c)},r=function(b){e.get_string("edittitleinstructions").done(function(c){var d=a('<span class="editinstructions">'+c+"</span>").attr("id",q("id_editinstructions_",20)),e=a('<input type="text"/>').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('<label class="accesshide">'+k.attr("data-editlabel")+"</label>").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("<select></select>").attr("id",q("id_inplacevalue_",20)).addClass("custom-select"),g=a('<label class="accesshide">'+k.attr("data-editlabel")+"</label>").attr("for",e.attr("id"));for(d in c)e.append(a("<option>").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("<select></select>").attr("id",q("id_inplacevalue_",20)).addClass("form-autocomplete-original-select").addClass("custom-select"),j=a('<label class="accesshide">'+k.attr("data-editlabel")+"</label>").attr("for",i.attr("id")),l=f.options,m=f.attributes,p=a('<a href="#"></a>'),r=a('<a href="#"></a>');for(g in l)i.append(a("<option>").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)}}),{}});
|
||||
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("<img/>").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;d++)e+=String(Math.floor(10*Math.random()));return 0===a("#"+e).length?e:s(b,c)},t=function(b){e.get_string("edittitleinstructions").done(function(c){var d=a('<span class="editinstructions">'+c+"</span>").attr("id",s("id_editinstructions_",20)),e=a('<input type="text"/>').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('<label class="accesshide">'+m.attr("data-editlabel")+"</label>").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("<select></select>").attr("id",s("id_inplacevalue_",20)).addClass("custom-select"),g=a('<label class="accesshide">'+m.attr("data-editlabel")+"</label>").attr("for",e.attr("id"));for(d in c)e.append(a("<option>").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("<select></select>").attr("id",s("id_inplacevalue_",20)).addClass("form-autocomplete-original-select").addClass("custom-select"),j=a('<label class="accesshide">'+m.attr("data-editlabel")+"</label>").attr("for",i.attr("id")),k=f.options,l=f.attributes,n=a('<a href="#"></a>'),o=a('<a href="#"></a>');for(g in k)i.append(a("<option>").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()}}),{}});
|
||||
+261
-135
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
|
||||
});
|
||||
|
||||
|
||||
+22
-14
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
{ "inputID": 1, "suggestionsId": 2, "selectionId": 3, "downArrowId": 4, "placeholder": "Select something" }
|
||||
}}
|
||||
{{#showSuggestions}}
|
||||
<input type="text" id="{{inputId}}" list="{{suggestionsId}}" placeholder="{{placeholder}}" role="combobox" aria-expanded="false" autocomplete="off" autocorrect="off" autocapitalize="off" aria-autocomplete="list" aria-owns="{{suggestionsId}} {{selectionId}}"/><span class="form-autocomplete-downarrow" id="{{downArrowId}}">▼</span>
|
||||
<input type="text" id="{{inputId}}" list="{{suggestionsId}}" placeholder="{{placeholder}}" role="combobox" aria-expanded="false" autocomplete="off" autocorrect="off" autocapitalize="off" aria-autocomplete="list" aria-owns="{{suggestionsId}} {{selectionId}}" {{#tags}}data-tags="1"{{/tags}}/><span class="form-autocomplete-downarrow" id="{{downArrowId}}">▼</span>
|
||||
{{/showSuggestions}}
|
||||
{{^showSuggestions}}
|
||||
<input type="text" id="{{inputId}}" placeholder="{{placeholder}}" role="textbox" aria-owns="{{selectionId}}"/>
|
||||
|
||||
@@ -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'])) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
{ "inputID": 1, "suggestionsId": 2, "selectionId": 3, "downArrowId": 4, "placeholder": "Select something" }
|
||||
}}
|
||||
{{#showSuggestions}}
|
||||
<input type="text" id="{{inputId}}" class="form-control" list="{{suggestionsId}}" placeholder="{{placeholder}}" role="combobox" aria-expanded="false" autocomplete="off" autocorrect="off" autocapitalize="off" aria-autocomplete="list" aria-owns="{{suggestionsId}} {{selectionId}}"/><span class="form-autocomplete-downarrow" id="{{downArrowId}}">▼</span>
|
||||
<input type="text" id="{{inputId}}" class="form-control" list="{{suggestionsId}}" placeholder="{{placeholder}}" role="combobox" aria-expanded="false" autocomplete="off" autocorrect="off" autocapitalize="off" aria-autocomplete="list" aria-owns="{{suggestionsId}} {{selectionId}}" {{#tags}}data-tags="1"{{/tags}}/><span class="form-autocomplete-downarrow" id="{{downArrowId}}">▼</span>
|
||||
{{/showSuggestions}}
|
||||
{{^showSuggestions}}
|
||||
<input type="text" id="{{inputId}}" placeholder="{{placeholder}}" role="textbox" aria-owns="{{selectionId}}"/>
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user