Merge branch 'MDL-62514-35' of git://github.com/andrewnicols/moodle into MOODLE_35_STABLE

This commit is contained in:
Eloy Lafuente (stronk7)
2019-01-30 23:01:27 +01:00
14 changed files with 395 additions and 202 deletions
@@ -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"
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -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
View File
@@ -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);
});
+47 -35
View File
@@ -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
View File
@@ -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);
}
}
}
+14
View File
@@ -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}}">&#x25BC;</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}}">&#x25BC;</span>
{{/showSuggestions}}
{{^showSuggestions}}
<input type="text" id="{{inputId}}" placeholder="{{placeholder}}" role="textbox" aria-owns="{{selectionId}}"/>
+1 -1
View File
@@ -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'])) {
+1 -1
View File
@@ -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}}">&#x25BC;</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}}">&#x25BC;</span>
{{/showSuggestions}}
{{^showSuggestions}}
<input type="text" id="{{inputId}}" placeholder="{{placeholder}}" role="textbox" aria-owns="{{selectionId}}"/>
+3
View File
@@ -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