From df5feea4903020bd0c63f526ea3e4a5c60fd4ad7 Mon Sep 17 00:00:00 2001 From: Andrew Nicols Date: Mon, 14 Jan 2019 13:48:27 +0800 Subject: [PATCH 1/2] MDL-63937 autocomplete: Add a loading icon when fetching via AJAX --- lib/amd/src/form-autocomplete.js | 5 +- lib/amd/src/loadingicon.js | 110 +++++++++++++++++++++++++++++ theme/boost/scss/moodle/forms.scss | 7 ++ theme/boost/style/moodle.css | 5 ++ 4 files changed, 126 insertions(+), 1 deletion(-) create mode 100644 lib/amd/src/loadingicon.js 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; } From efef2efdd6beed7ba7bfe0c037c0d427fbe5cc7e Mon Sep 17 00:00:00 2001 From: Huong Nguyen Date: Wed, 20 Feb 2019 15:13:40 +0700 Subject: [PATCH 2/2] MDL-63937 autocomplete: Add the indicator when processing the request --- lib/amd/build/form-autocomplete.min.js | 2 +- lib/amd/build/loadingicon.min.js | 1 + lib/amd/src/form-autocomplete.js | 5 ++++- lib/amd/src/loadingicon.js | 16 ++++++++-------- 4 files changed, 14 insertions(+), 10 deletions(-) create mode 100644 lib/amd/build/loadingicon.min.js diff --git a/lib/amd/build/form-autocomplete.min.js b/lib/amd/build/form-autocomplete.min.js index 830d6fbc362..ccd55f0b881 100644 --- a/lib/amd/build/form-autocomplete.min.js +++ b/lib/amd/build/form-autocomplete.min.js @@ -1 +1 @@ -define(["jquery","core/log","core/str","core/templates","core/notification"],function(a,b,c,d,e){var f={DOWN:40,ENTER:13,SPACE:32,ESCAPE:27,COMMA:44,UP:38},g=a.now(),h=function(b,c){var d=a(document.getElementById(c.selectionId)),e=d.children("[aria-selected=true]").length;for(b%=e;b<0;)b+=e;var f=a(d.children("[aria-selected=true]").get(b)),g=c.selectionId+"-"+b;return d.children().attr("data-active-selection",!1).attr("id",""),f.attr("data-active-selection",!0).attr("id",g),d.attr("aria-activedescendant",g),a.Deferred().resolve()},i=function(b,c,f){var g="form-autocomplete-updateSelectionList-"+c.inputId;M.util.js_pending(g);var i=[],j=a(document.getElementById(c.selectionId)),k=j.attr("aria-activedescendant"),l=!1;k&&(l=a(document.getElementById(k)).attr("data-value")),f.children("option").each(function(b,c){if(a(c).prop("selected")){var d;d=a(c).data("html")?a(c).data("html"):a(c).html(),i.push({label:d,value:a(c).attr("value")})}});var m=a.extend({items:i},b,c);return d.render("core/form_autocomplete_selection",m).then(function(b,e){return d.replaceNodeContents(j,b,e),l!==!1&&j.children("[aria-selected=true]").each(function(b,d){a(d).attr("data-value")===l&&h(b,c)}),l}).then(function(){return M.util.js_complete(g)})["catch"](e.exception)},j=function(a){"undefined"!=typeof M.core_formchangechecker&&M.core_formchangechecker.set_form_changed(),a.change()},k=function(b,c,d,e){var f=a(d).attr("data-value");return b.multiple&&e.children("option").each(function(b,c){a(c).attr("value")==f&&(a(c).prop("selected",!1),a(c).attr("data-iscustom")&&a(c).remove())}),i(b,c,e).then(function(){j(e)})},l=function(b,c){var d=a(document.getElementById(c.inputId)),e=a(document.getElementById(c.suggestionsId)),f=e.children("[aria-hidden=false]").length;for(b%=f;b<0;)b+=f;var g=a(e.children("[aria-hidden=false]").get(b)),h=a(e.children("[role=option]")).index(g),i=c.suggestionsId+"-"+h;e.children().attr("aria-selected",!1).attr("id",""),g.attr("aria-selected",!0).attr("id",i),d.attr("aria-activedescendant",i);var j=g.offset().top-e.offset().top+e.scrollTop()-e.height()/2;return e.animate({scrollTop:j},100).promise()},m=function(b){var c=a(document.getElementById(b.suggestionsId)),d=c.children("[aria-selected=true]"),e=c.children("[aria-hidden=false]").index(d);return l(e+1,b)},n=function(b){var c=a(document.getElementById(b.selectionId)),d=c.children("[data-active-selection=true]");if(!d)return h(0,b);var e=c.children("[aria-selected=true]").index(d);return h(e-1,b)},o=function(b){var c=a(document.getElementById(b.selectionId)),d=c.children("[data-active-selection=true]"),e=0;return d?(e=c.children("[aria-selected=true]").index(d),e+=1):e=0,h(e,b)},p=function(b){var c=a(document.getElementById(b.suggestionsId)),d=c.children("[aria-selected=true]"),e=c.children("[aria-hidden=false]").index(d);return l(e-1,b)},q=function(b){var c=a(document.getElementById(b.inputId)),d=a(document.getElementById(b.suggestionsId));return c.attr("aria-expanded",!1).attr("aria-activedescendant",b.selectionId),d.hide().attr("aria-hidden",!0),a.Deferred().resolve()},r=function(b,f,g,h){var i="form-autocomplete-updateSuggestions-"+f.inputId;M.util.js_pending(i);var j=a(document.getElementById(f.inputId)),k=a(document.getElementById(f.suggestionsId)),m=!1,n=[];h.children("option").each(function(b,c){a(c).prop("selected")!==!0&&(n[n.length]={label:c.innerHTML,value:a(c).attr("value")})});var o=f.caseSensitive?g:g.toLocaleLowerCase(),p=a.extend({options:n},b,f),q=d.render("core/form_autocomplete_suggestions",p).then(function(e,g){return d.replaceNode(k,e,g),k=a(document.getElementById(f.suggestionsId)),k.show().attr("aria-hidden",!1),k.children().each(function(c,d){d=a(d),b.caseSensitive&&d.text().indexOf(o)>-1||!b.caseSensitive&&d.text().toLocaleLowerCase().indexOf(o)>-1?(d.show().attr("aria-hidden",!1),m=!0):d.hide().attr("aria-hidden",!0)}),j.attr("aria-expanded",!0),h.attr("data-notice")?k.html(h.attr("data-notice")):m?b.tags||l(0,f):c.get_string("nosuggestions","form").done(function(a){k.html(a)}),k}).then(function(){return M.util.js_complete(i)})["catch"](e.exception);return q},s=function(b,c,d){var e=a(document.getElementById(c.inputId)),f=e.val(),g=f.split(","),h=!1;return a.each(g,function(c,e){if(e=e.trim(),""!==e&&(b.multiple||d.children("option").prop("selected",!1),d.children("option").each(function(b,c){a(c).attr("value")==e&&(h=!0,a(c).prop("selected",!0))}),!h)){var f=a("