diff --git a/admin/tool/usertours/amd/build/filter_cssselector.min.js b/admin/tool/usertours/amd/build/filter_cssselector.min.js
new file mode 100644
index 00000000000..6dbb873f9c9
--- /dev/null
+++ b/admin/tool/usertours/amd/build/filter_cssselector.min.js
@@ -0,0 +1,2 @@
+define ("tool_usertours/filter_cssselector",["exports"],function(a){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.filterMatches=void 0;a.filterMatches=function filterMatches(a){var b=a.filtervalues.cssselector;if(b[0]){return!!document.querySelector(b[0])}return!0}});
+//# sourceMappingURL=filter_cssselector.min.js.map
diff --git a/admin/tool/usertours/amd/build/filter_cssselector.min.js.map b/admin/tool/usertours/amd/build/filter_cssselector.min.js.map
new file mode 100644
index 00000000000..9ea27cad215
--- /dev/null
+++ b/admin/tool/usertours/amd/build/filter_cssselector.min.js.map
@@ -0,0 +1 @@
+{"version":3,"sources":["../src/filter_cssselector.js"],"names":["filterMatches","tourConfig","filterValues","filtervalues","cssselector","document","querySelector"],"mappings":"yKA+B6B,QAAhBA,CAAAA,aAAgB,CAASC,CAAT,CAAqB,CAC9C,GAAIC,CAAAA,CAAY,CAAGD,CAAU,CAACE,YAAX,CAAwBC,WAA3C,CACA,GAAIF,CAAY,CAAC,CAAD,CAAhB,CAAqB,CACjB,MAAO,CAAC,CAACG,QAAQ,CAACC,aAAT,CAAuBJ,CAAY,CAAC,CAAD,CAAnC,CACZ,CAED,QACH,C","sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * CSS selector client side filter.\n *\n * @module tool_usertours/filter_cssselector\n * @class filter_cssselector\n * @package tool_usertours\n * @copyright 2020 The Open University\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n/**\n * Checks whether the configured CSS selector exists on this page.\n *\n * @param {array} tourConfig The tour configuration.\n * @returns {boolean}\n */\nexport const filterMatches = function(tourConfig) {\n let filterValues = tourConfig.filtervalues.cssselector;\n if (filterValues[0]) {\n return !!document.querySelector(filterValues[0]);\n }\n // If there is no CSS selector configured, this page matches.\n return true;\n};\n"],"file":"filter_cssselector.min.js"}
\ No newline at end of file
diff --git a/admin/tool/usertours/amd/build/usertours.min.js b/admin/tool/usertours/amd/build/usertours.min.js
index 6b16a51a322..8ebe59f57dd 100644
--- a/admin/tool/usertours/amd/build/usertours.min.js
+++ b/admin/tool/usertours/amd/build/usertours.min.js
@@ -1,2 +1,2 @@
-define ("tool_usertours/usertours",["core/ajax","tool_usertours/tour","jquery","core/templates","core/str","core/log","core/notification"],function(a,b,c,d,e,f,g){var h={tourId:null,currentTour:null,context:null,init:function init(a,b,d){h.tourId=a;h.context=d;if("undefined"==typeof b){b=!0}if(b){h.fetchTour(a)}h.addResetLink();c("body").on("click","[data-action=\"tool_usertours/resetpagetour\"]",function(a){a.preventDefault();h.resetTourState(h.tourId)})},fetchTour:function fetchTour(b){M.util.js_pending("admin_usertour_fetchTour"+b);c.when(a.call([{methodname:"tool_usertours_fetch_and_start_tour",args:{tourid:b,context:h.context,pageurl:window.location.href}}])[0],d.render("tool_usertours/tourstep",{})).then(function(a,c){if(!a.hasOwnProperty("tourconfig")){return}return h.startBootstrapTour(b,c[0],a.tourconfig)}).always(function(){M.util.js_complete("admin_usertour_fetchTour"+b)}).fail(g.exception)},addResetLink:function addResetLink(){var a;M.util.js_pending("admin_usertour_addResetLink");if(c(".tool_usertours-resettourcontainer").length){a=c(".tool_usertours-resettourcontainer")}else if(c(".logininfo").length){a=c(".logininfo")}else if(c("footer").length){a=c("footer")}else{a=c("body")}d.render("tool_usertours/resettour",{}).then(function(b,c){d.appendNodeContents(a,b,c)}).always(function(){M.util.js_complete("admin_usertour_addResetLink")}).fail()},startBootstrapTour:function startBootstrapTour(a,c,d){if(h.currentTour){d.onEnd=null;h.currentTour.endTour();delete h.currentTour}d.eventHandlers={afterEnd:[h.markTourComplete],afterRender:[h.markStepShown]};d.tourName=d.name;delete d.name;d.template=c;d.steps=d.steps.map(function(a){if("undefined"!=typeof a.element){a.target=a.element;delete a.element}if("undefined"!=typeof a.reflex){a.moveOnClick=!!a.reflex;delete a.reflex}if("undefined"!=typeof a.content){a.body=a.content;delete a.content}return a});h.currentTour=new b(d);return h.currentTour.startTour()},markStepShown:function markStepShown(){var b=this.getStepConfig(this.getCurrentStepNumber());c.when(a.call([{methodname:"tool_usertours_step_shown",args:{tourid:h.tourId,context:h.context,pageurl:window.location.href,stepid:b.stepid,stepindex:this.getCurrentStepNumber()}}])[0]).fail(f.error)},markTourComplete:function markTourComplete(){var b=this.getStepConfig(this.getCurrentStepNumber());c.when(a.call([{methodname:"tool_usertours_complete_tour",args:{tourid:h.tourId,context:h.context,pageurl:window.location.href,stepid:b.stepid,stepindex:this.getCurrentStepNumber()}}])[0]).fail(f.error)},resetTourState:function resetTourState(b){c.when(a.call([{methodname:"tool_usertours_reset_tour",args:{tourid:b,context:h.context,pageurl:window.location.href}}])[0]).then(function(a){if(a.startTour){h.fetchTour(a.startTour)}}).fail(g.exception)}};return{init:h.init,resetTourState:h.resetTourState}});
+define ("tool_usertours/usertours",["core/ajax","tool_usertours/tour","jquery","core/templates","core/str","core/log","core/notification"],function(a,b,c,d,e,f,g){var h={tourId:null,currentTour:null,init:function init(a,b){for(var d=[],e=0;e\n */\ndefine(\n['core/ajax', 'tool_usertours/tour', 'jquery', 'core/templates', 'core/str', 'core/log', 'core/notification'],\nfunction(ajax, BootstrapTour, $, templates, str, log, notification) {\n var usertours = {\n tourId: null,\n\n currentTour: null,\n\n context: null,\n\n /**\n * Initialise the user tour for the current page.\n *\n * @method init\n * @param {Number} tourId The ID of the tour to start.\n * @param {Bool} startTour Attempt to start the tour now.\n * @param {Number} context The context of the current page.\n */\n init: function(tourId, startTour, context) {\n // Only one tour per page is allowed.\n usertours.tourId = tourId;\n\n usertours.context = context;\n\n if (typeof startTour === 'undefined') {\n startTour = true;\n }\n\n if (startTour) {\n // Fetch the tour configuration.\n usertours.fetchTour(tourId);\n }\n\n usertours.addResetLink();\n // Watch for the reset link.\n $('body').on('click', '[data-action=\"tool_usertours/resetpagetour\"]', function(e) {\n e.preventDefault();\n usertours.resetTourState(usertours.tourId);\n });\n },\n\n /**\n * Fetch the configuration specified tour, and start the tour when it has been fetched.\n *\n * @method fetchTour\n * @param {Number} tourId The ID of the tour to start.\n */\n fetchTour: function(tourId) {\n M.util.js_pending('admin_usertour_fetchTour' + tourId);\n $.when(\n ajax.call([\n {\n methodname: 'tool_usertours_fetch_and_start_tour',\n args: {\n tourid: tourId,\n context: usertours.context,\n pageurl: window.location.href,\n }\n }\n ])[0],\n templates.render('tool_usertours/tourstep', {})\n )\n .then(function(response, template) {\n // If we don't have any tour config (because it doesn't need showing for the current user), return early.\n if (!response.hasOwnProperty('tourconfig')) {\n return;\n }\n\n return usertours.startBootstrapTour(tourId, template[0], response.tourconfig);\n })\n .always(function() {\n M.util.js_complete('admin_usertour_fetchTour' + tourId);\n\n return;\n })\n .fail(notification.exception);\n },\n\n /**\n * Add a reset link to the page.\n *\n * @method addResetLink\n */\n addResetLink: function() {\n var ele;\n M.util.js_pending('admin_usertour_addResetLink');\n\n // Append the link to the most suitable place on the page\n // with fallback to legacy selectors and finally the body\n // if there is no better place.\n if ($('.tool_usertours-resettourcontainer').length) {\n ele = $('.tool_usertours-resettourcontainer');\n } else if ($('.logininfo').length) {\n ele = $('.logininfo');\n } else if ($('footer').length) {\n ele = $('footer');\n } else {\n ele = $('body');\n }\n templates.render('tool_usertours/resettour', {})\n .then(function(html, js) {\n templates.appendNodeContents(ele, html, js);\n\n return;\n })\n .always(function() {\n M.util.js_complete('admin_usertour_addResetLink');\n\n return;\n })\n .fail();\n },\n\n /**\n * Start the specified tour.\n *\n * @method startBootstrapTour\n * @param {Number} tourId The ID of the tour to start.\n * @param {String} template The template to use.\n * @param {Object} tourConfig The tour configuration.\n * @return {Object}\n */\n startBootstrapTour: function(tourId, template, tourConfig) {\n if (usertours.currentTour) {\n // End the current tour, but disable end tour handler.\n tourConfig.onEnd = null;\n usertours.currentTour.endTour();\n delete usertours.currentTour;\n }\n\n // Normalize for the new library.\n tourConfig.eventHandlers = {\n afterEnd: [usertours.markTourComplete],\n afterRender: [usertours.markStepShown],\n };\n\n // Sort out the tour name.\n tourConfig.tourName = tourConfig.name;\n delete tourConfig.name;\n\n // Add the template to the configuration.\n // This enables translations of the buttons.\n tourConfig.template = template;\n\n tourConfig.steps = tourConfig.steps.map(function(step) {\n if (typeof step.element !== 'undefined') {\n step.target = step.element;\n delete step.element;\n }\n\n if (typeof step.reflex !== 'undefined') {\n step.moveOnClick = !!step.reflex;\n delete step.reflex;\n }\n\n if (typeof step.content !== 'undefined') {\n step.body = step.content;\n delete step.content;\n }\n\n return step;\n });\n\n usertours.currentTour = new BootstrapTour(tourConfig);\n return usertours.currentTour.startTour();\n },\n\n /**\n * Mark the specified step as being shownd by the user.\n *\n * @method markStepShown\n */\n markStepShown: function() {\n var stepConfig = this.getStepConfig(this.getCurrentStepNumber());\n $.when(\n ajax.call([\n {\n methodname: 'tool_usertours_step_shown',\n args: {\n tourid: usertours.tourId,\n context: usertours.context,\n pageurl: window.location.href,\n stepid: stepConfig.stepid,\n stepindex: this.getCurrentStepNumber(),\n }\n }\n ])[0]\n ).fail(log.error);\n },\n\n /**\n * Mark the specified tour as being completed by the user.\n *\n * @method markTourComplete\n */\n markTourComplete: function() {\n var stepConfig = this.getStepConfig(this.getCurrentStepNumber());\n $.when(\n ajax.call([\n {\n methodname: 'tool_usertours_complete_tour',\n args: {\n tourid: usertours.tourId,\n context: usertours.context,\n pageurl: window.location.href,\n stepid: stepConfig.stepid,\n stepindex: this.getCurrentStepNumber(),\n }\n }\n ])[0]\n ).fail(log.error);\n },\n\n /**\n * Reset the state, and restart the the tour on the current page.\n *\n * @method resetTourState\n * @param {Number} tourId The ID of the tour to start.\n */\n resetTourState: function(tourId) {\n $.when(\n ajax.call([\n {\n methodname: 'tool_usertours_reset_tour',\n args: {\n tourid: tourId,\n context: usertours.context,\n pageurl: window.location.href,\n }\n }\n ])[0]\n ).then(function(response) {\n if (response.startTour) {\n usertours.fetchTour(response.startTour);\n }\n return;\n }).fail(notification.exception);\n }\n };\n\n return /** @alias module:tool_usertours/usertours */ {\n /**\n * Initialise the user tour for the current page.\n *\n * @method init\n * @param {Number} tourId The ID of the tour to start.\n * @param {Bool} startTour Attempt to start the tour now.\n */\n init: usertours.init,\n\n /**\n * Reset the state, and restart the the tour on the current page.\n *\n * @method resetTourState\n * @param {Number} tourId The ID of the tour to restart.\n */\n resetTourState: usertours.resetTourState\n };\n});\n"],"file":"usertours.min.js"}
\ No newline at end of file
+{"version":3,"sources":["../src/usertours.js"],"names":["define","ajax","BootstrapTour","$","templates","str","log","notification","usertours","tourId","currentTour","init","tourDetails","filters","requirements","req","length","require","matchingTour","key","tour","i","filter","arguments","filterMatches","startTour","fetchTour","addResetLink","on","e","preventDefault","resetTourState","M","util","js_pending","when","call","methodname","args","tourid","context","cfg","contextid","pageurl","window","location","href","render","then","response","template","hasOwnProperty","startBootstrapTour","tourconfig","always","js_complete","fail","exception","ele","html","js","appendNodeContents","tourConfig","onEnd","endTour","eventHandlers","afterEnd","markTourComplete","afterRender","markStepShown","tourName","name","steps","map","step","element","target","reflex","moveOnClick","content","body","stepConfig","getStepConfig","getCurrentStepNumber","stepid","stepindex","error"],"mappings":"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","sourcesContent":["/**\n * User tour control library.\n *\n * @module tool_usertours/usertours\n * @class usertours\n * @package tool_usertours\n * @copyright 2016 Andrew Nicols \n */\ndefine(\n['core/ajax', 'tool_usertours/tour', 'jquery', 'core/templates', 'core/str', 'core/log', 'core/notification'],\nfunction(ajax, BootstrapTour, $, templates, str, log, notification) {\n var usertours = {\n tourId: null,\n\n currentTour: null,\n\n /**\n * Initialise the user tour for the current page.\n *\n * @method init\n * @param {Array} tourDetails The matching tours for this page.\n * @param {Array} filters The names of all client side filters.\n */\n init: function(tourDetails, filters) {\n let requirements = [];\n for (var req = 0; req < filters.length; req++) {\n requirements[req] = 'tool_usertours/filter_' + filters[req];\n }\n require(requirements, function() {\n // Run the client side filters to find the first matching tour.\n let matchingTour = null;\n for (let key in tourDetails) {\n let tour = tourDetails[key];\n for (let i = 0; i < filters.length; i++) {\n let filter = arguments[i];\n if (filter.filterMatches(tour)) {\n matchingTour = tour;\n } else {\n // If any filter doesn't match, move on to the next tour.\n matchingTour = null;\n break;\n }\n }\n // If all filters matched then use this tour.\n if (matchingTour) {\n break;\n }\n }\n\n if (matchingTour === null) {\n return;\n }\n\n // Only one tour per page is allowed.\n usertours.tourId = matchingTour.tourId;\n\n let startTour = matchingTour.startTour;\n if (typeof startTour === 'undefined') {\n startTour = true;\n }\n\n if (startTour) {\n // Fetch the tour configuration.\n usertours.fetchTour(usertours.tourId);\n }\n\n usertours.addResetLink();\n // Watch for the reset link.\n $('body').on('click', '[data-action=\"tool_usertours/resetpagetour\"]', function(e) {\n e.preventDefault();\n usertours.resetTourState(usertours.tourId);\n });\n });\n },\n\n /**\n * Fetch the configuration specified tour, and start the tour when it has been fetched.\n *\n * @method fetchTour\n * @param {Number} tourId The ID of the tour to start.\n */\n fetchTour: function(tourId) {\n M.util.js_pending('admin_usertour_fetchTour' + tourId);\n $.when(\n ajax.call([\n {\n methodname: 'tool_usertours_fetch_and_start_tour',\n args: {\n tourid: tourId,\n context: M.cfg.contextid,\n pageurl: window.location.href,\n }\n }\n ])[0],\n templates.render('tool_usertours/tourstep', {})\n )\n .then(function(response, template) {\n // If we don't have any tour config (because it doesn't need showing for the current user), return early.\n if (!response.hasOwnProperty('tourconfig')) {\n return;\n }\n\n return usertours.startBootstrapTour(tourId, template[0], response.tourconfig);\n })\n .always(function() {\n M.util.js_complete('admin_usertour_fetchTour' + tourId);\n\n return;\n })\n .fail(notification.exception);\n },\n\n /**\n * Add a reset link to the page.\n *\n * @method addResetLink\n */\n addResetLink: function() {\n var ele;\n M.util.js_pending('admin_usertour_addResetLink');\n\n // Append the link to the most suitable place on the page\n // with fallback to legacy selectors and finally the body\n // if there is no better place.\n if ($('.tool_usertours-resettourcontainer').length) {\n ele = $('.tool_usertours-resettourcontainer');\n } else if ($('.logininfo').length) {\n ele = $('.logininfo');\n } else if ($('footer').length) {\n ele = $('footer');\n } else {\n ele = $('body');\n }\n templates.render('tool_usertours/resettour', {})\n .then(function(html, js) {\n templates.appendNodeContents(ele, html, js);\n\n return;\n })\n .always(function() {\n M.util.js_complete('admin_usertour_addResetLink');\n\n return;\n })\n .fail();\n },\n\n /**\n * Start the specified tour.\n *\n * @method startBootstrapTour\n * @param {Number} tourId The ID of the tour to start.\n * @param {String} template The template to use.\n * @param {Object} tourConfig The tour configuration.\n * @return {Object}\n */\n startBootstrapTour: function(tourId, template, tourConfig) {\n if (usertours.currentTour) {\n // End the current tour, but disable end tour handler.\n tourConfig.onEnd = null;\n usertours.currentTour.endTour();\n delete usertours.currentTour;\n }\n\n // Normalize for the new library.\n tourConfig.eventHandlers = {\n afterEnd: [usertours.markTourComplete],\n afterRender: [usertours.markStepShown],\n };\n\n // Sort out the tour name.\n tourConfig.tourName = tourConfig.name;\n delete tourConfig.name;\n\n // Add the template to the configuration.\n // This enables translations of the buttons.\n tourConfig.template = template;\n\n tourConfig.steps = tourConfig.steps.map(function(step) {\n if (typeof step.element !== 'undefined') {\n step.target = step.element;\n delete step.element;\n }\n\n if (typeof step.reflex !== 'undefined') {\n step.moveOnClick = !!step.reflex;\n delete step.reflex;\n }\n\n if (typeof step.content !== 'undefined') {\n step.body = step.content;\n delete step.content;\n }\n\n return step;\n });\n\n usertours.currentTour = new BootstrapTour(tourConfig);\n return usertours.currentTour.startTour();\n },\n\n /**\n * Mark the specified step as being shownd by the user.\n *\n * @method markStepShown\n */\n markStepShown: function() {\n var stepConfig = this.getStepConfig(this.getCurrentStepNumber());\n $.when(\n ajax.call([\n {\n methodname: 'tool_usertours_step_shown',\n args: {\n tourid: usertours.tourId,\n context: M.cfg.contextid,\n pageurl: window.location.href,\n stepid: stepConfig.stepid,\n stepindex: this.getCurrentStepNumber(),\n }\n }\n ])[0]\n ).fail(log.error);\n },\n\n /**\n * Mark the specified tour as being completed by the user.\n *\n * @method markTourComplete\n */\n markTourComplete: function() {\n var stepConfig = this.getStepConfig(this.getCurrentStepNumber());\n $.when(\n ajax.call([\n {\n methodname: 'tool_usertours_complete_tour',\n args: {\n tourid: usertours.tourId,\n context: M.cfg.contextid,\n pageurl: window.location.href,\n stepid: stepConfig.stepid,\n stepindex: this.getCurrentStepNumber(),\n }\n }\n ])[0]\n ).fail(log.error);\n },\n\n /**\n * Reset the state, and restart the the tour on the current page.\n *\n * @method resetTourState\n * @param {Number} tourId The ID of the tour to start.\n */\n resetTourState: function(tourId) {\n $.when(\n ajax.call([\n {\n methodname: 'tool_usertours_reset_tour',\n args: {\n tourid: tourId,\n context: M.cfg.contextid,\n pageurl: window.location.href,\n }\n }\n ])[0]\n ).then(function(response) {\n if (response.startTour) {\n usertours.fetchTour(response.startTour);\n }\n return;\n }).fail(notification.exception);\n }\n };\n\n return /** @alias module:tool_usertours/usertours */ {\n /**\n * Initialise the user tour for the current page.\n *\n * @method init\n * @param {Number} tourId The ID of the tour to start.\n * @param {Bool} startTour Attempt to start the tour now.\n */\n init: usertours.init,\n\n /**\n * Reset the state, and restart the the tour on the current page.\n *\n * @method resetTourState\n * @param {Number} tourId The ID of the tour to restart.\n */\n resetTourState: usertours.resetTourState\n };\n});\n"],"file":"usertours.min.js"}
\ No newline at end of file
diff --git a/admin/tool/usertours/amd/src/filter_cssselector.js b/admin/tool/usertours/amd/src/filter_cssselector.js
new file mode 100644
index 00000000000..06e825c2655
--- /dev/null
+++ b/admin/tool/usertours/amd/src/filter_cssselector.js
@@ -0,0 +1,39 @@
+// 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 .
+
+/**
+ * CSS selector client side filter.
+ *
+ * @module tool_usertours/filter_cssselector
+ * @class filter_cssselector
+ * @package tool_usertours
+ * @copyright 2020 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+/**
+ * Checks whether the configured CSS selector exists on this page.
+ *
+ * @param {array} tourConfig The tour configuration.
+ * @returns {boolean}
+ */
+export const filterMatches = function(tourConfig) {
+ let filterValues = tourConfig.filtervalues.cssselector;
+ if (filterValues[0]) {
+ return !!document.querySelector(filterValues[0]);
+ }
+ // If there is no CSS selector configured, this page matches.
+ return true;
+};
diff --git a/admin/tool/usertours/amd/src/usertours.js b/admin/tool/usertours/amd/src/usertours.js
index 4bb10506169..a79f7f23651 100644
--- a/admin/tool/usertours/amd/src/usertours.js
+++ b/admin/tool/usertours/amd/src/usertours.js
@@ -14,36 +14,62 @@ function(ajax, BootstrapTour, $, templates, str, log, notification) {
currentTour: null,
- context: null,
-
/**
* Initialise the user tour for the current page.
*
* @method init
- * @param {Number} tourId The ID of the tour to start.
- * @param {Bool} startTour Attempt to start the tour now.
- * @param {Number} context The context of the current page.
+ * @param {Array} tourDetails The matching tours for this page.
+ * @param {Array} filters The names of all client side filters.
*/
- init: function(tourId, startTour, context) {
- // Only one tour per page is allowed.
- usertours.tourId = tourId;
-
- usertours.context = context;
-
- if (typeof startTour === 'undefined') {
- startTour = true;
+ init: function(tourDetails, filters) {
+ let requirements = [];
+ for (var req = 0; req < filters.length; req++) {
+ requirements[req] = 'tool_usertours/filter_' + filters[req];
}
+ require(requirements, function() {
+ // Run the client side filters to find the first matching tour.
+ let matchingTour = null;
+ for (let key in tourDetails) {
+ let tour = tourDetails[key];
+ for (let i = 0; i < filters.length; i++) {
+ let filter = arguments[i];
+ if (filter.filterMatches(tour)) {
+ matchingTour = tour;
+ } else {
+ // If any filter doesn't match, move on to the next tour.
+ matchingTour = null;
+ break;
+ }
+ }
+ // If all filters matched then use this tour.
+ if (matchingTour) {
+ break;
+ }
+ }
- if (startTour) {
- // Fetch the tour configuration.
- usertours.fetchTour(tourId);
- }
+ if (matchingTour === null) {
+ return;
+ }
- usertours.addResetLink();
- // Watch for the reset link.
- $('body').on('click', '[data-action="tool_usertours/resetpagetour"]', function(e) {
- e.preventDefault();
- usertours.resetTourState(usertours.tourId);
+ // Only one tour per page is allowed.
+ usertours.tourId = matchingTour.tourId;
+
+ let startTour = matchingTour.startTour;
+ if (typeof startTour === 'undefined') {
+ startTour = true;
+ }
+
+ if (startTour) {
+ // Fetch the tour configuration.
+ usertours.fetchTour(usertours.tourId);
+ }
+
+ usertours.addResetLink();
+ // Watch for the reset link.
+ $('body').on('click', '[data-action="tool_usertours/resetpagetour"]', function(e) {
+ e.preventDefault();
+ usertours.resetTourState(usertours.tourId);
+ });
});
},
@@ -61,7 +87,7 @@ function(ajax, BootstrapTour, $, templates, str, log, notification) {
methodname: 'tool_usertours_fetch_and_start_tour',
args: {
tourid: tourId,
- context: usertours.context,
+ context: M.cfg.contextid,
pageurl: window.location.href,
}
}
@@ -186,7 +212,7 @@ function(ajax, BootstrapTour, $, templates, str, log, notification) {
methodname: 'tool_usertours_step_shown',
args: {
tourid: usertours.tourId,
- context: usertours.context,
+ context: M.cfg.contextid,
pageurl: window.location.href,
stepid: stepConfig.stepid,
stepindex: this.getCurrentStepNumber(),
@@ -209,7 +235,7 @@ function(ajax, BootstrapTour, $, templates, str, log, notification) {
methodname: 'tool_usertours_complete_tour',
args: {
tourid: usertours.tourId,
- context: usertours.context,
+ context: M.cfg.contextid,
pageurl: window.location.href,
stepid: stepConfig.stepid,
stepindex: this.getCurrentStepNumber(),
@@ -232,7 +258,7 @@ function(ajax, BootstrapTour, $, templates, str, log, notification) {
methodname: 'tool_usertours_reset_tour',
args: {
tourid: tourId,
- context: usertours.context,
+ context: M.cfg.contextid,
pageurl: window.location.href,
}
}
diff --git a/admin/tool/usertours/classes/external/tour.php b/admin/tool/usertours/classes/external/tour.php
index c16c5a015ee..7ec9cb8e40f 100644
--- a/admin/tool/usertours/classes/external/tour.php
+++ b/admin/tool/usertours/classes/external/tour.php
@@ -131,8 +131,9 @@ class tour extends external_api {
$result = [];
- if ($tourinstance = \tool_usertours\manager::get_matching_tours(new \moodle_url($params['pageurl']))) {
- if ($tour->get_id() === $tourinstance->get_id()) {
+ $matchingtours = \tool_usertours\manager::get_matching_tours(new \moodle_url($params['pageurl']));
+ foreach ($matchingtours as $match) {
+ if ($tour->get_id() === $match->get_id()) {
$result['startTour'] = $tour->get_id();
\tool_usertours\event\tour_reset::create([
@@ -142,7 +143,7 @@ class tour extends external_api {
'pageurl' => $params['pageurl'],
],
])->trigger();
-
+ break;
}
}
diff --git a/admin/tool/usertours/classes/helper.php b/admin/tool/usertours/classes/helper.php
index df04ed926ee..f1f8e4b716e 100644
--- a/admin/tool/usertours/classes/helper.php
+++ b/admin/tool/usertours/classes/helper.php
@@ -24,6 +24,8 @@
namespace tool_usertours;
+use tool_usertours\local\clientside_filter\clientside_filter;
+
defined('MOODLE_INTERNAL') || die();
/**
@@ -523,23 +525,28 @@ class helper {
}
self::$bootstrapped = true;
- if ($tour = manager::get_current_tour()) {
- $PAGE->requires->js_call_amd('tool_usertours/usertours', 'init', [
- $tour->get_id(),
- $tour->should_show_for_user(),
- $PAGE->context->id,
- ]);
- }
- }
+ $tours = manager::get_current_tours();
- /**
- * Add the reset link to the current page.
- */
- public static function bootstrap_reset() {
- if (manager::get_current_tour()) {
- echo \html_writer::link('', get_string('resettouronpage', 'tool_usertours'), [
- 'data-action' => 'tool_usertours/resetpagetour',
- ]);
+ if ($tours) {
+ $filters = static::get_all_clientside_filters();
+
+ $tourdetails = array_map(function($tour) use ($filters) {
+ return [
+ 'tourId' => $tour->get_id(),
+ 'startTour' => $tour->should_show_for_user(),
+ 'filtervalues' => $tour->get_client_filter_values($filters),
+ ];
+ }, $tours);
+
+ $filternames = [];
+ foreach ($filters as $filter) {
+ $filternames[] = $filter::get_filter_name();
+ }
+
+ $PAGE->requires->js_call_amd('tool_usertours/usertours', 'init', [
+ $tourdetails,
+ $filternames,
+ ]);
}
}
@@ -557,6 +564,25 @@ class helper {
return $rc->isInstantiable();
});
+ $filters = array_merge($filters, static::get_all_clientside_filters());
+
+ return $filters;
+ }
+
+ /**
+ * Get a list of all clientside filters.
+ *
+ * @return array
+ */
+ public static function get_all_clientside_filters() {
+ $filters = \core_component::get_component_classes_in_namespace('tool_usertours', 'local\clientside_filter');
+ $filters = array_keys($filters);
+
+ $filters = array_filter($filters, function($filterclass) {
+ $rc = new \ReflectionClass($filterclass);
+ return $rc->isInstantiable();
+ });
+
return $filters;
}
}
diff --git a/admin/tool/usertours/classes/local/clientside_filter/clientside_filter.php b/admin/tool/usertours/classes/local/clientside_filter/clientside_filter.php
new file mode 100644
index 00000000000..6fa403ac57a
--- /dev/null
+++ b/admin/tool/usertours/classes/local/clientside_filter/clientside_filter.php
@@ -0,0 +1,55 @@
+.
+
+/**
+ * Clientside filter base.
+ *
+ * @package tool_usertours
+ * @copyright 2020 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace tool_usertours\local\clientside_filter;
+
+defined('MOODLE_INTERNAL') || die();
+
+use stdClass;
+use tool_usertours\local\filter\base;
+use tool_usertours\tour;
+
+/**
+ * Clientside filter base.
+ *
+ * @copyright 2020 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+abstract class clientside_filter extends base {
+ /**
+ * Returns the filter values needed for client side filtering.
+ *
+ * @param tour $tour The tour to find the filter values for
+ * @return stdClass
+ */
+ public static function get_client_side_values(tour $tour): stdClass {
+ $data = (object) [];
+
+ if (is_a(static::class, clientside_filter::class, true)) {
+ $data->filterdata = $tour->get_filter_values(static::get_filter_name());
+ }
+
+ return $data;
+ }
+}
\ No newline at end of file
diff --git a/admin/tool/usertours/classes/local/clientside_filter/cssselector.php b/admin/tool/usertours/classes/local/clientside_filter/cssselector.php
new file mode 100644
index 00000000000..e6d6c2d778e
--- /dev/null
+++ b/admin/tool/usertours/classes/local/clientside_filter/cssselector.php
@@ -0,0 +1,115 @@
+.
+
+/**
+ * Selector filter.
+ *
+ * @package tool_usertours
+ * @copyright 2020 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace tool_usertours\local\clientside_filter;
+
+use stdClass;
+use tool_usertours\tour;
+
+/**
+ * Course filter.
+ *
+ * @copyright 2020 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class cssselector extends clientside_filter {
+ /**
+ * The name of the filter.
+ *
+ * @return string
+ */
+ public static function get_filter_name() {
+ return 'cssselector';
+ }
+
+ /**
+ * Overrides the base add form element with a selector text box.
+ *
+ * @param \MoodleQuickForm $mform
+ */
+ public static function add_filter_to_form(\MoodleQuickForm &$mform) {
+ $filtername = self::get_filter_name();
+ $key = "filter_{$filtername}";
+
+ $mform->addElement('text', $key, get_string($key, 'tool_usertours'));
+ $mform->setType($key, PARAM_RAW);
+ $mform->addHelpButton($key, $key, 'tool_usertours');
+ }
+
+ /**
+ * Prepare the filter values for the form.
+ *
+ * @param tour $tour The tour to prepare values from
+ * @param stdClass $data The data value
+ * @return stdClass
+ */
+ public static function prepare_filter_values_for_form(tour $tour, \stdClass $data) {
+ $filtername = static::get_filter_name();
+
+ $key = "filter_{$filtername}";
+ $values = $tour->get_filter_values($filtername);
+ if (empty($values)) {
+ $values = [""];
+ }
+ $data->$key = $values[0];
+
+ return $data;
+ }
+
+ /**
+ * Save the filter values from the form to the tour.
+ *
+ * @param tour $tour The tour to save values to
+ * @param stdClass $data The data submitted in the form
+ */
+ public static function save_filter_values_from_form(tour $tour, \stdClass $data) {
+ $filtername = static::get_filter_name();
+
+ $key = "filter_{$filtername}";
+
+ $newvalue = [$data->$key];
+ if (empty($data->$key)) {
+ $newvalue = [];
+ }
+
+ $tour->set_filter_values($filtername, $newvalue);
+ }
+
+ /**
+ * Returns the filter values needed for client side filtering.
+ *
+ * @param tour $tour The tour to find the filter values for
+ * @return stdClass
+ */
+ public static function get_client_side_values(tour $tour): stdClass {
+ $filtername = static::get_filter_name();
+ $filtervalues = $tour->get_filter_values($filtername);
+
+ // Filter values might not exist for tours that were created before this filter existed.
+ if (!$filtervalues) {
+ return new stdClass;
+ }
+
+ return (object) $filtervalues;
+ }
+}
diff --git a/admin/tool/usertours/classes/manager.php b/admin/tool/usertours/classes/manager.php
index 6741568b0b5..437b416b2f2 100644
--- a/admin/tool/usertours/classes/manager.php
+++ b/admin/tool/usertours/classes/manager.php
@@ -608,42 +608,44 @@ class manager {
}
/**
- * Get the first tour matching the current page URL.
+ * Get all tours for the current page URL.
*
- * @param bool $reset Forcibly update the current tour
- * @return tour
+ * @param bool $reset Forcibly update the current tours
+ * @return array
*/
- public static function get_current_tour($reset = false) {
+ public static function get_current_tours($reset = false): array {
global $PAGE;
- static $tour = false;
+ static $tours = false;
- if ($tour === false || $reset) {
- $tour = self::get_matching_tours($PAGE->url);
+ if ($tours === false || $reset) {
+ $tours = self::get_matching_tours($PAGE->url);
}
- return $tour;
+ return $tours;
}
/**
- * Get the first tour matching the specified URL.
+ * Get all tours matching the specified URL.
*
* @param moodle_url $pageurl The URL to match.
- * @return tour
+ * @return array
*/
- public static function get_matching_tours(\moodle_url $pageurl) {
+ public static function get_matching_tours(\moodle_url $pageurl): array {
global $PAGE;
$tours = cache::get_matching_tourdata($pageurl);
+ $matches = [];
+ $filters = helper::get_all_filters();
foreach ($tours as $record) {
$tour = tour::load_from_record($record);
- if ($tour->is_enabled() && $tour->matches_all_filters($PAGE->context)) {
- return $tour;
+ if ($tour->is_enabled() && $tour->matches_all_filters($PAGE->context, $filters)) {
+ $matches[] = $tour;
}
}
- return null;
+ return $matches;
}
/**
diff --git a/admin/tool/usertours/classes/tour.php b/admin/tool/usertours/classes/tour.php
index 0765ee35155..08e8279d7d5 100644
--- a/admin/tool/usertours/classes/tour.php
+++ b/admin/tool/usertours/classes/tour.php
@@ -24,6 +24,8 @@
namespace tool_usertours;
+use tool_usertours\local\clientside_filter\clientside_filter;
+
defined('MOODLE_INTERNAL') || die();
/**
@@ -769,11 +771,14 @@ class tour {
/**
* Check whether this tour matches all filters.
*
- * @param context $context The context to check
+ * @param \context $context The context to check.
+ * @param array|null $filters Optional array of filters.
* @return bool
*/
- public function matches_all_filters(\context $context) {
- $filters = helper::get_all_filters();
+ public function matches_all_filters(\context $context, array $filters = null): bool {
+ if (!$filters) {
+ $filters = helper::get_all_filters();
+ }
// All filters must match.
// If any one filter fails to match, we return false.
@@ -785,4 +790,20 @@ class tour {
return true;
}
+
+ /**
+ * Gets all filter values for use in client side filters.
+ *
+ * @param array $filters Array of clientside filters.
+ * @return array
+ */
+ public function get_client_filter_values(array $filters): array {
+ $results = [];
+
+ foreach ($filters as $filter) {
+ $results[$filter::get_filter_name()] = $filter::get_client_side_values($this);
+ }
+
+ return $results;
+ }
}
diff --git a/admin/tool/usertours/lang/en/tool_usertours.php b/admin/tool/usertours/lang/en/tool_usertours.php
index 3d86e56b861..f142b46f304 100644
--- a/admin/tool/usertours/lang/en/tool_usertours.php
+++ b/admin/tool/usertours/lang/en/tool_usertours.php
@@ -63,6 +63,8 @@ $string['filter_course'] = 'Courses';
$string['filter_course_help'] = 'Show the tour on a page that is associated with the selected course.';
$string['filter_courseformat'] = 'Course format';
$string['filter_courseformat_help'] = 'Show the tour on a page that is associated with a course using the selected course format.';
+$string['filter_cssselector'] = 'CSS selector';
+$string['filter_cssselector_help'] = 'Only show the tour when the specified CSS selector is found on the page.';
$string['filter_header'] = 'Tour filters';
$string['filter_help'] = 'Select the conditions under which the tour will be shown. All of the filters must match for a tour to be shown to a user.';
$string['filter_date_account_creation'] = 'User account creation date within';
diff --git a/admin/tool/usertours/tests/behat/tour_filter.feature b/admin/tool/usertours/tests/behat/tour_filter.feature
index ac3a1640d3f..0fe72f41f80 100644
--- a/admin/tool/usertours/tests/behat/tour_filter.feature
+++ b/admin/tool/usertours/tests/behat/tour_filter.feature
@@ -142,3 +142,88 @@ Feature: Apply tour filters to a tour
When I am on "Course 2" course homepage
And I wait until the page is ready
Then I should not see "Welcome to your course tour."
+
+ @javascript
+ Scenario: Add tours with CSS selectors
+ Given the following "users" exist:
+ | username | firstname | lastname | email |
+ | student1 | Student | 1 | student1@example.com |
+ Given the following "courses" exist:
+ | fullname | shortname | format | enablecompletion |
+ | Course 1 | C1 | topics | 1 |
+ | Course 2 | C2 | topics | 1 |
+ And I log in as "admin"
+ And I am on "Course 1" course homepage with editing mode on
+ And I add a "Wiki" to section "1" and I fill the form with:
+ | Wiki name | Test wiki name |
+ | Description | Test wiki description |
+ | First page name | First page |
+ | Wiki mode | Collaborative wiki |
+ And I am on "Course 2" course homepage
+ And I add a "Forum" to section "1" and I fill the form with:
+ | Forum name | Test forum name |
+ | Forum type | Standard forum for general use |
+ | Description | Test forum description |
+ And I add a new user tour with:
+ | Name | Wiki tour |
+ | Description | A tour with both matches |
+ | Apply to URL match | /course/view.php% |
+ | Tour is enabled | 1 |
+ | CSS selector | .modtype_wiki |
+ And I add steps to the "Wiki tour" tour:
+ | targettype | Title | Content |
+ | Display in middle of page | Welcome | Welcome to the Wiki tour |
+ And I add a new user tour with:
+ | Name | Forum tour |
+ | Description | A tour with both matches |
+ | Apply to URL match | /course/view.php% |
+ | Tour is enabled | 1 |
+ | CSS selector | .modtype_forum |
+ And I add steps to the "Forum tour" tour:
+ | targettype | Title | Content |
+ | Display in middle of page | Welcome | Welcome to the Forum tour |
+ And I am on "Course 1" course homepage
+ Then I should see "Welcome to the Wiki tour"
+ And I am on "Course 2" course homepage
+ Then I should see "Welcome to the Forum tour"
+
+ @javascript
+ Scenario: Check filtering respects the sort order
+ Given the following "users" exist:
+ | username | firstname | lastname | email |
+ | student1 | Student | 1 | student1@example.com |
+ And I log in as "admin"
+ And I add a new user tour with:
+ | Name | First tour |
+ | Description | The first tour |
+ | Apply to URL match | /my/% |
+ | Tour is enabled | 1 |
+ | CSS selector | #page-my-index |
+ And I add steps to the "First tour" tour:
+ | targettype | Title | Content |
+ | Display in middle of page | Welcome | Welcome to the First tour |
+ And I add a new user tour with:
+ | Name | Second tour |
+ | Description | The second tour |
+ | Apply to URL match | /my/% |
+ | Tour is enabled | 0 |
+ | CSS selector | #page-my-index |
+ And I add steps to the "Second tour" tour:
+ | targettype | Title | Content |
+ | Display in middle of page | Welcome | Welcome to the Second tour |
+ And I add a new user tour with:
+ | Name | Third tour |
+ | Description | The third tour |
+ | Apply to URL match | /my/% |
+ | Tour is enabled | 1 |
+ | CSS selector | #page-my-index |
+ And I add steps to the "Third tour" tour:
+ | targettype | Title | Content |
+ | Display in middle of page | Welcome | Welcome to the Third tour |
+ And I am on homepage
+ Then I should see "Welcome to the First tour"
+ And I open the User tour settings page
+ And I click on "Move tour down" "link" in the "The first tour" "table_row"
+ And I click on "Move tour down" "link" in the "The first tour" "table_row"
+ And I am on homepage
+ Then I should see "Welcome to the Third tour"
diff --git a/admin/tool/usertours/tests/manager_test.php b/admin/tool/usertours/tests/manager_test.php
index 913c3e1b86e..d5e4893ccf5 100644
--- a/admin/tool/usertours/tests/manager_test.php
+++ b/admin/tool/usertours/tests/manager_test.php
@@ -222,6 +222,13 @@ class tool_usertours_manager_testcase extends advanced_testcase {
'description' => '',
'configdata' => '',
],
+ [
+ 'pathmatch' => '/my/%',
+ 'enabled' => true,
+ 'name' => 'My tour enabled 2',
+ 'description' => '',
+ 'configdata' => '',
+ ],
[
'pathmatch' => '/my/%',
'enabled' => false,
@@ -277,32 +284,32 @@ class tool_usertours_manager_testcase extends advanced_testcase {
'No matches found' => [
$alltours,
$CFG->wwwroot . '/some/invalid/value',
- null,
+ [],
],
'Never return a disabled tour' => [
$alltours,
$CFG->wwwroot . '/my/index.php',
- 'My tour enabled',
+ ['My tour enabled', 'My tour enabled 2'],
],
'My not course' => [
$alltours,
$CFG->wwwroot . '/my/index.php',
- 'My tour enabled',
+ ['My tour enabled', 'My tour enabled 2'],
],
'My with params' => [
$alltours,
$CFG->wwwroot . '/my/index.php?id=42',
- 'My tour enabled',
+ ['My tour enabled', 'My tour enabled 2'],
],
'Course with params' => [
$alltours,
$CFG->wwwroot . '/course/?id=42',
- 'course tour enabled',
+ ['course tour enabled'],
],
'Course with params and trailing content' => [
$alltours,
$CFG->wwwroot . '/course/?id=42&foo=bar',
- 'course tour with additional params enabled',
+ ['course tour with additional params enabled', 'course tour enabled'],
],
];
}
@@ -311,11 +318,11 @@ class tool_usertours_manager_testcase extends advanced_testcase {
* Tests for the get_matching_tours function.
*
* @dataProvider get_matching_tours_provider
- * @param array $alltours The list of tours to insert
- * @param string $url The URL to test
- * @param string $expected The name of the expected matching tour
+ * @param array $alltours The list of tours to insert.
+ * @param string $url The URL to test.
+ * @param array $expected List of names of the expected matching tours.
*/
- public function test_get_matching_tours($alltours, $url, $expected) {
+ public function test_get_matching_tours(array $alltours, string $url, array $expected) {
$this->resetAfterTest();
foreach ($alltours as $tourconfig) {
@@ -323,12 +330,10 @@ class tool_usertours_manager_testcase extends advanced_testcase {
$this->helper_create_step((object) ['tourid' => $tour->get_id()]);
}
- $match = \tool_usertours\manager::get_matching_tours(new moodle_url($url));
- if ($expected === null) {
- $this->assertNull($match);
- } else {
- $this->assertNotNull($match);
- $this->assertEquals($expected, $match->get_name());
+ $matches = \tool_usertours\manager::get_matching_tours(new moodle_url($url));
+ $this->assertEquals(count($expected), count($matches));
+ for ($i = 0; $i < count($matches); $i++) {
+ $this->assertEquals($expected[$i], $matches[$i]->get_name());
}
}
}
diff --git a/admin/tool/usertours/version.php b/admin/tool/usertours/version.php
index ead833e8ce0..ed8e6eef28a 100644
--- a/admin/tool/usertours/version.php
+++ b/admin/tool/usertours/version.php
@@ -24,6 +24,6 @@
defined('MOODLE_INTERNAL') || die();
-$plugin->version = 2021052501; // The current module version (Date: YYYYMMDDXX).
+$plugin->version = 2021052502; // The current module version (Date: YYYYMMDDXX).
$plugin->requires = 2021052500; // Requires this Moodle version.
$plugin->component = 'tool_usertours'; // Full name of the plugin (used for diagnostics).