diff --git a/admin/tool/usertours/amd/build/usertours.min.js b/admin/tool/usertours/amd/build/usertours.min.js index 652ed1d1c90..4778d0b8860 100644 --- a/admin/tool/usertours/amd/build/usertours.min.js +++ b/admin/tool/usertours/amd/build/usertours.min.js @@ -1,3 +1,3 @@ -define("tool_usertours/usertours",["exports","./tour","core/templates","core/log","core/notification","./repository","core/pending","./events"],(function(_exports,_tour,_templates,_log,_notification,tourRepository,_pending,_events){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.resetTourState=_exports.init=void 0,_tour=_interopRequireDefault(_tour),_templates=_interopRequireDefault(_templates),_log=_interopRequireDefault(_log),_notification=_interopRequireDefault(_notification),tourRepository=function(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}newObj.default=obj,cache&&cache.set(obj,newObj);return newObj}(tourRepository),_pending=_interopRequireDefault(_pending);var _systemImportTransformerGlobalIdentifier="undefined"!=typeof window?window:"undefined"!=typeof self?self:"undefined"!=typeof global?global:{};function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}let currentTour=null,tourId=null,restartTourAndKeepProgress=!1,currentStepNo=null;_exports.init=async(tourDetails,filters)=>{const requirements=[];filters.forEach((filter=>{requirements.push("function"==typeof _systemImportTransformerGlobalIdentifier.define&&_systemImportTransformerGlobalIdentifier.define.amd?new Promise((function(resolve,reject){_systemImportTransformerGlobalIdentifier.require(["tool_usertours/filter_".concat(filter)],resolve,reject)})):"undefined"!=typeof module&&module.exports&&"undefined"!=typeof require||"undefined"!=typeof module&&module.component&&_systemImportTransformerGlobalIdentifier.require&&"component"===_systemImportTransformerGlobalIdentifier.require.loader?Promise.resolve(require("tool_usertours/filter_".concat(filter))):Promise.resolve(_systemImportTransformerGlobalIdentifier["tool_usertours/filter_".concat(filter)]))}));const matchingTour=((tourDetails,filters)=>tourDetails.find((tour=>filters.some((filter=>!filter||!filter.filterMatches||filter.filterMatches(tour))))))(tourDetails,await Promise.all(requirements));if(!matchingTour)return;tourId=matchingTour.tourId;let startTour=matchingTour.startTour;void 0===startTour&&(startTour=!0),startTour&&fetchTour(tourId),addResetLink(),document.querySelector("body").addEventListener("click",(e=>{e.target.closest("#resetpagetour")&&(e.preventDefault(),resetTourState(tourId))})),window.addEventListener("resize",(()=>{currentTour&¤tTour.tourRunning&&(clearTimeout(window.resizedFinished),window.resizedFinished=setTimeout((()=>{currentStepNo=currentTour.getCurrentStepNumber(),restartTourAndKeepProgress=!0,resetTourState(tourId)}),250))}))};const fetchTour=async tourId=>{const pendingPromise=new _pending.default("admin_usertour_fetchTour:".concat(tourId));try{const response=await tourRepository.fetchTour(tourId);if(response.hasOwnProperty("tourconfig")){const{html:html}=await _templates.default.renderForPromise("tool_usertours/tourstep",response.tourconfig);startBootstrapTour(tourId,html,response.tourconfig)}pendingPromise.resolve()}catch(error){pendingPromise.resolve(),_notification.default.exception(error)}},addResetLink=()=>{const pendingPromise=new _pending.default("admin_usertour_addResetLink");_templates.default.render("tool_usertours/resettour",{}).then((function(html,js){_templates.default.appendNodeContents((()=>{let location=document.querySelector(".tool_usertours-resettourcontainer");return location||(location=document.querySelector(".logininfo"),location||(location=document.querySelector("footer"),location||document.body))})(),html,js)})).catch().then(pendingPromise.resolve).catch()},startBootstrapTour=(tourId,template,tourConfig)=>{currentTour&¤tTour.tourRunning&&(currentTour.endTour(),currentTour=null),document.addEventListener(_events.eventTypes.tourEnded,markTourComplete),document.addEventListener(_events.eventTypes.stepRenderer,markStepShown),tourConfig.tourName=tourConfig.name,delete tourConfig.name,tourConfig.template=template,tourConfig.steps=tourConfig.steps.map((function(step){return void 0!==step.element&&(step.target=step.element,delete step.element),void 0!==step.reflex&&(step.moveOnClick=!!step.reflex,delete step.reflex),void 0!==step.content&&(step.body=step.content,delete step.content),step})),currentTour=new _tour.default(tourConfig);let startAt=0;return restartTourAndKeepProgress&¤tStepNo&&(startAt=currentStepNo,restartTourAndKeepProgress=!1,currentStepNo=null),currentTour.startTour(startAt)},markStepShown=e=>{const tour=e.detail.tour,stepConfig=tour.getStepConfig(tour.getCurrentStepNumber());tourRepository.markStepShown(stepConfig.stepid,tourId,tour.getCurrentStepNumber()).catch(_log.default.error)},markTourComplete=e=>{document.removeEventListener(_events.eventTypes.tourEnded,markTourComplete),document.removeEventListener(_events.eventTypes.stepRenderer,markStepShown);const tour=e.detail.tour,stepConfig=tour.getStepConfig(tour.getCurrentStepNumber());tourRepository.markTourComplete(stepConfig.stepid,tourId,tour.getCurrentStepNumber()).catch(_log.default.error)},resetTourState=tourId=>tourRepository.resetTourState(tourId).then((response=>{response.startTour&&fetchTour(response.startTour)})).catch(_notification.default.exception);_exports.resetTourState=resetTourState})); +define("tool_usertours/usertours",["exports","./tour","core/templates","core/log","core/notification","./repository","core/pending","./events"],(function(_exports,_tour,_templates,_log,_notification,tourRepository,_pending,_events){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.resetTourState=_exports.init=void 0,_tour=_interopRequireDefault(_tour),_templates=_interopRequireDefault(_templates),_log=_interopRequireDefault(_log),_notification=_interopRequireDefault(_notification),tourRepository=function(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}newObj.default=obj,cache&&cache.set(obj,newObj);return newObj}(tourRepository),_pending=_interopRequireDefault(_pending);var _systemImportTransformerGlobalIdentifier="undefined"!=typeof window?window:"undefined"!=typeof self?self:"undefined"!=typeof global?global:{};function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}let currentTour=null,tourId=null,restartTourAndKeepProgress=!1,currentStepNo=null;_exports.init=async(tourDetails,filters)=>{const requirements=[];filters.forEach((filter=>{requirements.push("function"==typeof _systemImportTransformerGlobalIdentifier.define&&_systemImportTransformerGlobalIdentifier.define.amd?new Promise((function(resolve,reject){_systemImportTransformerGlobalIdentifier.require([filter],resolve,reject)})):"undefined"!=typeof module&&module.exports&&"undefined"!=typeof require||"undefined"!=typeof module&&module.component&&_systemImportTransformerGlobalIdentifier.require&&"component"===_systemImportTransformerGlobalIdentifier.require.loader?Promise.resolve(require(filter)):Promise.resolve(_systemImportTransformerGlobalIdentifier[filter]))}));const matchingTour=((tourDetails,filters)=>tourDetails.find((tour=>filters.some((filter=>!filter||!filter.filterMatches||filter.filterMatches(tour))))))(tourDetails,await Promise.all(requirements));if(!matchingTour)return;tourId=matchingTour.tourId;let startTour=matchingTour.startTour;void 0===startTour&&(startTour=!0),startTour&&fetchTour(tourId),addResetLink(),document.querySelector("body").addEventListener("click",(e=>{e.target.closest("#resetpagetour")&&(e.preventDefault(),resetTourState(tourId))})),window.addEventListener("resize",(()=>{currentTour&¤tTour.tourRunning&&(clearTimeout(window.resizedFinished),window.resizedFinished=setTimeout((()=>{currentStepNo=currentTour.getCurrentStepNumber(),restartTourAndKeepProgress=!0,resetTourState(tourId)}),250))}))};const fetchTour=async tourId=>{const pendingPromise=new _pending.default("admin_usertour_fetchTour:".concat(tourId));try{const response=await tourRepository.fetchTour(tourId);if(response.hasOwnProperty("tourconfig")){const{html:html}=await _templates.default.renderForPromise("tool_usertours/tourstep",response.tourconfig);startBootstrapTour(tourId,html,response.tourconfig)}pendingPromise.resolve()}catch(error){pendingPromise.resolve(),_notification.default.exception(error)}},addResetLink=()=>{const pendingPromise=new _pending.default("admin_usertour_addResetLink");_templates.default.render("tool_usertours/resettour",{}).then((function(html,js){_templates.default.appendNodeContents((()=>{let location=document.querySelector(".tool_usertours-resettourcontainer");return location||(location=document.querySelector(".logininfo"),location||(location=document.querySelector("footer"),location||document.body))})(),html,js)})).catch().then(pendingPromise.resolve).catch()},startBootstrapTour=(tourId,template,tourConfig)=>{currentTour&¤tTour.tourRunning&&(currentTour.endTour(),currentTour=null),document.addEventListener(_events.eventTypes.tourEnded,markTourComplete),document.addEventListener(_events.eventTypes.stepRenderer,markStepShown),tourConfig.tourName=tourConfig.name,delete tourConfig.name,tourConfig.template=template,tourConfig.steps=tourConfig.steps.map((function(step){return void 0!==step.element&&(step.target=step.element,delete step.element),void 0!==step.reflex&&(step.moveOnClick=!!step.reflex,delete step.reflex),void 0!==step.content&&(step.body=step.content,delete step.content),step})),currentTour=new _tour.default(tourConfig);let startAt=0;return restartTourAndKeepProgress&¤tStepNo&&(startAt=currentStepNo,restartTourAndKeepProgress=!1,currentStepNo=null),currentTour.startTour(startAt)},markStepShown=e=>{const tour=e.detail.tour,stepConfig=tour.getStepConfig(tour.getCurrentStepNumber());tourRepository.markStepShown(stepConfig.stepid,tourId,tour.getCurrentStepNumber()).catch(_log.default.error)},markTourComplete=e=>{document.removeEventListener(_events.eventTypes.tourEnded,markTourComplete),document.removeEventListener(_events.eventTypes.stepRenderer,markStepShown);const tour=e.detail.tour,stepConfig=tour.getStepConfig(tour.getCurrentStepNumber());tourRepository.markTourComplete(stepConfig.stepid,tourId,tour.getCurrentStepNumber()).catch(_log.default.error)},resetTourState=tourId=>tourRepository.resetTourState(tourId).then((response=>{response.startTour&&fetchTour(response.startTour)})).catch(_notification.default.exception);_exports.resetTourState=resetTourState})); //# sourceMappingURL=usertours.min.js.map \ No newline at end of file diff --git a/admin/tool/usertours/amd/build/usertours.min.js.map b/admin/tool/usertours/amd/build/usertours.min.js.map index 8c3fa410513..05c28d7d39e 100644 --- a/admin/tool/usertours/amd/build/usertours.min.js.map +++ b/admin/tool/usertours/amd/build/usertours.min.js.map @@ -1 +1 @@ -{"version":3,"file":"usertours.min.js","sources":["../src/usertours.js"],"sourcesContent":["/**\n * User tour control library.\n *\n * @module tool_usertours/usertours\n * @copyright 2016 Andrew Nicols \n */\nimport BootstrapTour from './tour';\nimport Templates from 'core/templates';\nimport log from 'core/log';\nimport notification from 'core/notification';\nimport * as tourRepository from './repository';\nimport Pending from 'core/pending';\nimport {eventTypes} from './events';\n\nlet currentTour = null;\nlet tourId = null;\nlet restartTourAndKeepProgress = false;\nlet currentStepNo = null;\n\n/**\n * Find the first matching tour.\n *\n * @param {object[]} tourDetails\n * @param {object[]} filters\n * @returns {null|object}\n */\nconst findMatchingTour = (tourDetails, filters) => {\n return tourDetails.find(tour => filters.some(filter => {\n if (filter && filter.filterMatches) {\n return filter.filterMatches(tour);\n }\n\n return true;\n }));\n};\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 */\nexport const init = async(tourDetails, filters) => {\n const requirements = [];\n filters.forEach(filter => {\n requirements.push(import(`tool_usertours/filter_${filter}`));\n });\n\n const filterPlugins = await Promise.all(requirements);\n\n const matchingTour = findMatchingTour(tourDetails, filterPlugins);\n if (!matchingTour) {\n return;\n }\n\n // Only one tour per page is allowed.\n 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 fetchTour(tourId);\n }\n\n addResetLink();\n\n // Watch for the reset link.\n document.querySelector('body').addEventListener('click', e => {\n const resetLink = e.target.closest('#resetpagetour');\n if (resetLink) {\n e.preventDefault();\n resetTourState(tourId);\n }\n });\n\n // Watch for the resize event.\n window.addEventListener(\"resize\", () => {\n // Only listen for the running tour.\n if (currentTour && currentTour.tourRunning) {\n clearTimeout(window.resizedFinished);\n window.resizedFinished = setTimeout(() => {\n // Wait until the resize event has finished.\n currentStepNo = currentTour.getCurrentStepNumber();\n restartTourAndKeepProgress = true;\n resetTourState(tourId);\n }, 250);\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 */\nconst fetchTour = async tourId => {\n const pendingPromise = new Pending(`admin_usertour_fetchTour:${tourId}`);\n\n try {\n // If we don't have any tour config (because it doesn't need showing for the current user), return early.\n const response = await tourRepository.fetchTour(tourId);\n if (response.hasOwnProperty('tourconfig')) {\n const {html} = await Templates.renderForPromise('tool_usertours/tourstep', response.tourconfig);\n startBootstrapTour(tourId, html, response.tourconfig);\n }\n pendingPromise.resolve();\n } catch (error) {\n pendingPromise.resolve();\n notification.exception(error);\n }\n};\n\nconst getPreferredResetLocation = () => {\n let location = document.querySelector('.tool_usertours-resettourcontainer');\n if (location) {\n return location;\n }\n\n location = document.querySelector('.logininfo');\n if (location) {\n return location;\n }\n\n location = document.querySelector('footer');\n if (location) {\n return location;\n }\n\n return document.body;\n};\n\n/**\n * Add a reset link to the page.\n *\n * @method addResetLink\n */\nconst addResetLink = () => {\n const pendingPromise = new Pending('admin_usertour_addResetLink');\n\n Templates.render('tool_usertours/resettour', {})\n .then(function(html, js) {\n // Append the link to the most suitable place on the page with fallback to legacy selectors and finally the body if\n // there is no better place.\n Templates.appendNodeContents(getPreferredResetLocation(), html, js);\n\n return;\n })\n .catch()\n .then(pendingPromise.resolve)\n .catch();\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 */\nconst startBootstrapTour = (tourId, template, tourConfig) => {\n if (currentTour && currentTour.tourRunning) {\n // End the current tour.\n currentTour.endTour();\n currentTour = null;\n }\n\n document.addEventListener(eventTypes.tourEnded, markTourComplete);\n document.addEventListener(eventTypes.stepRenderer, markStepShown);\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 currentTour = new BootstrapTour(tourConfig);\n let startAt = 0;\n if (restartTourAndKeepProgress && currentStepNo) {\n startAt = currentStepNo;\n restartTourAndKeepProgress = false;\n currentStepNo = null;\n }\n return currentTour.startTour(startAt);\n};\n\n/**\n * Mark the specified step as being shownd by the user.\n *\n * @method markStepShown\n * @param {Event} e\n */\nconst markStepShown = e => {\n const tour = e.detail.tour;\n const stepConfig = tour.getStepConfig(tour.getCurrentStepNumber());\n tourRepository.markStepShown(\n stepConfig.stepid,\n tourId,\n tour.getCurrentStepNumber()\n ).catch(log.error);\n};\n\n/**\n * Mark the specified tour as being completed by the user.\n *\n * @method markTourComplete\n * @param {Event} e\n * @listens tool_usertours/stepRendered\n */\nconst markTourComplete = e => {\n document.removeEventListener(eventTypes.tourEnded, markTourComplete);\n document.removeEventListener(eventTypes.stepRenderer, markStepShown);\n\n const tour = e.detail.tour;\n const stepConfig = tour.getStepConfig(tour.getCurrentStepNumber());\n tourRepository.markTourComplete(\n stepConfig.stepid,\n tourId,\n tour.getCurrentStepNumber()\n ).catch(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 * @returns {Promise}\n */\nexport const resetTourState = tourId => tourRepository.resetTourState(tourId)\n.then(response => {\n if (response.startTour) {\n fetchTour(response.startTour);\n }\n return;\n}).catch(notification.exception);\n"],"names":["currentTour","tourId","restartTourAndKeepProgress","currentStepNo","async","tourDetails","filters","requirements","forEach","filter","push","matchingTour","find","tour","some","filterMatches","findMatchingTour","Promise","all","startTour","fetchTour","addResetLink","document","querySelector","addEventListener","e","target","closest","preventDefault","resetTourState","window","tourRunning","clearTimeout","resizedFinished","setTimeout","getCurrentStepNumber","pendingPromise","Pending","response","tourRepository","hasOwnProperty","html","Templates","renderForPromise","tourconfig","startBootstrapTour","resolve","error","exception","render","then","js","appendNodeContents","location","body","getPreferredResetLocation","catch","template","tourConfig","endTour","eventTypes","tourEnded","markTourComplete","stepRenderer","markStepShown","tourName","name","steps","map","step","element","reflex","moveOnClick","content","BootstrapTour","startAt","detail","stepConfig","getStepConfig","stepid","log","removeEventListener","notification"],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"usertours.min.js","sources":["../src/usertours.js"],"sourcesContent":["/**\n * User tour control library.\n *\n * @module tool_usertours/usertours\n * @copyright 2016 Andrew Nicols \n */\nimport BootstrapTour from './tour';\nimport Templates from 'core/templates';\nimport log from 'core/log';\nimport notification from 'core/notification';\nimport * as tourRepository from './repository';\nimport Pending from 'core/pending';\nimport {eventTypes} from './events';\n\nlet currentTour = null;\nlet tourId = null;\nlet restartTourAndKeepProgress = false;\nlet currentStepNo = null;\n\n/**\n * Find the first matching tour.\n *\n * @param {object[]} tourDetails\n * @param {object[]} filters\n * @returns {null|object}\n */\nconst findMatchingTour = (tourDetails, filters) => {\n return tourDetails.find(tour => filters.some(filter => {\n if (filter && filter.filterMatches) {\n return filter.filterMatches(tour);\n }\n\n return true;\n }));\n};\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 */\nexport const init = async(tourDetails, filters) => {\n const requirements = [];\n filters.forEach((filter) => {\n requirements.push(import(filter));\n });\n\n const filterPlugins = await Promise.all(requirements);\n\n const matchingTour = findMatchingTour(tourDetails, filterPlugins);\n if (!matchingTour) {\n return;\n }\n\n // Only one tour per page is allowed.\n 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 fetchTour(tourId);\n }\n\n addResetLink();\n\n // Watch for the reset link.\n document.querySelector('body').addEventListener('click', e => {\n const resetLink = e.target.closest('#resetpagetour');\n if (resetLink) {\n e.preventDefault();\n resetTourState(tourId);\n }\n });\n\n // Watch for the resize event.\n window.addEventListener(\"resize\", () => {\n // Only listen for the running tour.\n if (currentTour && currentTour.tourRunning) {\n clearTimeout(window.resizedFinished);\n window.resizedFinished = setTimeout(() => {\n // Wait until the resize event has finished.\n currentStepNo = currentTour.getCurrentStepNumber();\n restartTourAndKeepProgress = true;\n resetTourState(tourId);\n }, 250);\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 */\nconst fetchTour = async tourId => {\n const pendingPromise = new Pending(`admin_usertour_fetchTour:${tourId}`);\n\n try {\n // If we don't have any tour config (because it doesn't need showing for the current user), return early.\n const response = await tourRepository.fetchTour(tourId);\n if (response.hasOwnProperty('tourconfig')) {\n const {html} = await Templates.renderForPromise('tool_usertours/tourstep', response.tourconfig);\n startBootstrapTour(tourId, html, response.tourconfig);\n }\n pendingPromise.resolve();\n } catch (error) {\n pendingPromise.resolve();\n notification.exception(error);\n }\n};\n\nconst getPreferredResetLocation = () => {\n let location = document.querySelector('.tool_usertours-resettourcontainer');\n if (location) {\n return location;\n }\n\n location = document.querySelector('.logininfo');\n if (location) {\n return location;\n }\n\n location = document.querySelector('footer');\n if (location) {\n return location;\n }\n\n return document.body;\n};\n\n/**\n * Add a reset link to the page.\n *\n * @method addResetLink\n */\nconst addResetLink = () => {\n const pendingPromise = new Pending('admin_usertour_addResetLink');\n\n Templates.render('tool_usertours/resettour', {})\n .then(function(html, js) {\n // Append the link to the most suitable place on the page with fallback to legacy selectors and finally the body if\n // there is no better place.\n Templates.appendNodeContents(getPreferredResetLocation(), html, js);\n\n return;\n })\n .catch()\n .then(pendingPromise.resolve)\n .catch();\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 */\nconst startBootstrapTour = (tourId, template, tourConfig) => {\n if (currentTour && currentTour.tourRunning) {\n // End the current tour.\n currentTour.endTour();\n currentTour = null;\n }\n\n document.addEventListener(eventTypes.tourEnded, markTourComplete);\n document.addEventListener(eventTypes.stepRenderer, markStepShown);\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 currentTour = new BootstrapTour(tourConfig);\n let startAt = 0;\n if (restartTourAndKeepProgress && currentStepNo) {\n startAt = currentStepNo;\n restartTourAndKeepProgress = false;\n currentStepNo = null;\n }\n return currentTour.startTour(startAt);\n};\n\n/**\n * Mark the specified step as being shownd by the user.\n *\n * @method markStepShown\n * @param {Event} e\n */\nconst markStepShown = e => {\n const tour = e.detail.tour;\n const stepConfig = tour.getStepConfig(tour.getCurrentStepNumber());\n tourRepository.markStepShown(\n stepConfig.stepid,\n tourId,\n tour.getCurrentStepNumber()\n ).catch(log.error);\n};\n\n/**\n * Mark the specified tour as being completed by the user.\n *\n * @method markTourComplete\n * @param {Event} e\n * @listens tool_usertours/stepRendered\n */\nconst markTourComplete = e => {\n document.removeEventListener(eventTypes.tourEnded, markTourComplete);\n document.removeEventListener(eventTypes.stepRenderer, markStepShown);\n\n const tour = e.detail.tour;\n const stepConfig = tour.getStepConfig(tour.getCurrentStepNumber());\n tourRepository.markTourComplete(\n stepConfig.stepid,\n tourId,\n tour.getCurrentStepNumber()\n ).catch(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 * @returns {Promise}\n */\nexport const resetTourState = tourId => tourRepository.resetTourState(tourId)\n.then(response => {\n if (response.startTour) {\n fetchTour(response.startTour);\n }\n return;\n}).catch(notification.exception);\n"],"names":["currentTour","tourId","restartTourAndKeepProgress","currentStepNo","async","tourDetails","filters","requirements","forEach","filter","push","matchingTour","find","tour","some","filterMatches","findMatchingTour","Promise","all","startTour","fetchTour","addResetLink","document","querySelector","addEventListener","e","target","closest","preventDefault","resetTourState","window","tourRunning","clearTimeout","resizedFinished","setTimeout","getCurrentStepNumber","pendingPromise","Pending","response","tourRepository","hasOwnProperty","html","Templates","renderForPromise","tourconfig","startBootstrapTour","resolve","error","exception","render","then","js","appendNodeContents","location","body","getPreferredResetLocation","catch","template","tourConfig","endTour","eventTypes","tourEnded","markTourComplete","stepRenderer","markStepShown","tourName","name","steps","map","step","element","reflex","moveOnClick","content","BootstrapTour","startAt","detail","stepConfig","getStepConfig","stepid","log","removeEventListener","notification"],"mappings":"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"} \ No newline at end of file diff --git a/admin/tool/usertours/amd/src/usertours.js b/admin/tool/usertours/amd/src/usertours.js index 8e64e8d68ea..b9e2c4ee364 100644 --- a/admin/tool/usertours/amd/src/usertours.js +++ b/admin/tool/usertours/amd/src/usertours.js @@ -43,8 +43,8 @@ const findMatchingTour = (tourDetails, filters) => { */ export const init = async(tourDetails, filters) => { const requirements = []; - filters.forEach(filter => { - requirements.push(import(`tool_usertours/filter_${filter}`)); + filters.forEach((filter) => { + requirements.push(import(filter)); }); const filterPlugins = await Promise.all(requirements); diff --git a/admin/tool/usertours/classes/helper.php b/admin/tool/usertours/classes/helper.php index 8327dbd0ed4..13ca3ddd68f 100644 --- a/admin/tool/usertours/classes/helper.php +++ b/admin/tool/usertours/classes/helper.php @@ -16,6 +16,7 @@ namespace tool_usertours; +use coding_exception; use core\output\inplace_editable; /** @@ -534,10 +535,7 @@ class helper { ]; }, $tours); - $filternames = []; - foreach ($filters as $filter) { - $filternames[] = $filter::get_filter_name(); - } + $filternames = self::get_clientside_filter_module_names($filters); $PAGE->requires->js_call_amd('tool_usertours/usertours', 'init', [ $tourdetails, @@ -546,19 +544,48 @@ class helper { } } + /** + * Get the JS module names for the filters. + * + * @param array $filters + * @return array + * @throws coding_exception + */ + public static function get_clientside_filter_module_names(array $filters): array { + $filternames = []; + foreach ($filters as $filter) { + if ($component = \core_component::get_component_from_classname($filter)) { + $filternames[] = sprintf( + "%s/filter_%s", + $component, + $filter::get_filter_name(), + ); + } else { + throw new \coding_exception("Could not determine component for filter class {$filter}"); + } + } + + return $filternames; + } + /** * Get a list of all possible filters. * * @return array */ public static function get_all_filters() { - $filters = \core_component::get_component_classes_in_namespace('tool_usertours', 'local\filter'); - $filters = array_keys($filters); + $hook = new hook\before_serverside_filter_fetch(array_keys( + \core_component::get_component_classes_in_namespace('tool_usertours', 'local\filter') + )); + \core\di::get(\core\hook\manager::class)->dispatch($hook); - $filters = array_filter($filters, function ($filterclass) { - $rc = new \ReflectionClass($filterclass); - return $rc->isInstantiable(); - }); + $filters = array_filter( + $hook->get_filter_list(), + function ($filterclass) { + $rc = new \ReflectionClass($filterclass); + return $rc->isInstantiable(); + } + ); $filters = array_merge($filters, static::get_all_clientside_filters()); @@ -571,13 +598,18 @@ class helper { * @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); + $hook = new hook\before_clientside_filter_fetch(array_keys( + \core_component::get_component_classes_in_namespace('tool_usertours', 'local\clientside_filter') + )); + \core\di::get(\core\hook\manager::class)->dispatch($hook); - $filters = array_filter($filters, function ($filterclass) { - $rc = new \ReflectionClass($filterclass); - return $rc->isInstantiable(); - }); + $filters = array_filter( + $hook->get_filter_list(), + function ($filterclass) { + $rc = new \ReflectionClass($filterclass); + return $rc->isInstantiable(); + } + ); return $filters; } diff --git a/admin/tool/usertours/classes/hook/before_clientside_filter_fetch.php b/admin/tool/usertours/classes/hook/before_clientside_filter_fetch.php new file mode 100644 index 00000000000..ceea91eeb45 --- /dev/null +++ b/admin/tool/usertours/classes/hook/before_clientside_filter_fetch.php @@ -0,0 +1,74 @@ +. + +namespace tool_usertours\hook; + +/** + * Provides the ability to add and remove custom client-side filters to the user tour filter list. + * + * @package tool_usertours + * @copyright 2024 Andrew Lyons + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +#[\core\attribute\label('Provides the ability to add and remove custom client-side filters to the user tour filter list.')] +#[\core\attribute\tags('tool_usertours')] +class before_clientside_filter_fetch { + /** + * Create a new instance of the hook. + * + * @param array $filters + */ + public function __construct( + /** @var array The list of filters applied */ + protected array $filters, + ) { + } + + /** + * Add a filter classname to the list of filters to be processed. + * + * @param string $classname + * @return self + */ + public function add_filter_by_classname(string $classname): self { + if (!\is_a($classname, \tool_usertours\local\clientside_filter\clientside_filter::class, true)) { + throw new \coding_exception("Invalid clientside filter class {$classname}"); + } + $this->filters[] = $classname; + + return $this; + } + + /** + * Remove a filter classname from the list of filters to be processed. + * + * @param string $classname + * @return self + */ + public function remove_filter_by_classname(string $classname): self { + $this->filters = array_filter($this->filters, fn($filter) => $filter !== $classname); + return $this; + } + + /** + * Get the list of filters to be processed. + * + * @return array + */ + public function get_filter_list(): array { + return $this->filters; + } +} diff --git a/admin/tool/usertours/classes/hook/before_serverside_filter_fetch.php b/admin/tool/usertours/classes/hook/before_serverside_filter_fetch.php new file mode 100644 index 00000000000..079816e9663 --- /dev/null +++ b/admin/tool/usertours/classes/hook/before_serverside_filter_fetch.php @@ -0,0 +1,78 @@ +. + +namespace tool_usertours\hook; + +/** + * Provides the ability to add and remove custom server-side filters to the user tour filter list. + * + * @package tool_usertours + * @copyright 2024 Andrew Lyons + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +#[\core\attribute\label('Provides the ability to add and remove custom server-side filters to the user tour filter list.')] +#[\core\attribute\tags('tool_usertours')] +class before_serverside_filter_fetch { + /** + * Create a new instance of the hook. + * + * @param array $filters + */ + public function __construct( + /** @var array The list of filters applied */ + protected array $filters, + ) { + } + + /** + * Add a filter classname to the list of filters to be processed. + * + * @param string $classname + * @return self + */ + public function add_filter_by_classname(string $classname): self { + if (!\is_a($classname, \tool_usertours\local\filter\base::class, true)) { + throw new \coding_exception("Invalid filter class {$classname}"); + } + + if (\is_a($classname, \tool_usertours\local\clientside_filter\clientside_filter::class, true)) { + throw new \coding_exception("Invalid filter class {$classname} (client-side filter for server-side hook)"); + } + + $this->filters[] = $classname; + return $this; + } + + /** + * Remove a filter classname from the list of filters to be processed. + * + * @param string $classname + * @return self + */ + public function remove_filter_by_classname(string $classname): self { + $this->filters = array_filter($this->filters, fn($filter) => $filter !== $classname); + return $this; + } + + /** + * Get the list of filters to be processed. + * + * @return array + */ + public function get_filter_list(): array { + return $this->filters; + } +} diff --git a/admin/tool/usertours/tests/fixtures/clientside_filter_for_serverside_hook.php b/admin/tool/usertours/tests/fixtures/clientside_filter_for_serverside_hook.php new file mode 100644 index 00000000000..692eca99f8c --- /dev/null +++ b/admin/tool/usertours/tests/fixtures/clientside_filter_for_serverside_hook.php @@ -0,0 +1,45 @@ +. + +/** + * Hook fixtures for testing of hooks. + * + * @package tool_usertours + * @copyright 2024 Andrew Lyons + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace tool_usertours\test\hook\clientside_filter_for_serverside_hook; + +defined('MOODLE_INTERNAL') || die(); + +final class filter_class extends \tool_usertours\local\clientside_filter\clientside_filter { +} + +final class callback { + public static function callme( + \tool_usertours\hook\before_serverside_filter_fetch $hook + ): void { + $hook->add_filter_by_classname(filter_class::class); + } +} + +$callbacks = [ + [ + 'hook' => \tool_usertours\hook\before_serverside_filter_fetch::class, + 'callback' => callback::class . '::callme', + ], +]; diff --git a/admin/tool/usertours/tests/fixtures/hook_fixtures.php b/admin/tool/usertours/tests/fixtures/hook_fixtures.php new file mode 100644 index 00000000000..ce98df86bbd --- /dev/null +++ b/admin/tool/usertours/tests/fixtures/hook_fixtures.php @@ -0,0 +1,61 @@ +. + +/** + * Hook fixtures for testing of hooks. + * + * @package tool_usertours + * @copyright 2024 Andrew Lyons + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace tool_usertours\test\hook; + +defined('MOODLE_INTERNAL') || die(); + +final class serverside_filter_fixture extends \tool_usertours\local\filter\base { +} + +final class clientside_filter_fixture extends \tool_usertours\local\clientside_filter\clientside_filter { +} + +final class hook_fixtures { + public static function example_serverside_hook( + \tool_usertours\hook\before_serverside_filter_fetch $hook + ): void { + // Add a valid serverside and an invalid clientside filter. + $hook->add_filter_by_classname(\tool_usertours\test\hook\serverside_filter_fixture::class); + $hook->remove_filter_by_classname(\tool_usertours\local\filter\accessdate::class); + } + + public static function example_clientside_hook( + \tool_usertours\hook\before_clientside_filter_fetch $hook + ): void { + $hook->add_filter_by_classname(\tool_usertours\test\hook\clientside_filter_fixture::class); + $hook->remove_filter_by_classname(\tool_usertours\local\clientside_filter\cssselector::class); + } +} + +$callbacks = [ + [ + 'hook' => \tool_usertours\hook\before_serverside_filter_fetch::class, + 'callback' => \tool_usertours\test\hook\hook_fixtures::class . '::example_serverside_hook', + ], + [ + 'hook' => \tool_usertours\hook\before_clientside_filter_fetch::class, + 'callback' => \tool_usertours\test\hook\hook_fixtures::class . '::example_clientside_hook', + ], +]; diff --git a/admin/tool/usertours/tests/fixtures/invalid_clientside_hook_fixture.php b/admin/tool/usertours/tests/fixtures/invalid_clientside_hook_fixture.php new file mode 100644 index 00000000000..2d6f7684874 --- /dev/null +++ b/admin/tool/usertours/tests/fixtures/invalid_clientside_hook_fixture.php @@ -0,0 +1,43 @@ +. + +/** + * Hook fixtures for testing of hooks. + * + * @package tool_usertours + * @copyright 2024 Andrew Lyons + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +final class nocomponent_clientside_filter_fixture extends \tool_usertours\local\clientside_filter\clientside_filter { +} + +final class nocomponent_clientside_hook_fixtures { + public static function example_clientside_hook( + \tool_usertours\hook\before_clientside_filter_fetch $hook + ): void { + $hook->add_filter_by_classname(\nocomponent_clientside_filter_fixture::class); + } +} + +$callbacks = [ + [ + 'hook' => \tool_usertours\hook\before_clientside_filter_fetch::class, + 'callback' => \nocomponent_clientside_hook_fixtures::class . '::example_clientside_hook', + ], +]; diff --git a/admin/tool/usertours/tests/fixtures/invalid_serverside_hook_fixture.php b/admin/tool/usertours/tests/fixtures/invalid_serverside_hook_fixture.php new file mode 100644 index 00000000000..214b85a5696 --- /dev/null +++ b/admin/tool/usertours/tests/fixtures/invalid_serverside_hook_fixture.php @@ -0,0 +1,43 @@ +. + +/** + * Hook fixtures for testing of hooks. + * + * @package tool_usertours + * @copyright 2024 Andrew Lyons + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +final class nocomponent_serverside_filter_fixture { +} + +final class nocomponent_serverside_hook_fixtures { + public static function example_serverside_hook( + \tool_usertours\hook\before_serverside_filter_fetch $hook + ): void { + $hook->add_filter_by_classname(\nocomponent_serverside_filter_fixture::class); + } +} + +$callbacks = [ + [ + 'hook' => \tool_usertours\hook\before_serverside_filter_fetch::class, + 'callback' => \nocomponent_serverside_hook_fixtures::class . '::example_serverside_hook', + ], +]; diff --git a/admin/tool/usertours/tests/fixtures/serverside_filter_for_clientside_hook.php b/admin/tool/usertours/tests/fixtures/serverside_filter_for_clientside_hook.php new file mode 100644 index 00000000000..6fb2310f4ce --- /dev/null +++ b/admin/tool/usertours/tests/fixtures/serverside_filter_for_clientside_hook.php @@ -0,0 +1,45 @@ +. + +/** + * Hook fixtures for testing of hooks. + * + * @package tool_usertours + * @copyright 2024 Andrew Lyons + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace tool_usertours\test\hook\serverside_filter_for_clientside_hook; + +defined('MOODLE_INTERNAL') || die(); + +final class filter_class extends \tool_usertours\local\filter\base { +} + +final class callback { + public static function callme( + \tool_usertours\hook\before_clientside_filter_fetch $hook + ): void { + $hook->add_filter_by_classname(filter_class::class); + } +} + +$callbacks = [ + [ + 'hook' => \tool_usertours\hook\before_clientside_filter_fetch::class, + 'callback' => callback::class . '::callme', + ], +]; diff --git a/admin/tool/usertours/tests/helper_test.php b/admin/tool/usertours/tests/helper_test.php index 3a2f1cdad26..2b118af6899 100644 --- a/admin/tool/usertours/tests/helper_test.php +++ b/admin/tool/usertours/tests/helper_test.php @@ -16,17 +16,18 @@ namespace tool_usertours; -use advanced_testcase; - /** * Tests for helper. * * @package tool_usertours + * @category test * @copyright 2022 Huong Nguyen * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - * @covers \tool_usertours\helper + * @covers \tool_usertours\helper + * @covers \tool_usertours\hook\before_serverside_filter_fetch + * @covers \tool_usertours\hook\before_clientside_filter_fetch */ -class helper_test extends advanced_testcase { +final class helper_test extends \advanced_testcase { /** * Data Provider for get_string_from_input. * @@ -71,4 +72,117 @@ class helper_test extends advanced_testcase { public function test_get_string_from_input($string, $expected): void { $this->assertEquals($expected, helper::get_string_from_input($string)); } + + public function test_get_all_filters(): void { + $filters = helper::get_all_filters(); + $this->assertIsArray($filters); + + array_map( + function ($filter) { + $this->assertIsString($filter); + $this->assertTrue(class_exists($filter)); + $this->assertTrue(is_a($filter, \tool_usertours\local\filter\base::class, true)); + $rc = new \ReflectionClass($filter); + $this->assertTrue($rc->isInstantiable()); + }, + $filters, + ); + + $this->assertNotContains(\tool_usertours\test\hook\serverside_filter_fixture::class, $filters); + $this->assertNotContains(\tool_usertours\test\hook\clientside_filter_fixture::class, $filters); + $this->assertContains(\tool_usertours\local\filter\accessdate::class, $filters); + $this->assertContains(\tool_usertours\local\clientside_filter\cssselector::class, $filters); + + $filters = helper::get_all_clientside_filters(); + array_map( + function ($filter) { + $this->assertIsString($filter); + }, + $filters, + ); + } + + public function test_get_invalid_server_filter(): void { + \core\di::set( + \core\hook\manager::class, + \core\hook\manager::phpunit_get_instance([ + 'test_plugin1' => __DIR__ . '/fixtures/invalid_serverside_hook_fixture.php', + ]), + ); + + $this->expectException(\coding_exception::class); + helper::get_all_filters(); + } + + public function test_clientside_filter_for_serverside_hook(): void { + \core\di::set( + \core\hook\manager::class, + \core\hook\manager::phpunit_get_instance([ + 'test_plugin1' => __DIR__ . '/fixtures/clientside_filter_for_serverside_hook.php', + ]), + ); + + $this->expectException(\coding_exception::class); + helper::get_all_filters(); + } + + public function test_serverside_filter_for_clientside_hook(): void { + \core\di::set( + \core\hook\manager::class, + \core\hook\manager::phpunit_get_instance([ + 'test_plugin1' => __DIR__ . '/fixtures/serverside_filter_for_clientside_hook.php', + ]), + ); + + $this->expectException(\coding_exception::class); + helper::get_all_clientside_filters(); + } + + public function test_filter_hooks(): void { + \core\di::set( + \core\hook\manager::class, + \core\hook\manager::phpunit_get_instance([ + 'test_plugin1' => __DIR__ . '/fixtures/hook_fixtures.php', + ]), + ); + + $filters = helper::get_all_filters(); + $this->assertIsArray($filters); + + // Check the modifications from the serverside hook. + $this->assertContains(\tool_usertours\test\hook\serverside_filter_fixture::class, $filters); + $this->assertNotContains(\tool_usertours\test\hook\another_clientside_filter_fixture::class, $filters); + $this->assertNotContains(\tool_usertours\local\filter\accessdate::class, $filters); + + // Check the modifications from the clientside hook. + $this->assertContains(\tool_usertours\test\hook\clientside_filter_fixture::class, $filters); + $this->assertNotContains(\tool_usertours\test\hook\another_serverside_filter_fixture::class, $filters); + $this->assertNotContains(\tool_usertours\local\clientside_filter\cssselector::class, $filters); + + array_map( + function ($filter) { + $this->assertIsString($filter); + $this->assertTrue(class_exists($filter)); + $this->assertTrue(is_a($filter, \tool_usertours\local\filter\base::class, true)); + $rc = new \ReflectionClass($filter); + $this->assertTrue($rc->isInstantiable()); + }, + $filters, + ); + } + + public function test_get_clientside_filter_module_names(): void { + \core\di::set( + \core\hook\manager::class, + \core\hook\manager::phpunit_get_instance([ + 'test_plugin1' => __DIR__ . '/fixtures/invalid_clientside_hook_fixture.php', + ]), + ); + + $filters = helper::get_all_clientside_filters(); + + $this->expectException(\coding_exception::class); + $this->expectExceptionMessageMatches('/Could not determine component/'); + helper::get_clientside_filter_module_names($filters); + } } diff --git a/admin/tool/usertours/upgrade.txt b/admin/tool/usertours/upgrade.txt index 78c6749faad..f1c9f7445ce 100644 --- a/admin/tool/usertours/upgrade.txt +++ b/admin/tool/usertours/upgrade.txt @@ -1,5 +1,12 @@ This files describes API changes in the tool_usertours code. +=== 4.4 === +* New hooks have been provided to allow plugins to define their own server-side, and client-side user tour filters. + The new hooks are named: + - \tool_usertours\hook\before_serverside_filter_fetch + - \tool_usertours\hook\before_clientside_filter_fetch + These hooks allow addition, and removal, of filters. + === 4.0 === * The `tourconfig` property returned by the `tool_usertours_fetch_and_start_tour` external method has also an `endtourlabel` property that contains the label to be used