diff --git a/lang/en/moodle.php b/lang/en/moodle.php
index bac6f87ecd4..2836ad0f12c 100644
--- a/lang/en/moodle.php
+++ b/lang/en/moodle.php
@@ -2067,6 +2067,7 @@ $string['targetrole'] = 'Target role';
$string['teacheronly'] = 'for the {$a} only';
$string['teacherroles'] = '{$a} roles';
$string['teachers'] = 'Teachers';
+$string['textcopiedtoclipboard'] = 'Text copied to clipboard';
$string['textediting'] = 'Text editor';
$string['textediting_help'] = 'If an HTML editor such as Atto or TinyMCE is selected, text input areas will have a toolbar with buttons for easily adding content.
@@ -2146,6 +2147,7 @@ $string['trysearching'] = 'Try searching instead.';
$string['turneditingoff'] = 'Turn editing off';
$string['turneditingon'] = 'Turn editing on';
$string['unauthorisedlogin'] = 'The user account "{$a}" is not available on this site';
+$string['unabletocopytoclipboard'] = 'Unable to copy text to the clipboard';
$string['undecided'] = 'Undecided';
$string['unexistingcmid'] = 'Course module id not found';
$string['unexistingsectionid'] = 'Course section id not found';
diff --git a/lib/amd/build/copy_to_clipboard.min.js b/lib/amd/build/copy_to_clipboard.min.js
new file mode 100644
index 00000000000..2f1807525ba
--- /dev/null
+++ b/lib/amd/build/copy_to_clipboard.min.js
@@ -0,0 +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}});
+//# 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
new file mode 100644
index 00000000000..fdbb716df77
--- /dev/null
+++ b/lib/amd/build/copy_to_clipboard.min.js.map
@@ -0,0 +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
diff --git a/lib/amd/src/copy_to_clipboard.js b/lib/amd/src/copy_to_clipboard.js
new file mode 100644
index 00000000000..46aa3b9b016
--- /dev/null
+++ b/lib/amd/src/copy_to_clipboard.js
@@ -0,0 +1,213 @@
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see .
+
+/**
+ * A JavaScript module that enhances a button and text container to support copy-to-clipboard functionality.
+ *
+ * This module needs to be loaded by pages/templates/modules that require this functionality.
+ *
+ * To enable copy-to-clipboard functionality, we need a trigger element (usually a button) and a copy target element
+ * (e.g. a div, span, text input, or text area).
+ *
+ * In the trigger element, we need to declare the data-action="copytoclipboard" attribute and set the
+ * data-clipboard-target attribute which is the CSS selector that points to the target element that contains the text
+ * to be copied.
+ *
+ * When the text is successfully copied to the clipboard, a toast message that indicates that the copy operation was a success
+ * will be shown. This success message can be customised by setting the data-clipboard-success-message attribute in the
+ * trigger element.
+ *
+ * @module core/copy_to_clipboard
+ * @copyright 2021 Jun Pataleta
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ *
+ * @example
Markup for the trigger and target elements
+ *
+ *
+ */
+import {get_string as getString} from 'core/str';
+import {add as addToast} from 'core/toast';
+import {prefetchStrings} from 'core/prefetch';
+
+/**
+ * Add event listeners to trigger elements through event delegation.
+ *
+ * @private
+ */
+const addEventListeners = () => {
+ document.addEventListener('click', e => {
+ const copyButton = e.target.closest('[data-action="copytoclipboard"]');
+ if (!copyButton) {
+ return;
+ }
+
+ if (!copyButton.dataset.clipboardTarget) {
+ return;
+ }
+
+ const copyTarget = document.querySelector(copyButton.dataset.clipboardTarget);
+ if (!copyTarget) {
+ return;
+ }
+
+ // This is a copy target and there is content.
+ // Prevent the default action.
+ e.preventDefault();
+
+ // We have a copy target - great. Let's copy its content.
+ const textToCopy = getTextFromContainer(copyTarget);
+ if (!textToCopy) {
+ displayFailureToast();
+ return;
+ }
+
+ if (navigator.clipboard) {
+ navigator.clipboard.writeText(textToCopy)
+ .then(() => displaySuccessToast(copyButton)).catch();
+
+ return;
+ }
+
+ // The clipboard API is not available.
+ // This may happen when the page is not served over SSL.
+ // Try to fall back to document.execCommand() approach of copying the text.
+ // WARNING: This is deprecated functionality that may get dropped at anytime by browsers.
+
+ if (copyTarget instanceof HTMLInputElement || copyTarget instanceof HTMLTextAreaElement) {
+ // Focus and select the text in the target element.
+ // If the execCommand fails, at least the user can readily copy the text.
+ copyTarget.focus();
+
+ if (copyNodeContentToClipboard(copyButton, copyTarget)) {
+ // If the copy was successful then focus back on the copy button.
+ copyButton.focus();
+ }
+ } else {
+ // This copyTarget is not an input, or text area so cannot be used with the execCommand('copy') command.
+ // To work around this we create a new textarea and copy that.
+ // This textarea must be part of the DOM and must be visible.
+ // We (ab)use the sr-only tag to ensure that it is considered visible to the browser, whilst being
+ // hidden from view by the user.
+ const copyRegion = document.createElement('textarea');
+ copyRegion.value = textToCopy;
+ copyRegion.classList.add('sr-only');
+ document.body.appendChild(copyRegion);
+
+ copyNodeContentToClipboard(copyButton, copyRegion);
+
+ // After copying, remove the temporary element and move focus back to the triggering button.
+ copyRegion.remove();
+ copyButton.focus();
+ }
+ });
+};
+
+/**
+ * Copy the content of the selected element to the clipboard, and display a notifiction if successful.
+ *
+ * @param {HTMLElement} copyButton
+ * @param {HTMLElement} copyTarget
+ * @returns {boolean}
+ * @private
+ */
+const copyNodeContentToClipboard = (copyButton, copyTarget) => {
+ copyTarget.select();
+
+ // Try to copy the text from the target element.
+ if (document.execCommand('copy')) {
+ displaySuccessToast(copyButton);
+ return true;
+ }
+
+ displayFailureToast();
+ return false;
+};
+
+/**
+ * Displays a toast containing the success message.
+ *
+ * @param {HTMLElement} copyButton The element that copies the text from the container.
+ * @returns {Promise}
+ * @private
+ */
+const displaySuccessToast = copyButton => getSuccessText(copyButton)
+ .then(successMessage => addToast(successMessage, {}));
+
+/**
+ * Displays a toast containing the failure message.
+ *
+ * @returns {Promise}
+ * @private
+ */
+const displayFailureToast = () => getFailureText()
+ .then(message => addToast(message, {}));
+
+/**
+ * Fetches the failure message to show to the user.
+ *
+ * @returns {Promise}
+ * @private
+ */
+const getFailureText = () => getString('unabletocopytoclipboard', 'core');
+
+/**
+ * Fetches the success message to show to the user.
+ *
+ * @param {HTMLElement} copyButton The element that copies the text from the container. This may contain the custom success message
+ * via its data-clipboard-success-message attribute.
+ * @returns {Promise|*}
+ * @private
+ */
+const getSuccessText = copyButton => {
+ if (copyButton.dataset.clipboardSuccessMessage) {
+ return Promise.resolve(copyButton.dataset.clipboardSuccessMessage);
+ }
+
+ return getString('textcopiedtoclipboard', 'core');
+};
+
+/**
+ * Fetches the text to be copied from the container.
+ *
+ * @param {HTMLElement} container The element containing the text to be copied.
+ * @returns {null|string}
+ * @private
+ */
+const getTextFromContainer = container => {
+ if (container.value) {
+ // For containers which are form elements (e.g. text area, text input), get the element's value.
+ return container.value;
+ } else if (container.innerText) {
+ // For other elements, try to use the innerText attribute.
+ return container.innerText;
+ }
+
+ return null;
+};
+
+let loaded = false;
+if (!loaded) {
+ prefetchStrings('core', [
+ 'textcopiedtoclipboard',
+ 'unabletocopytoclipboard',
+ ]);
+
+ // Add event listeners.
+ addEventListeners();
+ loaded = true;
+}