diff --git a/lib/amd/src/form-autocomplete.js b/lib/amd/src/form-autocomplete.js
index 90275cbdb95..60204fbe7ab 100644
--- a/lib/amd/src/form-autocomplete.js
+++ b/lib/amd/src/form-autocomplete.js
@@ -24,7 +24,9 @@
* @since 3.0
*/
/* globals require: false */
-define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification'], function($, log, str, templates, notification) {
+define(
+ ['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification', 'core/loadingicon'],
+function($, log, str, templates, notification, LoadingIcon) {
// Private functions and variables.
/** @var {Object} KEYS - List of keycode constants. */
@@ -555,6 +557,7 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification']
*/
var updateAjax = function(e, options, state, originalSelect, ajaxHandler) {
var pendingPromise = addPendingJSPromise('updateAjax');
+ LoadingIcon.addIconToContainerRemoveOnCompletion($(document.getElementById(state.downArrowId)), pendingPromise);
// Get the query to pass to the ajax function.
var query = $(e.currentTarget).val();
diff --git a/lib/amd/src/loadingicon.js b/lib/amd/src/loadingicon.js
new file mode 100644
index 00000000000..33da54a9213
--- /dev/null
+++ b/lib/amd/src/loadingicon.js
@@ -0,0 +1,110 @@
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see .
+
+/**
+ * Contain the logic for the save/cancel modal.
+ *
+ * @module core/loading_icon
+ * @class loading_icon
+ * @package core
+ * @copyright 2019 Andrew Nicols
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+define(['jquery', 'core/templates'], function($, Templates) {
+ var TEMPLATES = {
+ LOADING: 'core/loading',
+ };
+
+ var getIcon = function() {
+ return Templates.render(TEMPLATES.LOADING, {});
+ };
+
+ /**
+ * Add a loading icon to the end of the specified container and return an unresolved promise.
+ *
+ * Resolution of the returned promise causes the icon to be faded out and removed.
+ *
+ * @method addIconToContainer
+ * @param {jQuery} container The element to add the spinner to
+ * @return {jQuery} The Promise used to create the icon.
+ */
+ var addIconToContainer = function(container) {
+ return getIcon()
+ .then(function(html) {
+ var loadingIcon = $(html).hide();
+ container.append(loadingIcon);
+ loadingIcon.fadeIn(150);
+
+ return loadingIcon;
+ });
+ };
+
+ /**
+ * Add a loading icon to the end of the specified container and return an unresolved promise.
+ *
+ * Resolution of the returned promise causes the icon to be faded out and removed.
+ *
+ * @method addIconToContainerWithPromise
+ * @param {jQuery} container The element to add the spinner to
+ * @param {jQuery} loadingIconPromise The jQuery Promise which determines the removal of the icon
+ * @return {jQuery} The Promise used to create and then remove the icon.
+ */
+ var addIconToContainerRemoveOnCompletion = function(container, loadingIconPromise) {
+ return getIcon()
+ .then(function(html) {
+ var loadingIcon = $(html).hide();
+ container.append(loadingIcon);
+ loadingIcon.fadeIn(150);
+
+ return $.when(loadingIcon.promise(), loadingIconPromise);
+ })
+ .then(function(loadingIcon) {
+ // Once the content has finished loading and
+ // the loading icon has been shown then we can
+ // fade the icon away to reveal the content.
+ return loadingIcon.fadeOut(100).promise();
+ })
+ .then(function(loadingIcon) {
+ loadingIcon.remove();
+
+ return;
+ });
+ };
+
+ /**
+ * Add a loading icon to the end of the specified container and return an unresolved promise.
+ *
+ * Resolution of the returned promise causes the icon to be faded out and removed.
+ *
+ * @method addIconToContainerWithPromise
+ * @param {jQuery} container The element to add the spinner to
+ * @return {jQuery} A jQuery Promise to resolve when ready
+ */
+ var addIconToContainerWithPromise = function(container) {
+ var loadingIconPromise = $.Deferred();
+
+ addIconToContainerRemoveOnCompletion(container, loadingIconPromise);
+
+ return loadingIconPromise;
+ };
+
+ return {
+ getIcon: getIcon,
+ addIconToContainer: addIconToContainer,
+ addIconToContainerWithPromise: addIconToContainerWithPromise,
+ addIconToContainerRemoveOnCompletion: addIconToContainerRemoveOnCompletion,
+ };
+
+});
diff --git a/theme/boost/scss/moodle/forms.scss b/theme/boost/scss/moodle/forms.scss
index 469e034a3f1..96b59be01ee 100644
--- a/theme/boost/scss/moodle/forms.scss
+++ b/theme/boost/scss/moodle/forms.scss
@@ -322,6 +322,13 @@ fieldset.coursesearchbox label {
top: 0.2em;
left: -1.5em;
cursor: pointer;
+
+ .loading-icon {
+ position: absolute;
+ top: 0;
+ left: 0;
+ background-color: $white;
+ }
}
.form-autocomplete-selection:focus {
diff --git a/theme/boost/style/moodle.css b/theme/boost/style/moodle.css
index a083d608c85..772d07196b7 100644
--- a/theme/boost/style/moodle.css
+++ b/theme/boost/style/moodle.css
@@ -15248,6 +15248,11 @@ fieldset.coursesearchbox label {
top: 0.2em;
left: -1.5em;
cursor: pointer; }
+ .form-autocomplete-downarrow .loading-icon {
+ position: absolute;
+ top: 0;
+ left: 0;
+ background-color: #fff; }
.form-autocomplete-selection:focus {
outline: none; }