MDL-61460 core: Create a copy to clipboard JS module
This works by adding event listeners for elements that contain
the 'copytoclipboard' "data-action" attribute through event delegation.
These trigger elements must also define a "data-clipboard-target"
attribute which contains the query selector for the target element
where text will be copied.
On success, a toast message is shown to the user that the text has
been copied to the clipboard. This success message can be customised
by passing a message string to the "data-clipboard-success-message"
attribute in the trigger element.
When the element's value or innerText cannot be found, a toast message
indicating that the text cannot be copied to the clipboard is shown.
The copy to clipboard functionality is primarily using the Clipboard API
but in the event that this is not available, e.g. the site is not
running on HTTPS, this falls back to the document.execCommand('copy')
approach of copying the text in the target container.
Special thanks to Andrew for improving this module with the event
delegation approach and improved fallback handling.
Co-authored-by: Andrew Lyons <[email protected]>
This commit is contained in:
committed by
Adrian Greeve
co-authored by
Andrew Lyons
parent
3610f1ee3b
commit
341578bfde
@@ -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';
|
||||
|
||||
+2
@@ -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
|
||||
File diff suppressed because one or more lines are too long
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* 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 <code>data-action="copytoclipboard"</code> attribute and set the
|
||||
* <code>data-clipboard-target</code> 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 <code>data-clipboard-success-message</code> 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 <caption>Markup for the trigger and target elements</caption>
|
||||
* <input type="text" id="textinputtocopy" class="form-control" value="Copy me!" readonly />
|
||||
* <button id="copybutton" data-action="copytoclipboard" data-clipboard-target="#textinputtocopy"
|
||||
* data-clipboard-success-message="Success!" class="btn btn-secondary">
|
||||
* Copy to clipboard
|
||||
* </button>
|
||||
*/
|
||||
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<void>}
|
||||
* @private
|
||||
*/
|
||||
const displaySuccessToast = copyButton => getSuccessText(copyButton)
|
||||
.then(successMessage => addToast(successMessage, {}));
|
||||
|
||||
/**
|
||||
* Displays a toast containing the failure message.
|
||||
*
|
||||
* @returns {Promise<void>}
|
||||
* @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;
|
||||
}
|
||||
Reference in New Issue
Block a user