diff --git a/lib/amd/build/copy_to_clipboard.min.js b/lib/amd/build/copy_to_clipboard.min.js index 2f1807525bab5..d33f92937b58d 100644 --- a/lib/amd/build/copy_to_clipboard.min.js +++ b/lib/amd/build/copy_to_clipboard.min.js @@ -1,2 +1,2 @@ -define ("core/copy_to_clipboard",["core/str","core/toast","core/prefetch"],function(a,b,c){"use strict";var d=function(){document.addEventListener("click",function(a){var b=a.target.closest("[data-action=\"copytoclipboard\"]");if(!b){return}if(!b.dataset.clipboardTarget){return}var c=document.querySelector(b.dataset.clipboardTarget);if(!c){return}a.preventDefault();var d=j(c);if(!d){g();return}if(navigator.clipboard){navigator.clipboard.writeText(d).then(function(){return f(b)}).catch();return}if(c instanceof HTMLInputElement||c instanceof HTMLTextAreaElement){c.focus();if(e(b,c)){b.focus()}}else{var h=document.createElement("textarea");h.value=d;h.classList.add("sr-only");document.body.appendChild(h);e(b,h);h.remove();b.focus()}})},e=function(a,b){b.select();if(document.execCommand("copy")){f(a);return!0}g();return!1},f=function(a){return i(a).then(function(a){return(0,b.add)(a,{})})},g=function(){return h().then(function(a){return(0,b.add)(a,{})})},h=function(){return(0,a.get_string)("unabletocopytoclipboard","core")},i=function(b){if(b.dataset.clipboardSuccessMessage){return Promise.resolve(b.dataset.clipboardSuccessMessage)}return(0,a.get_string)("textcopiedtoclipboard","core")},j=function(a){if(a.value){return a.value}else if(a.innerText){return a.innerText}return null},k=!1;if(!k){(0,c.prefetchStrings)("core",["textcopiedtoclipboard","unabletocopytoclipboard"]);d();k=!0}}); +define ("core/copy_to_clipboard",["core/str","core/toast","core/prefetch"],function(a,b,c){"use strict";var d=function(){document.addEventListener("click",function(a){var b=a.target.closest("[data-action=\"copytoclipboard\"]");if(!b){return}if(!b.dataset.clipboardTarget){return}var c=document.querySelector(b.dataset.clipboardTarget);if(!c){return}a.preventDefault();var d=j(c);if(!d){g();return}if(navigator.clipboard){navigator.clipboard.writeText(d).then(function(){return f(b)}).catch();return}if(c instanceof HTMLInputElement||c instanceof HTMLTextAreaElement){c.focus();if(e(b,c)){b.focus()}}else{var h=document.createElement("textarea");h.value=d;h.classList.add("sr-only");document.body.appendChild(h);e(b,h);h.remove();b.focus()}})},e=function(a,b){b.select();if(document.execCommand("copy")){f(a);return!0}g();return!1},f=function(a){return i(a).then(function(a){return(0,b.add)(a,{})})},g=function(){return h().then(function(a){return(0,b.add)(a,{type:"warning"})})},h=function(){return(0,a.get_string)("unabletocopytoclipboard","core")},i=function(b){if(b.dataset.clipboardSuccessMessage){return Promise.resolve(b.dataset.clipboardSuccessMessage)}return(0,a.get_string)("textcopiedtoclipboard","core")},j=function(a){if(a.value){return a.value}else if(a.innerText){return a.innerText}return null},k=!1;if(!k){(0,c.prefetchStrings)("core",["textcopiedtoclipboard","unabletocopytoclipboard"]);d();k=!0}}); //# sourceMappingURL=copy_to_clipboard.min.js.map diff --git a/lib/amd/build/copy_to_clipboard.min.js.map b/lib/amd/build/copy_to_clipboard.min.js.map index fdbb716df77e8..80f6fd970377a 100644 --- a/lib/amd/build/copy_to_clipboard.min.js.map +++ b/lib/amd/build/copy_to_clipboard.min.js.map @@ -1 +1 @@ -{"version":3,"sources":["../src/copy_to_clipboard.js"],"names":["addEventListeners","document","addEventListener","e","copyButton","target","closest","dataset","clipboardTarget","copyTarget","querySelector","preventDefault","textToCopy","getTextFromContainer","displayFailureToast","navigator","clipboard","writeText","then","displaySuccessToast","catch","HTMLInputElement","HTMLTextAreaElement","focus","copyNodeContentToClipboard","copyRegion","createElement","value","classList","add","body","appendChild","remove","select","execCommand","getSuccessText","successMessage","getFailureText","message","clipboardSuccessMessage","Promise","resolve","container","innerText","loaded"],"mappings":"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","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 * A JavaScript module that enhances a button and text container to support copy-to-clipboard functionality.\n *\n * This module needs to be loaded by pages/templates/modules that require this functionality.\n *\n * To enable copy-to-clipboard functionality, we need a trigger element (usually a button) and a copy target element\n * (e.g. a div, span, text input, or text area).\n *\n * In the trigger element, we need to declare the data-action=\"copytoclipboard\" attribute and set the\n * data-clipboard-target attribute which is the CSS selector that points to the target element that contains the text\n * to be copied.\n *\n * When the text is successfully copied to the clipboard, a toast message that indicates that the copy operation was a success\n * will be shown. This success message can be customised by setting the data-clipboard-success-message attribute in the\n * trigger element.\n *\n * @module core/copy_to_clipboard\n * @copyright 2021 Jun Pataleta\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n *\n * @example Markup for the trigger and target elements\n * \n * \n */\nimport {get_string as getString} from 'core/str';\nimport {add as addToast} from 'core/toast';\nimport {prefetchStrings} from 'core/prefetch';\n\n/**\n * Add event listeners to trigger elements through event delegation.\n *\n * @private\n */\nconst addEventListeners = () => {\n document.addEventListener('click', e => {\n const copyButton = e.target.closest('[data-action=\"copytoclipboard\"]');\n if (!copyButton) {\n return;\n }\n\n if (!copyButton.dataset.clipboardTarget) {\n return;\n }\n\n const copyTarget = document.querySelector(copyButton.dataset.clipboardTarget);\n if (!copyTarget) {\n return;\n }\n\n // This is a copy target and there is content.\n // Prevent the default action.\n e.preventDefault();\n\n // We have a copy target - great. Let's copy its content.\n const textToCopy = getTextFromContainer(copyTarget);\n if (!textToCopy) {\n displayFailureToast();\n return;\n }\n\n if (navigator.clipboard) {\n navigator.clipboard.writeText(textToCopy)\n .then(() => displaySuccessToast(copyButton)).catch();\n\n return;\n }\n\n // The clipboard API is not available.\n // This may happen when the page is not served over SSL.\n // Try to fall back to document.execCommand() approach of copying the text.\n // WARNING: This is deprecated functionality that may get dropped at anytime by browsers.\n\n if (copyTarget instanceof HTMLInputElement || copyTarget instanceof HTMLTextAreaElement) {\n // Focus and select the text in the target element.\n // If the execCommand fails, at least the user can readily copy the text.\n copyTarget.focus();\n\n if (copyNodeContentToClipboard(copyButton, copyTarget)) {\n // If the copy was successful then focus back on the copy button.\n copyButton.focus();\n }\n } else {\n // This copyTarget is not an input, or text area so cannot be used with the execCommand('copy') command.\n // To work around this we create a new textarea and copy that.\n // This textarea must be part of the DOM and must be visible.\n // We (ab)use the sr-only tag to ensure that it is considered visible to the browser, whilst being\n // hidden from view by the user.\n const copyRegion = document.createElement('textarea');\n copyRegion.value = textToCopy;\n copyRegion.classList.add('sr-only');\n document.body.appendChild(copyRegion);\n\n copyNodeContentToClipboard(copyButton, copyRegion);\n\n // After copying, remove the temporary element and move focus back to the triggering button.\n copyRegion.remove();\n copyButton.focus();\n }\n });\n};\n\n/**\n * Copy the content of the selected element to the clipboard, and display a notifiction if successful.\n *\n * @param {HTMLElement} copyButton\n * @param {HTMLElement} copyTarget\n * @returns {boolean}\n * @private\n */\nconst copyNodeContentToClipboard = (copyButton, copyTarget) => {\n copyTarget.select();\n\n // Try to copy the text from the target element.\n if (document.execCommand('copy')) {\n displaySuccessToast(copyButton);\n return true;\n }\n\n displayFailureToast();\n return false;\n};\n\n/**\n * Displays a toast containing the success message.\n *\n * @param {HTMLElement} copyButton The element that copies the text from the container.\n * @returns {Promise}\n * @private\n */\nconst displaySuccessToast = copyButton => getSuccessText(copyButton)\n .then(successMessage => addToast(successMessage, {}));\n\n/**\n * Displays a toast containing the failure message.\n *\n * @returns {Promise}\n * @private\n */\nconst displayFailureToast = () => getFailureText()\n .then(message => addToast(message, {}));\n\n/**\n * Fetches the failure message to show to the user.\n *\n * @returns {Promise}\n * @private\n */\nconst getFailureText = () => getString('unabletocopytoclipboard', 'core');\n\n/**\n * Fetches the success message to show to the user.\n *\n * @param {HTMLElement} copyButton The element that copies the text from the container. This may contain the custom success message\n * via its data-clipboard-success-message attribute.\n * @returns {Promise|*}\n * @private\n */\nconst getSuccessText = copyButton => {\n if (copyButton.dataset.clipboardSuccessMessage) {\n return Promise.resolve(copyButton.dataset.clipboardSuccessMessage);\n }\n\n return getString('textcopiedtoclipboard', 'core');\n};\n\n/**\n * Fetches the text to be copied from the container.\n *\n * @param {HTMLElement} container The element containing the text to be copied.\n * @returns {null|string}\n * @private\n */\nconst getTextFromContainer = container => {\n if (container.value) {\n // For containers which are form elements (e.g. text area, text input), get the element's value.\n return container.value;\n } else if (container.innerText) {\n // For other elements, try to use the innerText attribute.\n return container.innerText;\n }\n\n return null;\n};\n\nlet loaded = false;\nif (!loaded) {\n prefetchStrings('core', [\n 'textcopiedtoclipboard',\n 'unabletocopytoclipboard',\n ]);\n\n // Add event listeners.\n addEventListeners();\n loaded = true;\n}\n"],"file":"copy_to_clipboard.min.js"} \ No newline at end of file +{"version":3,"sources":["../src/copy_to_clipboard.js"],"names":["addEventListeners","document","addEventListener","e","copyButton","target","closest","dataset","clipboardTarget","copyTarget","querySelector","preventDefault","textToCopy","getTextFromContainer","displayFailureToast","navigator","clipboard","writeText","then","displaySuccessToast","catch","HTMLInputElement","HTMLTextAreaElement","focus","copyNodeContentToClipboard","copyRegion","createElement","value","classList","add","body","appendChild","remove","select","execCommand","getSuccessText","successMessage","getFailureText","message","type","clipboardSuccessMessage","Promise","resolve","container","innerText","loaded"],"mappings":"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","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 * A JavaScript module that enhances a button and text container to support copy-to-clipboard functionality.\n *\n * This module needs to be loaded by pages/templates/modules that require this functionality.\n *\n * To enable copy-to-clipboard functionality, we need a trigger element (usually a button) and a copy target element\n * (e.g. a div, span, text input, or text area).\n *\n * In the trigger element, we need to declare the data-action=\"copytoclipboard\" attribute and set the\n * data-clipboard-target attribute which is the CSS selector that points to the target element that contains the text\n * to be copied.\n *\n * When the text is successfully copied to the clipboard, a toast message that indicates that the copy operation was a success\n * will be shown. This success message can be customised by setting the data-clipboard-success-message attribute in the\n * trigger element.\n *\n * @module core/copy_to_clipboard\n * @copyright 2021 Jun Pataleta\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n *\n * @example Markup for the trigger and target elements\n * \n * \n */\nimport {get_string as getString} from 'core/str';\nimport {add as addToast} from 'core/toast';\nimport {prefetchStrings} from 'core/prefetch';\n\n/**\n * Add event listeners to trigger elements through event delegation.\n *\n * @private\n */\nconst addEventListeners = () => {\n document.addEventListener('click', e => {\n const copyButton = e.target.closest('[data-action=\"copytoclipboard\"]');\n if (!copyButton) {\n return;\n }\n\n if (!copyButton.dataset.clipboardTarget) {\n return;\n }\n\n const copyTarget = document.querySelector(copyButton.dataset.clipboardTarget);\n if (!copyTarget) {\n return;\n }\n\n // This is a copy target and there is content.\n // Prevent the default action.\n e.preventDefault();\n\n // We have a copy target - great. Let's copy its content.\n const textToCopy = getTextFromContainer(copyTarget);\n if (!textToCopy) {\n displayFailureToast();\n return;\n }\n\n if (navigator.clipboard) {\n navigator.clipboard.writeText(textToCopy)\n .then(() => displaySuccessToast(copyButton)).catch();\n\n return;\n }\n\n // The clipboard API is not available.\n // This may happen when the page is not served over SSL.\n // Try to fall back to document.execCommand() approach of copying the text.\n // WARNING: This is deprecated functionality that may get dropped at anytime by browsers.\n\n if (copyTarget instanceof HTMLInputElement || copyTarget instanceof HTMLTextAreaElement) {\n // Focus and select the text in the target element.\n // If the execCommand fails, at least the user can readily copy the text.\n copyTarget.focus();\n\n if (copyNodeContentToClipboard(copyButton, copyTarget)) {\n // If the copy was successful then focus back on the copy button.\n copyButton.focus();\n }\n } else {\n // This copyTarget is not an input, or text area so cannot be used with the execCommand('copy') command.\n // To work around this we create a new textarea and copy that.\n // This textarea must be part of the DOM and must be visible.\n // We (ab)use the sr-only tag to ensure that it is considered visible to the browser, whilst being\n // hidden from view by the user.\n const copyRegion = document.createElement('textarea');\n copyRegion.value = textToCopy;\n copyRegion.classList.add('sr-only');\n document.body.appendChild(copyRegion);\n\n copyNodeContentToClipboard(copyButton, copyRegion);\n\n // After copying, remove the temporary element and move focus back to the triggering button.\n copyRegion.remove();\n copyButton.focus();\n }\n });\n};\n\n/**\n * Copy the content of the selected element to the clipboard, and display a notifiction if successful.\n *\n * @param {HTMLElement} copyButton\n * @param {HTMLElement} copyTarget\n * @returns {boolean}\n * @private\n */\nconst copyNodeContentToClipboard = (copyButton, copyTarget) => {\n copyTarget.select();\n\n // Try to copy the text from the target element.\n if (document.execCommand('copy')) {\n displaySuccessToast(copyButton);\n return true;\n }\n\n displayFailureToast();\n return false;\n};\n\n/**\n * Displays a toast containing the success message.\n *\n * @param {HTMLElement} copyButton The element that copies the text from the container.\n * @returns {Promise}\n * @private\n */\nconst displaySuccessToast = copyButton => getSuccessText(copyButton)\n .then(successMessage => addToast(successMessage, {}));\n\n/**\n * Displays a toast containing the failure message.\n *\n * @returns {Promise}\n * @private\n */\nconst displayFailureToast = () => getFailureText()\n .then(message => addToast(message, {type: 'warning'}));\n\n/**\n * Fetches the failure message to show to the user.\n *\n * @returns {Promise}\n * @private\n */\nconst getFailureText = () => getString('unabletocopytoclipboard', 'core');\n\n/**\n * Fetches the success message to show to the user.\n *\n * @param {HTMLElement} copyButton The element that copies the text from the container. This may contain the custom success message\n * via its data-clipboard-success-message attribute.\n * @returns {Promise|*}\n * @private\n */\nconst getSuccessText = copyButton => {\n if (copyButton.dataset.clipboardSuccessMessage) {\n return Promise.resolve(copyButton.dataset.clipboardSuccessMessage);\n }\n\n return getString('textcopiedtoclipboard', 'core');\n};\n\n/**\n * Fetches the text to be copied from the container.\n *\n * @param {HTMLElement} container The element containing the text to be copied.\n * @returns {null|string}\n * @private\n */\nconst getTextFromContainer = container => {\n if (container.value) {\n // For containers which are form elements (e.g. text area, text input), get the element's value.\n return container.value;\n } else if (container.innerText) {\n // For other elements, try to use the innerText attribute.\n return container.innerText;\n }\n\n return null;\n};\n\nlet loaded = false;\nif (!loaded) {\n prefetchStrings('core', [\n 'textcopiedtoclipboard',\n 'unabletocopytoclipboard',\n ]);\n\n // Add event listeners.\n addEventListeners();\n loaded = true;\n}\n"],"file":"copy_to_clipboard.min.js"} \ No newline at end of file diff --git a/lib/amd/build/toast.min.js b/lib/amd/build/toast.min.js index 671212adcdce8..b2c63b3a4c21f 100644 --- a/lib/amd/build/toast.min.js +++ b/lib/amd/build/toast.min.js @@ -1,2 +1,2 @@ -define ("core/toast",["exports","core/templates","core/notification","core/pending"],function(a,b,c,d){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.add=a.addToastRegion=void 0;b=e(b);c=e(c);d=e(d);function e(a){return a&&a.__esModule?a:{default:a}}function f(a,b){var c=Object.keys(a);if(Object.getOwnPropertySymbols){var d=Object.getOwnPropertySymbols(a);if(b)d=d.filter(function(b){return Object.getOwnPropertyDescriptor(a,b).enumerable});c.push.apply(c,d)}return c}function g(a){for(var b=1,c;b.\n\n/**\n * A system for displaying small snackbar notifications to users which disappear shortly after they are shown.\n *\n * @module core/toast\n * @copyright 2019 Andrew Nicols \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nimport Templates from 'core/templates';\nimport Notification from 'core/notification';\nimport Pending from 'core/pending';\n\n/**\n * Add a new region to place toasts in, taking in a parent element.\n *\n * @method\n * @param {HTMLElement} parent\n */\nexport const addToastRegion = async(parent) => {\n const pendingPromise = new Pending('addToastRegion');\n\n try {\n const {html, js} = await Templates.renderForPromise('core/local/toast/wrapper', {});\n Templates.prependNodeContents(parent, html, js);\n } catch (e) {\n Notification.exception(e);\n }\n\n pendingPromise.resolve();\n};\n\n/**\n * Add a new toast or snackbar notification to the page.\n *\n * @method\n * @param {String} message\n * @param {Object} configuration\n * @param {String} [configuration.title]\n * @param {String} [configuration.subtitle]\n * @param {String} [configuration.type] Optional type of the toast notification ('success', 'info', 'warning' or 'danger')\n * @param {Boolean} [configuration.autohide=true]\n * @param {Boolean} [configuration.closeButton=false]\n * @param {Number} [configuration.delay=4000]\n *\n * @example\n * import {add as addToast} from 'core/toast';\n * import {get_string as getString} from 'core/str';\n *\n * getString('example', 'mod_myexample')\n * .then(str => {\n * addToast(str, {\n * type: 'warning',\n * autohide: false,\n * closeButton: true,\n * });\n * return;\n * })\n * .catch();\n */\nexport const add = async(message, configuration) => {\n const pendingPromise = new Pending('addToastRegion');\n configuration = {\n closeButton: false,\n autohide: true,\n delay: 4000,\n ...configuration,\n };\n\n const templateName = `core/local/toast/message`;\n try {\n const targetNode = await getTargetNode();\n const {html, js} = await Templates.renderForPromise(templateName, {\n message,\n ...configuration\n });\n Templates.prependNodeContents(targetNode, html, js);\n } catch (e) {\n Notification.exception(e);\n }\n\n pendingPromise.resolve();\n};\n\nconst getTargetNode = async() => {\n const regions = document.querySelectorAll('.toast-wrapper');\n\n if (regions.length) {\n return regions[regions.length - 1];\n }\n\n await addToastRegion(document.body, 'fixed-bottom');\n return getTargetNode();\n};\n"],"file":"toast.min.js"} \ No newline at end of file +{"version":3,"sources":["../src/toast.js"],"names":["addToastRegion","parent","pendingPromise","Pending","Templates","renderForPromise","html","js","prependNodeContents","Notification","exception","resolve","add","message","configuration","type","closeButton","autohide","delay","templateName","getTargetNode","targetNode","regions","document","querySelectorAll","length","body"],"mappings":"mMAsBA,OACA,OACA,O,6jCAQO,GAAMA,CAAAA,CAAc,4CAAG,WAAMC,CAAN,+FACpBC,CADoB,CACH,GAAIC,UAAJ,CAAY,gBAAZ,CADG,yBAIGC,WAAUC,gBAAV,CAA2B,0BAA3B,CAAuD,EAAvD,CAJH,iBAIfC,CAJe,GAIfA,IAJe,CAITC,CAJS,GAITA,EAJS,CAKtBH,UAAUI,mBAAV,CAA8BP,CAA9B,CAAsCK,CAAtC,CAA4CC,CAA5C,EALsB,qDAOtBE,UAAaC,SAAb,OAPsB,QAU1BR,CAAc,CAACS,OAAf,GAV0B,uDAAH,uDAApB,C,mBAyCA,GAAMC,CAAAA,CAAG,4CAAG,WAAMC,CAAN,CAAeC,CAAf,mGACTZ,CADS,CACQ,GAAIC,UAAJ,CAAY,gBAAZ,CADR,CAEfW,CAAa,IACTC,IAAI,CAAE,MADG,CAETC,WAAW,GAFF,CAGTC,QAAQ,GAHC,CAITC,KAAK,CAAE,GAJE,EAKNJ,CALM,CAAb,CAQMK,CAVS,oDAYcC,CAAAA,CAAa,EAZ3B,QAYLC,CAZK,uBAacjB,WAAUC,gBAAV,CAA2Bc,CAA3B,IACrBN,OAAO,CAAPA,CADqB,EAElBC,CAFkB,EAbd,iBAaJR,CAbI,GAaJA,IAbI,CAaEC,CAbF,GAaEA,EAbF,CAiBXH,UAAUI,mBAAV,CAA8Ba,CAA9B,CAA0Cf,CAA1C,CAAgDC,CAAhD,EAjBW,qDAmBXE,UAAaC,SAAb,OAnBW,QAsBfR,CAAc,CAACS,OAAf,GAtBe,uDAAH,uDAAT,C,QAyBP,GAAMS,CAAAA,CAAa,4CAAG,oGACZE,CADY,CACFC,QAAQ,CAACC,gBAAT,CAA0B,gBAA1B,CADE,KAGdF,CAAO,CAACG,MAHM,0CAIPH,CAAO,CAACA,CAAO,CAACG,MAAR,CAAiB,CAAlB,CAJA,wBAOZzB,CAAAA,CAAc,CAACuB,QAAQ,CAACG,IAAV,CAAgB,cAAhB,CAPF,iCAQXN,CAAa,EARF,0CAAH,uD","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 * A system for displaying small snackbar notifications to users which disappear shortly after they are shown.\n *\n * @module core/toast\n * @copyright 2019 Andrew Nicols \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nimport Templates from 'core/templates';\nimport Notification from 'core/notification';\nimport Pending from 'core/pending';\n\n/**\n * Add a new region to place toasts in, taking in a parent element.\n *\n * @method\n * @param {HTMLElement} parent\n */\nexport const addToastRegion = async(parent) => {\n const pendingPromise = new Pending('addToastRegion');\n\n try {\n const {html, js} = await Templates.renderForPromise('core/local/toast/wrapper', {});\n Templates.prependNodeContents(parent, html, js);\n } catch (e) {\n Notification.exception(e);\n }\n\n pendingPromise.resolve();\n};\n\n/**\n * Add a new toast or snackbar notification to the page.\n *\n * @method\n * @param {String} message\n * @param {Object} configuration\n * @param {String} [configuration.title]\n * @param {String} [configuration.subtitle]\n * @param {String} [configuration.type=info] Optional type of the toast notification ('success', 'info', 'warning' or 'danger')\n * @param {Boolean} [configuration.autohide=true]\n * @param {Boolean} [configuration.closeButton=false]\n * @param {Number} [configuration.delay=4000]\n *\n * @example\n * import {add as addToast} from 'core/toast';\n * import {get_string as getString} from 'core/str';\n *\n * getString('example', 'mod_myexample')\n * .then(str => {\n * addToast(str, {\n * type: 'warning',\n * autohide: false,\n * closeButton: true,\n * });\n * return;\n * })\n * .catch();\n */\nexport const add = async(message, configuration) => {\n const pendingPromise = new Pending('addToastRegion');\n configuration = {\n type: 'info',\n closeButton: false,\n autohide: true,\n delay: 4000,\n ...configuration,\n };\n\n const templateName = `core/local/toast/message`;\n try {\n const targetNode = await getTargetNode();\n const {html, js} = await Templates.renderForPromise(templateName, {\n message,\n ...configuration\n });\n Templates.prependNodeContents(targetNode, html, js);\n } catch (e) {\n Notification.exception(e);\n }\n\n pendingPromise.resolve();\n};\n\nconst getTargetNode = async() => {\n const regions = document.querySelectorAll('.toast-wrapper');\n\n if (regions.length) {\n return regions[regions.length - 1];\n }\n\n await addToastRegion(document.body, 'fixed-bottom');\n return getTargetNode();\n};\n"],"file":"toast.min.js"} \ No newline at end of file diff --git a/lib/amd/src/copy_to_clipboard.js b/lib/amd/src/copy_to_clipboard.js index 46aa3b9b01667..10c44946ad997 100644 --- a/lib/amd/src/copy_to_clipboard.js +++ b/lib/amd/src/copy_to_clipboard.js @@ -155,7 +155,7 @@ const displaySuccessToast = copyButton => getSuccessText(copyButton) * @private */ const displayFailureToast = () => getFailureText() - .then(message => addToast(message, {})); + .then(message => addToast(message, {type: 'warning'})); /** * Fetches the failure message to show to the user. diff --git a/lib/amd/src/toast.js b/lib/amd/src/toast.js index 0f7106977f7d3..70d16fb34f995 100644 --- a/lib/amd/src/toast.js +++ b/lib/amd/src/toast.js @@ -51,7 +51,7 @@ export const addToastRegion = async(parent) => { * @param {Object} configuration * @param {String} [configuration.title] * @param {String} [configuration.subtitle] - * @param {String} [configuration.type] Optional type of the toast notification ('success', 'info', 'warning' or 'danger') + * @param {String} [configuration.type=info] Optional type of the toast notification ('success', 'info', 'warning' or 'danger') * @param {Boolean} [configuration.autohide=true] * @param {Boolean} [configuration.closeButton=false] * @param {Number} [configuration.delay=4000] @@ -74,6 +74,7 @@ export const addToastRegion = async(parent) => { export const add = async(message, configuration) => { const pendingPromise = new Pending('addToastRegion'); configuration = { + type: 'info', closeButton: false, autohide: true, delay: 4000, diff --git a/lib/upgrade.txt b/lib/upgrade.txt index 32eceb15a20ba..5f84770574dfd 100644 --- a/lib/upgrade.txt +++ b/lib/upgrade.txt @@ -87,6 +87,7 @@ information provided here is intended especially for developers. The deprecated codes are removed from the questionlib for those two methods. * The postgres driver now wraps calls to pg_field_type() and caches them in databasemeta to save an invisible internal DB call on every request. +* The default type of 'core/toast' messages has been changed to 'information' (callers can still explicitely set the type) * As the message_jabber notification plugin has been moved to the plugins database, the XMPPHP library (aka Jabber) has been completely removed from Moodle core too. * The SWF media player has been completely removed (The Flash Player was deprecated in 2017 and officially discontinued diff --git a/mod/forum/amd/build/local/grades/grader.min.js b/mod/forum/amd/build/local/grades/grader.min.js index 1fc156f2d382d..be249f3dc067e 100644 --- a/mod/forum/amd/build/local/grades/grader.min.js +++ b/mod/forum/amd/build/local/grades/grader.min.js @@ -1,2 +1,2 @@ -function _typeof(a){"@babel/helpers - typeof";if("function"==typeof Symbol&&"symbol"==typeof Symbol.iterator){_typeof=function(a){return typeof a}}else{_typeof=function(a){return a&&"function"==typeof Symbol&&a.constructor===Symbol&&a!==Symbol.prototype?"symbol":typeof a}}return _typeof(a)}define ("mod_forum/local/grades/grader",["exports","core/templates","./local/grader/selectors","./local/grader/user_picker","mod_forum/local/layout/fullscreen","./local/grader/gradingpanel","core/toast","core/notification","core/str","core_grades/grades/grader/gradingpanel/normalise","core/loadingicon","core/utils","core_grades/grades/grader/gradingpanel/comparison","core/modal_factory","core/modal_events","core/pubsub","core/drawer_events"],function(a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q){"use strict";Object.defineProperty(a,"__esModule",{value:!0});Object.defineProperty(a,"getGradingPanelFunctions",{enumerable:!0,get:function get(){return f.default}});a.view=a.launch=void 0;b=t(b);c=t(c);d=t(d);f=t(f);n=s(n);o=s(o);q=t(q);function r(){if("function"!=typeof WeakMap)return null;var a=new WeakMap;r=function(){return a};return a}function s(a){if(a&&a.__esModule){return a}if(null===a||"object"!==_typeof(a)&&"function"!=typeof a){return{default:a}}var b=r();if(b&&b.has(a)){return b.get(a)}var c={},d=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var e in a){if(Object.prototype.hasOwnProperty.call(a,e)){var f=d?Object.getOwnPropertyDescriptor(a,e):null;if(f&&(f.get||f.set)){Object.defineProperty(c,e,f)}else{c[e]=a[e]}}}c.default=a;if(b){b.set(a,c)}return c}function t(a){return a&&a.__esModule?a:{default:a}}function u(a,b){var c=Object.keys(a);if(Object.getOwnPropertySymbols){var d=Object.getOwnPropertySymbols(a);if(b)d=d.filter(function(b){return Object.getOwnPropertyDescriptor(a,b).enumerable});c.push.apply(c,d)}return c}function v(a){for(var b=1,c;ba.length)b=a.length;for(var c=0,d=Array(b);ca.length)b=a.length;for(var c=0,d=Array(b);c.\n\n/**\n * This module will tie together all of the different calls the gradable module will make.\n *\n * @module mod_forum/local/grades/grader\n * @copyright 2019 Mathew May \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nimport Templates from 'core/templates';\nimport Selectors from './local/grader/selectors';\nimport getUserPicker from './local/grader/user_picker';\nimport {createLayout as createFullScreenWindow} from 'mod_forum/local/layout/fullscreen';\nimport getGradingPanelFunctions from './local/grader/gradingpanel';\nimport {add as addToast} from 'core/toast';\nimport {addNotification} from 'core/notification';\nimport {get_string as getString} from 'core/str';\nimport {failedUpdate} from 'core_grades/grades/grader/gradingpanel/normalise';\nimport {addIconToContainerWithPromise} from 'core/loadingicon';\nimport {debounce} from 'core/utils';\nimport {fillInitialValues} from 'core_grades/grades/grader/gradingpanel/comparison';\nimport * as Modal from 'core/modal_factory';\nimport * as ModalEvents from 'core/modal_events';\nimport {subscribe} from 'core/pubsub';\nimport DrawerEvents from 'core/drawer_events';\n\nconst templateNames = {\n grader: {\n app: 'mod_forum/local/grades/grader',\n gradingPanel: {\n error: 'mod_forum/local/grades/local/grader/gradingpanel/error',\n },\n searchResults: 'mod_forum/local/grades/local/grader/user_picker/user_search',\n status: 'mod_forum/local/grades/local/grader/status',\n },\n};\n\n/**\n * Helper function that replaces the user picker placeholder with what we get back from the user picker class.\n *\n * @param {HTMLElement} root\n * @param {String} html\n */\nconst displayUserPicker = (root, html) => {\n const pickerRegion = root.querySelector(Selectors.regions.pickerRegion);\n Templates.replaceNodeContents(pickerRegion, html, '');\n};\n\n/**\n * To be removed, this is now done as a part of Templates.renderForPromise()\n *\n * @param {String} html\n * @param {String} js\n * @returns {array} An array containing the HTML, and JS.\n */\nconst fetchContentFromRender = (html, js) => {\n return [html, js];\n};\n\n/**\n * Here we build the function that is passed to the user picker that'll handle updating the user content area\n * of the grading interface.\n *\n * @param {HTMLElement} root\n * @param {Function} getContentForUser\n * @param {Function} getGradeForUser\n * @param {Function} saveGradeForUser\n * @return {Function}\n */\nconst getUpdateUserContentFunction = (root, getContentForUser, getGradeForUser, saveGradeForUser) => {\n let firstLoad = true;\n\n return async(user) => {\n const spinner = firstLoad ? null : addIconToContainerWithPromise(root);\n const [\n [html, js],\n userGrade,\n ] = await Promise.all([\n getContentForUser(user.id).then(fetchContentFromRender),\n getGradeForUser(user.id),\n ]);\n Templates.replaceNodeContents(root.querySelector(Selectors.regions.moduleReplace), html, js);\n\n const [\n gradingPanelHtml,\n gradingPanelJS\n ] = await Templates.render(userGrade.templatename, userGrade.grade).then(fetchContentFromRender);\n const panelContainer = root.querySelector(Selectors.regions.gradingPanelContainer);\n const panel = panelContainer.querySelector(Selectors.regions.gradingPanel);\n Templates.replaceNodeContents(panel, gradingPanelHtml, gradingPanelJS);\n\n const form = panel.querySelector('form');\n fillInitialValues(form);\n\n form.addEventListener('submit', event => {\n saveGradeForUser(user);\n event.preventDefault();\n });\n\n panelContainer.scrollTop = 0;\n firstLoad = false;\n\n if (spinner) {\n spinner.resolve();\n }\n return userGrade;\n };\n};\n\n/**\n * Show the search results container and hide the user picker and body content.\n *\n * @param {HTMLElement} bodyContainer The container element for the body content\n * @param {HTMLElement} userPickerContainer The container element for the user picker\n * @param {HTMLElement} searchResultsContainer The container element for the search results\n */\nconst showSearchResultContainer = (bodyContainer, userPickerContainer, searchResultsContainer) => {\n bodyContainer.classList.add('hidden');\n userPickerContainer.classList.add('hidden');\n searchResultsContainer.classList.remove('hidden');\n};\n\n/**\n * Hide the search results container and show the user picker and body content.\n *\n * @param {HTMLElement} bodyContainer The container element for the body content\n * @param {HTMLElement} userPickerContainer The container element for the user picker\n * @param {HTMLElement} searchResultsContainer The container element for the search results\n */\nconst hideSearchResultContainer = (bodyContainer, userPickerContainer, searchResultsContainer) => {\n bodyContainer.classList.remove('hidden');\n userPickerContainer.classList.remove('hidden');\n searchResultsContainer.classList.add('hidden');\n};\n\n/**\n * Toggles the visibility of the user search.\n *\n * @param {HTMLElement} toggleSearchButton The button that toggles the search\n * @param {HTMLElement} searchContainer The container element for the user search\n * @param {HTMLElement} searchInput The input element for searching\n */\nconst showUserSearchInput = (toggleSearchButton, searchContainer, searchInput) => {\n searchContainer.classList.remove('collapsed');\n toggleSearchButton.setAttribute('aria-expanded', 'true');\n toggleSearchButton.classList.add('expand');\n toggleSearchButton.classList.remove('collapse');\n\n // Hide the grading info container from screen reader.\n const gradingInfoContainer = searchContainer.parentElement.querySelector(Selectors.regions.gradingInfoContainer);\n gradingInfoContainer.setAttribute('aria-hidden', 'true');\n\n // Hide the collapse grading drawer button from screen reader.\n const collapseGradingDrawer = searchContainer.parentElement.querySelector(Selectors.buttons.collapseGradingDrawer);\n collapseGradingDrawer.setAttribute('aria-hidden', 'true');\n collapseGradingDrawer.setAttribute('tabindex', '-1');\n\n searchInput.focus();\n};\n\n/**\n * Toggles the visibility of the user search.\n *\n * @param {HTMLElement} toggleSearchButton The button that toggles the search\n * @param {HTMLElement} searchContainer The container element for the user search\n * @param {HTMLElement} searchInput The input element for searching\n */\nconst hideUserSearchInput = (toggleSearchButton, searchContainer, searchInput) => {\n searchContainer.classList.add('collapsed');\n toggleSearchButton.setAttribute('aria-expanded', 'false');\n toggleSearchButton.classList.add('collapse');\n toggleSearchButton.classList.remove('expand');\n toggleSearchButton.focus();\n\n // Show the grading info container to screen reader.\n const gradingInfoContainer = searchContainer.parentElement.querySelector(Selectors.regions.gradingInfoContainer);\n gradingInfoContainer.removeAttribute('aria-hidden');\n\n // Show the collapse grading drawer button from screen reader.\n const collapseGradingDrawer = searchContainer.parentElement.querySelector(Selectors.buttons.collapseGradingDrawer);\n collapseGradingDrawer.removeAttribute('aria-hidden');\n collapseGradingDrawer.setAttribute('tabindex', '0');\n\n searchInput.value = '';\n};\n\n/**\n * Find the list of users who's names include the given search term.\n *\n * @param {Array} userList List of users for the grader\n * @param {String} searchTerm The search term to match\n * @return {Array}\n */\nconst searchForUsers = (userList, searchTerm) => {\n if (searchTerm === '') {\n return userList;\n }\n\n searchTerm = searchTerm.toLowerCase();\n\n return userList.filter((user) => {\n return user.fullname.toLowerCase().includes(searchTerm);\n });\n};\n\n/**\n * Render the list of users in the search results area.\n *\n * @param {HTMLElement} searchResultsContainer The container element for search results\n * @param {Array} users The list of users to display\n */\nconst renderSearchResults = async(searchResultsContainer, users) => {\n const {html, js} = await Templates.renderForPromise(templateNames.grader.searchResults, {users});\n Templates.replaceNodeContents(searchResultsContainer, html, js);\n};\n\n/**\n * Add click handlers to the buttons in the header of the grading interface.\n *\n * @param {HTMLElement} graderLayout\n * @param {Object} userPicker\n * @param {Function} saveGradeFunction\n * @param {Array} userList List of users for the grader.\n */\nconst registerEventListeners = (graderLayout, userPicker, saveGradeFunction, userList) => {\n const graderContainer = graderLayout.getContainer();\n const toggleSearchButton = graderContainer.querySelector(Selectors.buttons.toggleSearch);\n const searchInputContainer = graderContainer.querySelector(Selectors.regions.userSearchContainer);\n const searchInput = searchInputContainer.querySelector(Selectors.regions.userSearchInput);\n const bodyContainer = graderContainer.querySelector(Selectors.regions.bodyContainer);\n const userPickerContainer = graderContainer.querySelector(Selectors.regions.pickerRegion);\n const searchResultsContainer = graderContainer.querySelector(Selectors.regions.searchResultsContainer);\n\n graderContainer.addEventListener('click', (e) => {\n if (e.target.closest(Selectors.buttons.toggleFullscreen)) {\n e.stopImmediatePropagation();\n e.preventDefault();\n graderLayout.toggleFullscreen();\n\n return;\n }\n\n if (e.target.closest(Selectors.buttons.closeGrader)) {\n e.stopImmediatePropagation();\n e.preventDefault();\n\n graderLayout.close();\n\n return;\n }\n\n if (e.target.closest(Selectors.buttons.saveGrade)) {\n saveGradeFunction(userPicker.currentUser);\n }\n\n if (e.target.closest(Selectors.buttons.toggleSearch)) {\n if (toggleSearchButton.getAttribute('aria-expanded') === 'true') {\n // Search is open so let's close it.\n hideUserSearchInput(toggleSearchButton, searchInputContainer, searchInput);\n hideSearchResultContainer(bodyContainer, userPickerContainer, searchResultsContainer);\n searchResultsContainer.innerHTML = '';\n } else {\n // Search is closed so let's open it.\n showUserSearchInput(toggleSearchButton, searchInputContainer, searchInput);\n showSearchResultContainer(bodyContainer, userPickerContainer, searchResultsContainer);\n renderSearchResults(searchResultsContainer, userList);\n }\n\n return;\n }\n\n const selectUserButton = e.target.closest(Selectors.buttons.selectUser);\n if (selectUserButton) {\n const userId = selectUserButton.getAttribute('data-userid');\n const user = userList.find(user => user.id == userId);\n userPicker.setUserId(userId);\n userPicker.showUser(user);\n hideUserSearchInput(toggleSearchButton, searchInputContainer, searchInput);\n hideSearchResultContainer(bodyContainer, userPickerContainer, searchResultsContainer);\n searchResultsContainer.innerHTML = '';\n }\n });\n\n // Debounce the search input so that it only executes 300 milliseconds after the user has finished typing.\n searchInput.addEventListener('input', debounce(() => {\n const users = searchForUsers(userList, searchInput.value);\n renderSearchResults(searchResultsContainer, users);\n }, 300));\n\n // Remove the right margin of the content container when the grading panel is hidden so that it expands to full-width.\n subscribe(DrawerEvents.DRAWER_HIDDEN, (drawerRoot) => {\n const gradingPanel = drawerRoot[0];\n if (gradingPanel.querySelector(Selectors.regions.gradingPanel)) {\n setContentContainerMargin(graderContainer, 0);\n }\n });\n\n // Bring back the right margin of the content container when the grading panel is shown to give space for the grading panel.\n subscribe(DrawerEvents.DRAWER_SHOWN, (drawerRoot) => {\n const gradingPanel = drawerRoot[0];\n if (gradingPanel.querySelector(Selectors.regions.gradingPanel)) {\n setContentContainerMargin(graderContainer, gradingPanel.offsetWidth);\n }\n });\n};\n\n/**\n * Adjusts the right margin of the content container.\n *\n * @param {HTMLElement} graderContainer The container for the grader app.\n * @param {Number} rightMargin The right margin value.\n */\nconst setContentContainerMargin = (graderContainer, rightMargin) => {\n const contentContainer = graderContainer.querySelector(Selectors.regions.moduleContainer);\n if (contentContainer) {\n contentContainer.style.marginRight = `${rightMargin}px`;\n }\n};\n\n/**\n * Get the function used to save a user grade.\n *\n * @param {HTMLElement} root The container for the grader\n * @param {Function} setGradeForUser The function that will be called.\n * @return {Function}\n */\nconst getSaveUserGradeFunction = (root, setGradeForUser) => {\n return async(user) => {\n try {\n root.querySelector(Selectors.regions.gradingPanelErrors).innerHTML = '';\n const result = await setGradeForUser(\n user.id,\n root.querySelector(Selectors.values.sendStudentNotifications).value,\n root.querySelector(Selectors.regions.gradingPanel)\n );\n if (result.success) {\n addToast(await getString('grades:gradesavedfor', 'mod_forum', user));\n }\n if (result.failed) {\n displayGradingError(root, user, result.error);\n }\n\n return result;\n } catch (err) {\n displayGradingError(root, user, err);\n\n return failedUpdate(err);\n }\n };\n};\n\n/**\n * Display a grading error, typically from a failed save.\n *\n * @param {HTMLElement} root The container for the grader\n * @param {Object} user The user who was errored\n * @param {Object} err The details of the error\n */\nconst displayGradingError = async(root, user, err) => {\n const [\n {html, js},\n errorString\n ] = await Promise.all([\n Templates.renderForPromise(templateNames.grader.gradingPanel.error, {error: err}),\n await getString('grades:gradesavefailed', 'mod_forum', {error: err.message, ...user}),\n ]);\n\n Templates.replaceNodeContents(root.querySelector(Selectors.regions.gradingPanelErrors), html, js);\n addToast(errorString);\n};\n\n/**\n * Launch the grader interface with the specified parameters.\n *\n * @param {Function} getListOfUsers A function to get the list of users\n * @param {Function} getContentForUser A function to get the content for a specific user\n * @param {Function} getGradeForUser A function get the grade details for a specific user\n * @param {Function} setGradeForUser A function to set the grade for a specific user\n * @param {Object} Preferences for the launch function\n */\nexport const launch = async(getListOfUsers, getContentForUser, getGradeForUser, setGradeForUser, {\n initialUserId = null,\n moduleName,\n courseName,\n courseUrl,\n sendStudentNotifications,\n focusOnClose = null,\n} = {}) => {\n\n // We need all of these functions to be executed in series, if one step runs before another the interface\n // will not work.\n\n // We need this promise to resolve separately so that we can avoid loading the whole interface if there are no users.\n const userList = await getListOfUsers();\n if (!userList.length) {\n addNotification({\n message: await getString('nouserstograde', 'core_grades'),\n type: \"error\",\n });\n return;\n }\n\n // Now that we have confirmed there are at least some users let's boot up the grader interface.\n const [\n graderLayout,\n {html, js},\n ] = await Promise.all([\n createFullScreenWindow({\n fullscreen: false,\n showLoader: false,\n focusOnClose,\n }),\n Templates.renderForPromise(templateNames.grader.app, {\n moduleName,\n courseName,\n courseUrl,\n drawer: {show: true},\n defaultsendnotifications: sendStudentNotifications,\n }),\n ]);\n\n const graderContainer = graderLayout.getContainer();\n\n const saveGradeFunction = getSaveUserGradeFunction(graderContainer, setGradeForUser);\n\n Templates.replaceNodeContents(graderContainer, html, js);\n const updateUserContent = getUpdateUserContentFunction(graderContainer, getContentForUser, getGradeForUser, saveGradeFunction);\n\n const userIds = userList.map(user => user.id);\n const statusContainer = graderContainer.querySelector(Selectors.regions.statusContainer);\n // Fetch the userpicker for display.\n const userPicker = await getUserPicker(\n userList,\n async(user) => {\n const userGrade = await updateUserContent(user);\n const renderContext = {\n status: userGrade.hasgrade,\n index: userIds.indexOf(user.id) + 1,\n total: userList.length\n };\n Templates.render(templateNames.grader.status, renderContext).then(html => {\n statusContainer.innerHTML = html;\n return html;\n }).catch();\n },\n saveGradeFunction,\n {\n initialUserId,\n },\n );\n\n // Register all event listeners.\n registerEventListeners(graderLayout, userPicker, saveGradeFunction, userList);\n\n // Display the newly created user picker.\n displayUserPicker(graderContainer, userPicker.rootNode);\n};\n\n/**\n * Show the grade for a specific user.\n *\n * @param {Function} getGradeForUser A function get the grade details for a specific user\n * @param {Number} userid The ID of a specific user\n * @param {String} moduleName the name of the module\n */\nexport const view = async(getGradeForUser, userid, moduleName, {\n focusOnClose = null,\n} = {}) => {\n\n const [\n userGrade,\n modal,\n ] = await Promise.all([\n getGradeForUser(userid),\n Modal.create({\n title: moduleName,\n large: true,\n type: Modal.types.CANCEL\n }),\n ]);\n\n const spinner = addIconToContainerWithPromise(modal.getRoot());\n\n // Handle hidden event.\n modal.getRoot().on(ModalEvents.hidden, function() {\n // Destroy when hidden.\n modal.destroy();\n if (focusOnClose) {\n try {\n focusOnClose.focus();\n } catch (e) {\n // eslint-disable-line\n }\n }\n });\n\n modal.show();\n const output = document.createElement('div');\n const {html, js} = await Templates.renderForPromise('mod_forum/local/grades/view_grade', userGrade);\n Templates.replaceNodeContents(output, html, js);\n\n // Note: We do not use await here because it messes with the Modal transitions.\n const [gradeHTML, gradeJS] = await renderGradeTemplate(userGrade);\n const gradeReplace = output.querySelector('[data-region=\"grade-template\"]');\n Templates.replaceNodeContents(gradeReplace, gradeHTML, gradeJS);\n modal.setBody(output.outerHTML);\n spinner.resolve();\n};\n\nconst renderGradeTemplate = async(userGrade) => {\n const {html, js} = await Templates.renderForPromise(userGrade.templatename, userGrade.grade);\n return [html, js];\n};\nexport {getGradingPanelFunctions};\n"],"file":"grader.min.js"} \ No newline at end of file +{"version":3,"sources":["../../../src/local/grades/grader.js"],"names":["templateNames","grader","app","gradingPanel","error","searchResults","status","displayUserPicker","root","html","pickerRegion","querySelector","Selectors","regions","Templates","replaceNodeContents","fetchContentFromRender","js","getUpdateUserContentFunction","getContentForUser","getGradeForUser","saveGradeForUser","firstLoad","user","spinner","Promise","all","id","then","userGrade","moduleReplace","render","templatename","grade","gradingPanelHtml","gradingPanelJS","panelContainer","gradingPanelContainer","panel","form","addEventListener","event","preventDefault","scrollTop","resolve","showSearchResultContainer","bodyContainer","userPickerContainer","searchResultsContainer","classList","add","remove","hideSearchResultContainer","showUserSearchInput","toggleSearchButton","searchContainer","searchInput","setAttribute","gradingInfoContainer","parentElement","collapseGradingDrawer","buttons","focus","hideUserSearchInput","removeAttribute","value","searchForUsers","userList","searchTerm","toLowerCase","filter","fullname","includes","renderSearchResults","users","renderForPromise","registerEventListeners","graderLayout","userPicker","saveGradeFunction","graderContainer","getContainer","toggleSearch","searchInputContainer","userSearchContainer","userSearchInput","e","target","closest","toggleFullscreen","stopImmediatePropagation","closeGrader","close","saveGrade","currentUser","getAttribute","innerHTML","selectUserButton","selectUser","userId","find","setUserId","showUser","DrawerEvents","DRAWER_HIDDEN","drawerRoot","setContentContainerMargin","DRAWER_SHOWN","offsetWidth","rightMargin","contentContainer","moduleContainer","style","marginRight","getSaveUserGradeFunction","setGradeForUser","gradingPanelErrors","values","sendStudentNotifications","result","success","addToast","failed","displayGradingError","err","message","errorString","type","launch","getListOfUsers","initialUserId","moduleName","courseName","courseUrl","focusOnClose","length","addNotification","fullscreen","showLoader","drawer","show","defaultsendnotifications","updateUserContent","userIds","map","statusContainer","renderContext","hasgrade","index","indexOf","total","catch","rootNode","view","userid","Modal","create","title","large","types","CANCEL","modal","getRoot","on","ModalEvents","hidden","destroy","output","document","createElement","renderGradeTemplate","gradeHTML","gradeJS","gradeReplace","setBody","outerHTML"],"mappings":"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","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 * This module will tie together all of the different calls the gradable module will make.\n *\n * @module mod_forum/local/grades/grader\n * @copyright 2019 Mathew May \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nimport Templates from 'core/templates';\nimport Selectors from './local/grader/selectors';\nimport getUserPicker from './local/grader/user_picker';\nimport {createLayout as createFullScreenWindow} from 'mod_forum/local/layout/fullscreen';\nimport getGradingPanelFunctions from './local/grader/gradingpanel';\nimport {add as addToast} from 'core/toast';\nimport {addNotification} from 'core/notification';\nimport {get_string as getString} from 'core/str';\nimport {failedUpdate} from 'core_grades/grades/grader/gradingpanel/normalise';\nimport {addIconToContainerWithPromise} from 'core/loadingicon';\nimport {debounce} from 'core/utils';\nimport {fillInitialValues} from 'core_grades/grades/grader/gradingpanel/comparison';\nimport * as Modal from 'core/modal_factory';\nimport * as ModalEvents from 'core/modal_events';\nimport {subscribe} from 'core/pubsub';\nimport DrawerEvents from 'core/drawer_events';\n\nconst templateNames = {\n grader: {\n app: 'mod_forum/local/grades/grader',\n gradingPanel: {\n error: 'mod_forum/local/grades/local/grader/gradingpanel/error',\n },\n searchResults: 'mod_forum/local/grades/local/grader/user_picker/user_search',\n status: 'mod_forum/local/grades/local/grader/status',\n },\n};\n\n/**\n * Helper function that replaces the user picker placeholder with what we get back from the user picker class.\n *\n * @param {HTMLElement} root\n * @param {String} html\n */\nconst displayUserPicker = (root, html) => {\n const pickerRegion = root.querySelector(Selectors.regions.pickerRegion);\n Templates.replaceNodeContents(pickerRegion, html, '');\n};\n\n/**\n * To be removed, this is now done as a part of Templates.renderForPromise()\n *\n * @param {String} html\n * @param {String} js\n * @returns {array} An array containing the HTML, and JS.\n */\nconst fetchContentFromRender = (html, js) => {\n return [html, js];\n};\n\n/**\n * Here we build the function that is passed to the user picker that'll handle updating the user content area\n * of the grading interface.\n *\n * @param {HTMLElement} root\n * @param {Function} getContentForUser\n * @param {Function} getGradeForUser\n * @param {Function} saveGradeForUser\n * @return {Function}\n */\nconst getUpdateUserContentFunction = (root, getContentForUser, getGradeForUser, saveGradeForUser) => {\n let firstLoad = true;\n\n return async(user) => {\n const spinner = firstLoad ? null : addIconToContainerWithPromise(root);\n const [\n [html, js],\n userGrade,\n ] = await Promise.all([\n getContentForUser(user.id).then(fetchContentFromRender),\n getGradeForUser(user.id),\n ]);\n Templates.replaceNodeContents(root.querySelector(Selectors.regions.moduleReplace), html, js);\n\n const [\n gradingPanelHtml,\n gradingPanelJS\n ] = await Templates.render(userGrade.templatename, userGrade.grade).then(fetchContentFromRender);\n const panelContainer = root.querySelector(Selectors.regions.gradingPanelContainer);\n const panel = panelContainer.querySelector(Selectors.regions.gradingPanel);\n Templates.replaceNodeContents(panel, gradingPanelHtml, gradingPanelJS);\n\n const form = panel.querySelector('form');\n fillInitialValues(form);\n\n form.addEventListener('submit', event => {\n saveGradeForUser(user);\n event.preventDefault();\n });\n\n panelContainer.scrollTop = 0;\n firstLoad = false;\n\n if (spinner) {\n spinner.resolve();\n }\n return userGrade;\n };\n};\n\n/**\n * Show the search results container and hide the user picker and body content.\n *\n * @param {HTMLElement} bodyContainer The container element for the body content\n * @param {HTMLElement} userPickerContainer The container element for the user picker\n * @param {HTMLElement} searchResultsContainer The container element for the search results\n */\nconst showSearchResultContainer = (bodyContainer, userPickerContainer, searchResultsContainer) => {\n bodyContainer.classList.add('hidden');\n userPickerContainer.classList.add('hidden');\n searchResultsContainer.classList.remove('hidden');\n};\n\n/**\n * Hide the search results container and show the user picker and body content.\n *\n * @param {HTMLElement} bodyContainer The container element for the body content\n * @param {HTMLElement} userPickerContainer The container element for the user picker\n * @param {HTMLElement} searchResultsContainer The container element for the search results\n */\nconst hideSearchResultContainer = (bodyContainer, userPickerContainer, searchResultsContainer) => {\n bodyContainer.classList.remove('hidden');\n userPickerContainer.classList.remove('hidden');\n searchResultsContainer.classList.add('hidden');\n};\n\n/**\n * Toggles the visibility of the user search.\n *\n * @param {HTMLElement} toggleSearchButton The button that toggles the search\n * @param {HTMLElement} searchContainer The container element for the user search\n * @param {HTMLElement} searchInput The input element for searching\n */\nconst showUserSearchInput = (toggleSearchButton, searchContainer, searchInput) => {\n searchContainer.classList.remove('collapsed');\n toggleSearchButton.setAttribute('aria-expanded', 'true');\n toggleSearchButton.classList.add('expand');\n toggleSearchButton.classList.remove('collapse');\n\n // Hide the grading info container from screen reader.\n const gradingInfoContainer = searchContainer.parentElement.querySelector(Selectors.regions.gradingInfoContainer);\n gradingInfoContainer.setAttribute('aria-hidden', 'true');\n\n // Hide the collapse grading drawer button from screen reader.\n const collapseGradingDrawer = searchContainer.parentElement.querySelector(Selectors.buttons.collapseGradingDrawer);\n collapseGradingDrawer.setAttribute('aria-hidden', 'true');\n collapseGradingDrawer.setAttribute('tabindex', '-1');\n\n searchInput.focus();\n};\n\n/**\n * Toggles the visibility of the user search.\n *\n * @param {HTMLElement} toggleSearchButton The button that toggles the search\n * @param {HTMLElement} searchContainer The container element for the user search\n * @param {HTMLElement} searchInput The input element for searching\n */\nconst hideUserSearchInput = (toggleSearchButton, searchContainer, searchInput) => {\n searchContainer.classList.add('collapsed');\n toggleSearchButton.setAttribute('aria-expanded', 'false');\n toggleSearchButton.classList.add('collapse');\n toggleSearchButton.classList.remove('expand');\n toggleSearchButton.focus();\n\n // Show the grading info container to screen reader.\n const gradingInfoContainer = searchContainer.parentElement.querySelector(Selectors.regions.gradingInfoContainer);\n gradingInfoContainer.removeAttribute('aria-hidden');\n\n // Show the collapse grading drawer button from screen reader.\n const collapseGradingDrawer = searchContainer.parentElement.querySelector(Selectors.buttons.collapseGradingDrawer);\n collapseGradingDrawer.removeAttribute('aria-hidden');\n collapseGradingDrawer.setAttribute('tabindex', '0');\n\n searchInput.value = '';\n};\n\n/**\n * Find the list of users who's names include the given search term.\n *\n * @param {Array} userList List of users for the grader\n * @param {String} searchTerm The search term to match\n * @return {Array}\n */\nconst searchForUsers = (userList, searchTerm) => {\n if (searchTerm === '') {\n return userList;\n }\n\n searchTerm = searchTerm.toLowerCase();\n\n return userList.filter((user) => {\n return user.fullname.toLowerCase().includes(searchTerm);\n });\n};\n\n/**\n * Render the list of users in the search results area.\n *\n * @param {HTMLElement} searchResultsContainer The container element for search results\n * @param {Array} users The list of users to display\n */\nconst renderSearchResults = async(searchResultsContainer, users) => {\n const {html, js} = await Templates.renderForPromise(templateNames.grader.searchResults, {users});\n Templates.replaceNodeContents(searchResultsContainer, html, js);\n};\n\n/**\n * Add click handlers to the buttons in the header of the grading interface.\n *\n * @param {HTMLElement} graderLayout\n * @param {Object} userPicker\n * @param {Function} saveGradeFunction\n * @param {Array} userList List of users for the grader.\n */\nconst registerEventListeners = (graderLayout, userPicker, saveGradeFunction, userList) => {\n const graderContainer = graderLayout.getContainer();\n const toggleSearchButton = graderContainer.querySelector(Selectors.buttons.toggleSearch);\n const searchInputContainer = graderContainer.querySelector(Selectors.regions.userSearchContainer);\n const searchInput = searchInputContainer.querySelector(Selectors.regions.userSearchInput);\n const bodyContainer = graderContainer.querySelector(Selectors.regions.bodyContainer);\n const userPickerContainer = graderContainer.querySelector(Selectors.regions.pickerRegion);\n const searchResultsContainer = graderContainer.querySelector(Selectors.regions.searchResultsContainer);\n\n graderContainer.addEventListener('click', (e) => {\n if (e.target.closest(Selectors.buttons.toggleFullscreen)) {\n e.stopImmediatePropagation();\n e.preventDefault();\n graderLayout.toggleFullscreen();\n\n return;\n }\n\n if (e.target.closest(Selectors.buttons.closeGrader)) {\n e.stopImmediatePropagation();\n e.preventDefault();\n\n graderLayout.close();\n\n return;\n }\n\n if (e.target.closest(Selectors.buttons.saveGrade)) {\n saveGradeFunction(userPicker.currentUser);\n }\n\n if (e.target.closest(Selectors.buttons.toggleSearch)) {\n if (toggleSearchButton.getAttribute('aria-expanded') === 'true') {\n // Search is open so let's close it.\n hideUserSearchInput(toggleSearchButton, searchInputContainer, searchInput);\n hideSearchResultContainer(bodyContainer, userPickerContainer, searchResultsContainer);\n searchResultsContainer.innerHTML = '';\n } else {\n // Search is closed so let's open it.\n showUserSearchInput(toggleSearchButton, searchInputContainer, searchInput);\n showSearchResultContainer(bodyContainer, userPickerContainer, searchResultsContainer);\n renderSearchResults(searchResultsContainer, userList);\n }\n\n return;\n }\n\n const selectUserButton = e.target.closest(Selectors.buttons.selectUser);\n if (selectUserButton) {\n const userId = selectUserButton.getAttribute('data-userid');\n const user = userList.find(user => user.id == userId);\n userPicker.setUserId(userId);\n userPicker.showUser(user);\n hideUserSearchInput(toggleSearchButton, searchInputContainer, searchInput);\n hideSearchResultContainer(bodyContainer, userPickerContainer, searchResultsContainer);\n searchResultsContainer.innerHTML = '';\n }\n });\n\n // Debounce the search input so that it only executes 300 milliseconds after the user has finished typing.\n searchInput.addEventListener('input', debounce(() => {\n const users = searchForUsers(userList, searchInput.value);\n renderSearchResults(searchResultsContainer, users);\n }, 300));\n\n // Remove the right margin of the content container when the grading panel is hidden so that it expands to full-width.\n subscribe(DrawerEvents.DRAWER_HIDDEN, (drawerRoot) => {\n const gradingPanel = drawerRoot[0];\n if (gradingPanel.querySelector(Selectors.regions.gradingPanel)) {\n setContentContainerMargin(graderContainer, 0);\n }\n });\n\n // Bring back the right margin of the content container when the grading panel is shown to give space for the grading panel.\n subscribe(DrawerEvents.DRAWER_SHOWN, (drawerRoot) => {\n const gradingPanel = drawerRoot[0];\n if (gradingPanel.querySelector(Selectors.regions.gradingPanel)) {\n setContentContainerMargin(graderContainer, gradingPanel.offsetWidth);\n }\n });\n};\n\n/**\n * Adjusts the right margin of the content container.\n *\n * @param {HTMLElement} graderContainer The container for the grader app.\n * @param {Number} rightMargin The right margin value.\n */\nconst setContentContainerMargin = (graderContainer, rightMargin) => {\n const contentContainer = graderContainer.querySelector(Selectors.regions.moduleContainer);\n if (contentContainer) {\n contentContainer.style.marginRight = `${rightMargin}px`;\n }\n};\n\n/**\n * Get the function used to save a user grade.\n *\n * @param {HTMLElement} root The container for the grader\n * @param {Function} setGradeForUser The function that will be called.\n * @return {Function}\n */\nconst getSaveUserGradeFunction = (root, setGradeForUser) => {\n return async(user) => {\n try {\n root.querySelector(Selectors.regions.gradingPanelErrors).innerHTML = '';\n const result = await setGradeForUser(\n user.id,\n root.querySelector(Selectors.values.sendStudentNotifications).value,\n root.querySelector(Selectors.regions.gradingPanel)\n );\n if (result.success) {\n addToast(await getString('grades:gradesavedfor', 'mod_forum', user));\n }\n if (result.failed) {\n displayGradingError(root, user, result.error);\n }\n\n return result;\n } catch (err) {\n displayGradingError(root, user, err);\n\n return failedUpdate(err);\n }\n };\n};\n\n/**\n * Display a grading error, typically from a failed save.\n *\n * @param {HTMLElement} root The container for the grader\n * @param {Object} user The user who was errored\n * @param {Object} err The details of the error\n */\nconst displayGradingError = async(root, user, err) => {\n const [\n {html, js},\n errorString\n ] = await Promise.all([\n Templates.renderForPromise(templateNames.grader.gradingPanel.error, {error: err}),\n await getString('grades:gradesavefailed', 'mod_forum', {error: err.message, ...user}),\n ]);\n\n Templates.replaceNodeContents(root.querySelector(Selectors.regions.gradingPanelErrors), html, js);\n addToast(errorString, {type: 'warning'});\n};\n\n/**\n * Launch the grader interface with the specified parameters.\n *\n * @param {Function} getListOfUsers A function to get the list of users\n * @param {Function} getContentForUser A function to get the content for a specific user\n * @param {Function} getGradeForUser A function get the grade details for a specific user\n * @param {Function} setGradeForUser A function to set the grade for a specific user\n * @param {Object} Preferences for the launch function\n */\nexport const launch = async(getListOfUsers, getContentForUser, getGradeForUser, setGradeForUser, {\n initialUserId = null,\n moduleName,\n courseName,\n courseUrl,\n sendStudentNotifications,\n focusOnClose = null,\n} = {}) => {\n\n // We need all of these functions to be executed in series, if one step runs before another the interface\n // will not work.\n\n // We need this promise to resolve separately so that we can avoid loading the whole interface if there are no users.\n const userList = await getListOfUsers();\n if (!userList.length) {\n addNotification({\n message: await getString('nouserstograde', 'core_grades'),\n type: \"error\",\n });\n return;\n }\n\n // Now that we have confirmed there are at least some users let's boot up the grader interface.\n const [\n graderLayout,\n {html, js},\n ] = await Promise.all([\n createFullScreenWindow({\n fullscreen: false,\n showLoader: false,\n focusOnClose,\n }),\n Templates.renderForPromise(templateNames.grader.app, {\n moduleName,\n courseName,\n courseUrl,\n drawer: {show: true},\n defaultsendnotifications: sendStudentNotifications,\n }),\n ]);\n\n const graderContainer = graderLayout.getContainer();\n\n const saveGradeFunction = getSaveUserGradeFunction(graderContainer, setGradeForUser);\n\n Templates.replaceNodeContents(graderContainer, html, js);\n const updateUserContent = getUpdateUserContentFunction(graderContainer, getContentForUser, getGradeForUser, saveGradeFunction);\n\n const userIds = userList.map(user => user.id);\n const statusContainer = graderContainer.querySelector(Selectors.regions.statusContainer);\n // Fetch the userpicker for display.\n const userPicker = await getUserPicker(\n userList,\n async(user) => {\n const userGrade = await updateUserContent(user);\n const renderContext = {\n status: userGrade.hasgrade,\n index: userIds.indexOf(user.id) + 1,\n total: userList.length\n };\n Templates.render(templateNames.grader.status, renderContext).then(html => {\n statusContainer.innerHTML = html;\n return html;\n }).catch();\n },\n saveGradeFunction,\n {\n initialUserId,\n },\n );\n\n // Register all event listeners.\n registerEventListeners(graderLayout, userPicker, saveGradeFunction, userList);\n\n // Display the newly created user picker.\n displayUserPicker(graderContainer, userPicker.rootNode);\n};\n\n/**\n * Show the grade for a specific user.\n *\n * @param {Function} getGradeForUser A function get the grade details for a specific user\n * @param {Number} userid The ID of a specific user\n * @param {String} moduleName the name of the module\n */\nexport const view = async(getGradeForUser, userid, moduleName, {\n focusOnClose = null,\n} = {}) => {\n\n const [\n userGrade,\n modal,\n ] = await Promise.all([\n getGradeForUser(userid),\n Modal.create({\n title: moduleName,\n large: true,\n type: Modal.types.CANCEL\n }),\n ]);\n\n const spinner = addIconToContainerWithPromise(modal.getRoot());\n\n // Handle hidden event.\n modal.getRoot().on(ModalEvents.hidden, function() {\n // Destroy when hidden.\n modal.destroy();\n if (focusOnClose) {\n try {\n focusOnClose.focus();\n } catch (e) {\n // eslint-disable-line\n }\n }\n });\n\n modal.show();\n const output = document.createElement('div');\n const {html, js} = await Templates.renderForPromise('mod_forum/local/grades/view_grade', userGrade);\n Templates.replaceNodeContents(output, html, js);\n\n // Note: We do not use await here because it messes with the Modal transitions.\n const [gradeHTML, gradeJS] = await renderGradeTemplate(userGrade);\n const gradeReplace = output.querySelector('[data-region=\"grade-template\"]');\n Templates.replaceNodeContents(gradeReplace, gradeHTML, gradeJS);\n modal.setBody(output.outerHTML);\n spinner.resolve();\n};\n\nconst renderGradeTemplate = async(userGrade) => {\n const {html, js} = await Templates.renderForPromise(userGrade.templatename, userGrade.grade);\n return [html, js];\n};\nexport {getGradingPanelFunctions};\n"],"file":"grader.min.js"} \ No newline at end of file diff --git a/mod/forum/amd/src/local/grades/grader.js b/mod/forum/amd/src/local/grades/grader.js index d95d48bd45eb1..1de1762304a49 100644 --- a/mod/forum/amd/src/local/grades/grader.js +++ b/mod/forum/amd/src/local/grades/grader.js @@ -379,7 +379,7 @@ const displayGradingError = async(root, user, err) => { ]); Templates.replaceNodeContents(root.querySelector(Selectors.regions.gradingPanelErrors), html, js); - addToast(errorString); + addToast(errorString, {type: 'warning'}); }; /** diff --git a/payment/amd/build/gateways_modal.min.js b/payment/amd/build/gateways_modal.min.js index 80961db4f9946..0f808f4b853f4 100644 --- a/payment/amd/build/gateways_modal.min.js +++ b/payment/amd/build/gateways_modal.min.js @@ -1,2 +1,2 @@ -define ("core_payment/gateways_modal",["exports","core/modal_factory","core/templates","core/str","./repository","./selectors","core/modal_events","core_payment/events","core/toast","core/notification","./modal_gateways"],function(a,b,c,d,e,f,g,h,i,j,k){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.init=void 0;b=l(b);c=l(c);f=l(f);g=l(g);h=l(h);j=l(j);k=l(k);var o="undefined"!=typeof window?window:"undefined"!=typeof self?self:"undefined"!=typeof global?global:{};function l(a){return a&&a.__esModule?a:{default:a}}function m(a,b,c,d,e,f,g){try{var h=a[f](g),i=h.value}catch(a){c(a);return}if(h.done){b(i)}else{Promise.resolve(i).then(d,e)}}function n(a){return function(){var b=this,c=arguments;return new Promise(function(d,e){var h=a.apply(b,c);function f(a){m(h,d,e,f,g,"next",a)}function g(a){m(h,d,e,f,g,"throw",a)}f(void 0)})}}var p=function(){document.addEventListener("click",function(a){var b=a.target.closest("[data-action=\"core_payment/triggerPayment\"]");if(b){a.preventDefault();q(b,{focusOnClose:a.target})}})},q=function(){var a=n(regeneratorRuntime.mark(function a(l){var m,n,o,p,q,u,v,w,x,y,z=arguments;return regeneratorRuntime.wrap(function(a){while(1){switch(a.prev=a.next){case 0:m=1.\n\n/**\n * Contain the logic for the gateways modal.\n *\n * @module core_payment/gateways_modal\n * @copyright 2019 Shamim Rezaie \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport ModalFactory from 'core/modal_factory';\nimport Templates from 'core/templates';\nimport {get_string as getString} from 'core/str';\nimport {getAvailableGateways} from './repository';\nimport Selectors from './selectors';\nimport ModalEvents from 'core/modal_events';\nimport PaymentEvents from 'core_payment/events';\nimport {add as addToast, addToastRegion} from 'core/toast';\nimport Notification from 'core/notification';\nimport ModalGateways from './modal_gateways';\n\n/**\n * Register event listeners for the module.\n */\nconst registerEventListeners = () => {\n document.addEventListener('click', e => {\n const gatewayTrigger = e.target.closest('[data-action=\"core_payment/triggerPayment\"]');\n if (gatewayTrigger) {\n e.preventDefault();\n\n show(gatewayTrigger, {focusOnClose: e.target});\n }\n });\n};\n\n/**\n * Shows the gateway selector modal.\n *\n * @param {HTMLElement} rootNode\n * @param {Object} options - Additional options\n * @param {HTMLElement} options.focusOnClose The element to focus on when the modal is closed.\n */\nconst show = async(rootNode, {\n focusOnClose = null,\n} = {}) => {\n const modal = await ModalFactory.create({\n type: ModalGateways.TYPE,\n title: await getString('selectpaymenttype', 'core_payment'),\n body: await Templates.render('core_payment/gateways_modal', {}),\n });\n\n const rootElement = modal.getRoot()[0];\n addToastRegion(rootElement);\n\n modal.show();\n\n modal.getRoot().on(ModalEvents.hidden, () => {\n // Destroy when hidden.\n modal.destroy();\n try {\n focusOnClose.focus();\n } catch (e) {\n // eslint-disable-line\n }\n });\n\n modal.getRoot().on(PaymentEvents.proceed, (e) => {\n const gateway = (rootElement.querySelector(Selectors.values.gateway) || {value: ''}).value;\n\n if (gateway) {\n processPayment(\n gateway,\n rootNode.dataset.component,\n rootNode.dataset.paymentarea,\n rootNode.dataset.itemid,\n rootNode.dataset.description\n )\n .then(message => {\n modal.hide();\n Notification.addNotification({\n message: message,\n type: 'success',\n });\n location.href = rootNode.dataset.successurl;\n\n // The following return statement is never reached. It is put here just to make eslint happy.\n return message;\n })\n .catch(message => Notification.alert('', message));\n } else {\n // We cannot use await in the following line.\n // The reason is that we are preventing the default action of the save event being triggered,\n // therefore we cannot define the event handler function asynchronous.\n getString('nogatewayselected', 'core_payment').then(message => addToast(message)).catch();\n }\n\n e.preventDefault();\n });\n\n // Re-calculate the cost when gateway is changed.\n rootElement.addEventListener('change', e => {\n if (e.target.matches(Selectors.elements.gateways)) {\n updateCostRegion(rootElement, rootNode.dataset.cost);\n }\n });\n\n const gateways = await getAvailableGateways(rootNode.dataset.component, rootNode.dataset.paymentarea, rootNode.dataset.itemid);\n const context = {\n gateways\n };\n\n const {html, js} = await Templates.renderForPromise('core_payment/gateways', context);\n Templates.replaceNodeContents(rootElement.querySelector(Selectors.regions.gatewaysContainer), html, js);\n selectSingleGateway(rootElement);\n await updateCostRegion(rootElement, rootNode.dataset.cost);\n};\n\n/**\n * Auto-select the gateway if there is only one gateway.\n *\n * @param {HTMLElement} root An HTMLElement that contains the cost region\n */\nconst selectSingleGateway = root => {\n const gateways = root.querySelectorAll(Selectors.elements.gateways);\n\n if (gateways.length == 1) {\n gateways[0].checked = true;\n }\n};\n\n/**\n * Shows the cost of the item the user is purchasing in the cost region.\n *\n * @param {HTMLElement} root An HTMLElement that contains the cost region\n * @param {string} defaultCost The default cost that is going to be displayed if no gateway is selected\n * @returns {Promise}\n */\nconst updateCostRegion = async(root, defaultCost = '') => {\n const gatewayElement = root.querySelector(Selectors.values.gateway);\n const surcharge = parseInt((gatewayElement || {dataset: {surcharge: 0}}).dataset.surcharge);\n const cost = (gatewayElement || {dataset: {cost: defaultCost}}).dataset.cost;\n\n const {html, js} = await Templates.renderForPromise('core_payment/fee_breakdown', {fee: cost, surcharge});\n Templates.replaceNodeContents(root.querySelector(Selectors.regions.costContainer), html, js);\n};\n\n/**\n * Process payment using the selected gateway.\n *\n * @param {string} gateway The gateway to be used for payment\n * @param {string} component Name of the component that the itemId belongs to\n * @param {string} paymentArea Name of the area in the component that the itemId belongs to\n * @param {number} itemId An internal identifier that is used by the component\n * @param {string} description Description of the payment\n * @returns {Promise}\n */\nconst processPayment = async(gateway, component, paymentArea, itemId, description) => {\n const paymentMethod = await import(`paygw_${gateway}/gateways_modal`);\n return paymentMethod.process(component, paymentArea, itemId, description);\n};\n\n/**\n * Set up the payment actions.\n */\nexport const init = () => {\n if (!init.initialised) {\n // Event listeners should only be registered once.\n init.initialised = true;\n registerEventListeners();\n }\n};\n\n/**\n * Whether the init function was called before.\n *\n * @static\n * @type {boolean}\n */\ninit.initialised = false;\n"],"file":"gateways_modal.min.js"} \ No newline at end of file +{"version":3,"sources":["../src/gateways_modal.js"],"names":["registerEventListeners","document","addEventListener","e","gatewayTrigger","target","closest","preventDefault","show","focusOnClose","rootNode","ModalFactory","ModalGateways","TYPE","Templates","render","type","title","body","create","modal","rootElement","getRoot","on","ModalEvents","hidden","destroy","focus","PaymentEvents","proceed","gateway","querySelector","Selectors","values","value","processPayment","dataset","component","paymentarea","itemid","description","then","message","hide","Notification","addNotification","location","href","successurl","catch","alert","matches","elements","gateways","updateCostRegion","cost","context","renderForPromise","html","js","replaceNodeContents","regions","gatewaysContainer","selectSingleGateway","root","querySelectorAll","length","checked","defaultCost","gatewayElement","surcharge","parseInt","fee","costContainer","paymentArea","itemId","paymentMethod","process","init","initialised"],"mappings":"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","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 * Contain the logic for the gateways modal.\n *\n * @module core_payment/gateways_modal\n * @copyright 2019 Shamim Rezaie \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport ModalFactory from 'core/modal_factory';\nimport Templates from 'core/templates';\nimport {get_string as getString} from 'core/str';\nimport {getAvailableGateways} from './repository';\nimport Selectors from './selectors';\nimport ModalEvents from 'core/modal_events';\nimport PaymentEvents from 'core_payment/events';\nimport {add as addToast, addToastRegion} from 'core/toast';\nimport Notification from 'core/notification';\nimport ModalGateways from './modal_gateways';\n\n/**\n * Register event listeners for the module.\n */\nconst registerEventListeners = () => {\n document.addEventListener('click', e => {\n const gatewayTrigger = e.target.closest('[data-action=\"core_payment/triggerPayment\"]');\n if (gatewayTrigger) {\n e.preventDefault();\n\n show(gatewayTrigger, {focusOnClose: e.target});\n }\n });\n};\n\n/**\n * Shows the gateway selector modal.\n *\n * @param {HTMLElement} rootNode\n * @param {Object} options - Additional options\n * @param {HTMLElement} options.focusOnClose The element to focus on when the modal is closed.\n */\nconst show = async(rootNode, {\n focusOnClose = null,\n} = {}) => {\n const modal = await ModalFactory.create({\n type: ModalGateways.TYPE,\n title: await getString('selectpaymenttype', 'core_payment'),\n body: await Templates.render('core_payment/gateways_modal', {}),\n });\n\n const rootElement = modal.getRoot()[0];\n addToastRegion(rootElement);\n\n modal.show();\n\n modal.getRoot().on(ModalEvents.hidden, () => {\n // Destroy when hidden.\n modal.destroy();\n try {\n focusOnClose.focus();\n } catch (e) {\n // eslint-disable-line\n }\n });\n\n modal.getRoot().on(PaymentEvents.proceed, (e) => {\n const gateway = (rootElement.querySelector(Selectors.values.gateway) || {value: ''}).value;\n\n if (gateway) {\n processPayment(\n gateway,\n rootNode.dataset.component,\n rootNode.dataset.paymentarea,\n rootNode.dataset.itemid,\n rootNode.dataset.description\n )\n .then(message => {\n modal.hide();\n Notification.addNotification({\n message: message,\n type: 'success',\n });\n location.href = rootNode.dataset.successurl;\n\n // The following return statement is never reached. It is put here just to make eslint happy.\n return message;\n })\n .catch(message => Notification.alert('', message));\n } else {\n // We cannot use await in the following line.\n // The reason is that we are preventing the default action of the save event being triggered,\n // therefore we cannot define the event handler function asynchronous.\n getString('nogatewayselected', 'core_payment').then(message => addToast(message, {type: 'warning'})).catch();\n }\n\n e.preventDefault();\n });\n\n // Re-calculate the cost when gateway is changed.\n rootElement.addEventListener('change', e => {\n if (e.target.matches(Selectors.elements.gateways)) {\n updateCostRegion(rootElement, rootNode.dataset.cost);\n }\n });\n\n const gateways = await getAvailableGateways(rootNode.dataset.component, rootNode.dataset.paymentarea, rootNode.dataset.itemid);\n const context = {\n gateways\n };\n\n const {html, js} = await Templates.renderForPromise('core_payment/gateways', context);\n Templates.replaceNodeContents(rootElement.querySelector(Selectors.regions.gatewaysContainer), html, js);\n selectSingleGateway(rootElement);\n await updateCostRegion(rootElement, rootNode.dataset.cost);\n};\n\n/**\n * Auto-select the gateway if there is only one gateway.\n *\n * @param {HTMLElement} root An HTMLElement that contains the cost region\n */\nconst selectSingleGateway = root => {\n const gateways = root.querySelectorAll(Selectors.elements.gateways);\n\n if (gateways.length == 1) {\n gateways[0].checked = true;\n }\n};\n\n/**\n * Shows the cost of the item the user is purchasing in the cost region.\n *\n * @param {HTMLElement} root An HTMLElement that contains the cost region\n * @param {string} defaultCost The default cost that is going to be displayed if no gateway is selected\n * @returns {Promise}\n */\nconst updateCostRegion = async(root, defaultCost = '') => {\n const gatewayElement = root.querySelector(Selectors.values.gateway);\n const surcharge = parseInt((gatewayElement || {dataset: {surcharge: 0}}).dataset.surcharge);\n const cost = (gatewayElement || {dataset: {cost: defaultCost}}).dataset.cost;\n\n const {html, js} = await Templates.renderForPromise('core_payment/fee_breakdown', {fee: cost, surcharge});\n Templates.replaceNodeContents(root.querySelector(Selectors.regions.costContainer), html, js);\n};\n\n/**\n * Process payment using the selected gateway.\n *\n * @param {string} gateway The gateway to be used for payment\n * @param {string} component Name of the component that the itemId belongs to\n * @param {string} paymentArea Name of the area in the component that the itemId belongs to\n * @param {number} itemId An internal identifier that is used by the component\n * @param {string} description Description of the payment\n * @returns {Promise}\n */\nconst processPayment = async(gateway, component, paymentArea, itemId, description) => {\n const paymentMethod = await import(`paygw_${gateway}/gateways_modal`);\n return paymentMethod.process(component, paymentArea, itemId, description);\n};\n\n/**\n * Set up the payment actions.\n */\nexport const init = () => {\n if (!init.initialised) {\n // Event listeners should only be registered once.\n init.initialised = true;\n registerEventListeners();\n }\n};\n\n/**\n * Whether the init function was called before.\n *\n * @static\n * @type {boolean}\n */\ninit.initialised = false;\n"],"file":"gateways_modal.min.js"} \ No newline at end of file diff --git a/payment/amd/src/gateways_modal.js b/payment/amd/src/gateways_modal.js index 23b252bc04a35..96e51fdc77d6b 100644 --- a/payment/amd/src/gateways_modal.js +++ b/payment/amd/src/gateways_modal.js @@ -104,7 +104,7 @@ const show = async(rootNode, { // We cannot use await in the following line. // The reason is that we are preventing the default action of the save event being triggered, // therefore we cannot define the event handler function asynchronous. - getString('nogatewayselected', 'core_payment').then(message => addToast(message)).catch(); + getString('nogatewayselected', 'core_payment').then(message => addToast(message, {type: 'warning'})).catch(); } e.preventDefault();