问题
I've noticed that the document.execCommand('copy')
command times out after about 5s when running in the background. Is there a way to get around this limitation, or perhaps a fallback if it takes longer than that?
Here is the page that I've been using for the Clipboard docs. For example, I have a function that 'prepares' the data (generating html from tabular data) and then a second function that copies it to the clipboard with some additional markup. On large tables this can often take perhaps ten seconds from the time a user press Cmd-C until the html is generated and able to be copied.
Additionally, I've noticed Google Sheets allows Copy operations that extend beyond five seconds so I'm curious how they would be doing it:
# still works after 25 seconds!
[Violation] 'copy' handler took 25257ms 2217559571-waffle_js_prod_core.js:337
The code is minified/obfuscated so very difficult to read but here is the file from above: https://docs.google.com/static/spreadsheets2/client/js/2217559571-waffle_js_prod_core.js.
For reference, the amount of data being copied is about 50MB. Please use a ~10 second delay on the copy operation to simulate this long-running process.
For the bounty, I'm hoping someone could show a working example of doing a single Cmd-C to either:
- Is it possible to have a long-running copy operation in the background (i.e., asynchronously), for example with a web worker?
- If it must be done synchronously, an example of doing the copy operation, showing some progress -- for example, maybe the copy operation emits an event after every 10k rows or so.
It must generate html and must involve only a single Cmd-C (even if we use a preventDefault
and trigger the copy-event in the background.
You can use the following as a template for how the 'html-generation' function should work:
function sleepFor( sleepDuration ){
var now = new Date().getTime();
while(new Date().getTime() < now + sleepDuration){ /* do nothing */ }
}
// note: the data should be copied to a dom element and not a string
// so it can be used on `document.execCommand("copy")`
// but using a string below as its easier to demonstrate
// note, however, that it will give a "range exceeded" error
// on very large strings (when using the string, but ignore that,
// as it won't occur when using the proper dom element
var sall='<html><table>'
var srow='<tr><td ><div style="text-align: right"><span style="color: #060606; ">1</span></div></td><td ><div style="text-align: right"><span style="color: #060606; ">Feb 27, 2018</span></div></td><td ><div style="text-align: right"><span style="color: #060606; ">315965</span></div></td><td ><div style="text-align: left"><span style="color: #060606; ">CA</span></div></td><td ><div style="text-align: left"><span style="color: #060606; ">SDBUY</span></div></td><td ><div style="text-align: right"><span style="color: #060606; ">9.99</span></div></td><td ><div style="text-align: left"><span style="color: #060606; ">CAD</span></div></td><td ><div style="text-align: right"><span style="color: #060606; ">7.88</span></div></td></tr>'
for (i=0; i<1e6; i++) {
sall += srow;
if (i%1e5==0) sleepFor(1000); // simulate a 10 second operation...
if (i==(1e6-1)) console.log('Done')
}
sall += '</table></html>'
// now copy to clipboard
If helpful to reproduce a true copy event: https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Interact_with_the_clipboard.
回答1:
Here's what I found:
In this script: https://docs.google.com/static/spreadsheets2/client/js/1150385833-codemirror.js
I found this function:
function onCopyCut(e) {
if (!belongsToInput(e) || signalDOMEvent(cm, e))
return;
if (cm.somethingSelected()) {
setLastCopied({
lineWise: false,
text: cm.getSelections()
});
if (e.type == "cut")
cm.replaceSelection("", null, "cut")
} else if (!cm.options.lineWiseCopyCut)
return;
else {
var ranges = copyableRanges(cm);
setLastCopied({
lineWise: true,
text: ranges.text
});
if (e.type == "cut")
cm.operation(function() {
cm.setSelections(ranges.ranges, 0, sel_dontScroll);
cm.replaceSelection("", null, "cut")
})
}
if (e.clipboardData) {
e.clipboardData.clearData();
var content = lastCopied.text.join("\n");
e.clipboardData.setData("Text", content);
if (e.clipboardData.getData("Text") == content) {
e.preventDefault();
return
}
}
var kludge = hiddenTextarea(),
te = kludge.firstChild;
cm.display.lineSpace.insertBefore(kludge, cm.display.lineSpace.firstChild);
te.value = lastCopied.text.join("\n");
var hadFocus = document.activeElement;
selectInput(te);
setTimeout(function() {
cm.display.lineSpace.removeChild(kludge);
hadFocus.focus();
if (hadFocus == div)
input.showPrimarySelection()
}, 50)
}
NEW FIND
I found that Google sheets loads this script:
(function() {
window._docs_chrome_extension_exists = !0;
window._docs_chrome_extension_features_version = 1;
window._docs_chrome_extension_permissions = "alarms clipboardRead clipboardWrite identity power storage unlimitedStorage".split(" ");
}
).call(this);
This is tied to their own extensions
NEW FIND 2
When I paste in a cell, it uses these two functions:
Script: https://docs.google.com/static/spreadsheets2/client/js/1526657789-waffle_js_prod_core.js
p.B_a = function(a) {
var b = a.Ge().clipboardData;
if (b && (b = b.getData("text/plain"),
!be(Kf(b)))) {
b = Lm(b);
var c = this.C.getRange(),
d = this.C.getRange();
d.jq() && $fc(this.Fd(), d) == this.getValue().length && (c = this.Fd(),
d = c.childNodes.length,
c = TJ(c, 0 < d && XJ(c.lastChild) ? d - 1 : d));
c.yP(b);
VJ(b, !1);
a.preventDefault()
}
};
p.Z1b = function() {
var a = this.C.getRange();
a && 1 < fec(a).textContent.length && SAc(this)
}
NEW FIND 3
This function is used when I select all and copy:
Script: https://docs.google.com/static/spreadsheets2/client/js/1526657789-waffle_js_prod_core.js
p.bxa = function(a, b) {
this.D = b && b.Ge().clipboardData || null;
this.J = !1;
try {
this.rda();
if (this.D && "paste" == b.type) {
var c = this.D,
d = this.L,
e = {},
f = [];
if (void 0 !== c.items)
for (var h = c.items, k = 0; k < h.length; k++) {
var l = h[k],
n = l.type;
f.push(n);
if (!e[n] && d(n)) {
a: switch (l.kind) {
case "string":
var q = xk(c.getData(l.type));
break a;
case "file":
var t = l.getAsFile();
q = t ? Bnd(t) : null;
break a;
default:
q = null
}
var u = q;
u && (e[n] = u)
}
}
else {
var z = c.types || [];
for (h = 0; h < z.length; h++) {
var E = z[h];
f.push(E);
!e[E] && d(E) && (e[E] = xk(c.getData(E)))
}
k = c.files || [];
for (c = 0; c < k.length; c++) {
u = k[c];
var L = u.type;
f.push(L);
!e[L] && d(L) && (e[L] = Bnd(u))
}
}
this.C = e;
a: {
for (d = 0; d < f.length; d++)
if ("text/html" == f[d]) {
var Q = !0;
break a
}
Q = !1
}
this.H = Q || !And(f)
}
this.F.bxa(a, b);
this.J && b.preventDefault()
} finally {
this.D = null
}
}
ANSWER TO YOUR COMMENT
Here is the difference between e.clipboardData.setData()
and execCommand("copy")
:
e.clipboardData.setData()
is used to manipulate the data going to the clipboard.
execCommand("copy")
programatically calls the CMD/CTRL + C
.
If you call execCommand("copy")
, it will just copy your current selection just as if you pressed CMD/CTRL + C
. You can also use this function with e.clipboardData.setData()
:
//Button being a HTML button element
button.addEventListener("click",function(){
execCommand("copy");
});
//This function is called by a click or CMD/CTRL + C
window.addEventListener("copy",function(e){
e.preventDefault();
e.clipboardData.setData("text/plain", "Hey!");
}
NEW FIND 3 (POSSIBLE ANSWER)
Don't use setTimeout
for simulating long text because it will freeze up the UI. Instead, just use a large chunk of text.
This script works without timing out.
window.addEventListener('copy', function(e) {
e.preventDefault();
console.log("Started!");
//This will throw an error on StackOverflow, but works on my website.
//Use this to disable it for testing on StackOverflow
//if (!(navigator.clipboard)) {
if (navigator.clipboard) {
document.getElementById("status").innerHTML = 'Copying, do not leave page.';
document.getElementById("main").style.backgroundColor = '#BB595C';
tryCopyAsync(e).then(() =>
document.getElementById("main").style.backgroundColor = '#59BBB7',
document.getElementById("status").innerHTML = 'Idle... Try copying',
console.log('Copied!')
);
} else {
console.log('Not async...');
tryCopy(e);
console.log('Copied!');
}
});
function tryCopy(e) {
e.clipboardData.setData("text/html", getText());
}
function getText() {
var html = '';
var row = '<div></div>';
for (i = 0; i < 1000000; i++) {
html += row;
}
return html;
}
async function tryCopyAsync(e) {
navigator.clipboard.writeText(await getTextAsync());
}
async function getTextAsync() {
var html = '';
var row = '<div></div>';
await waitNextFrame();
for (i = 0; i < 1000000; i++) {
html += row;
}
await waitNextFrame();
html = [new ClipboardItem({"text/html": new Blob([html], {type: 'text/html'})})];
return html;
}
//Credit: https://stackoverflow.com/a/66165276/7872728
function waitNextFrame() {
return new Promise(postTask);
}
function postTask(task) {
const channel = postTask.channel || new MessageChannel();
channel.port1.addEventListener("message", () => task(), {
once: true
});
channel.port2.postMessage("");
channel.port1.start();
}
#main{
width:100%;
height:100vh;
background:gray;
color:white;
font-weight:bold;
}
#status{
text-align:center;
padding-top:24px;
font-size:16pt;
}
body{
padding:0;
margin:0;
overflow:hidden;
}
<div id='main'>
<div id='status'>Idle... Try copying</div>
</div>
To test, make sure you click inside the snippet before copying.
Full DEMO
window.addEventListener("load", function() {
window.addEventListener("click", function() {
hideCopying();
});
fallbackCopy = 0;
if (navigator.permissions && navigator.permissions.query && notUnsupportedBrowser()) {
navigator.permissions.query({
name: 'clipboard-write'
}).then(function(result) {
if (result.state === 'granted') {
clipboardAccess = 1;
} else if (result.state === 'prompt') {
clipboardAccess = 2;
} else {
clipboardAccess = 0;
}
});
} else {
clipboardAccess = 0;
}
window.addEventListener('copy', function(e) {
if (fallbackCopy === 0) {
showCopying();
console.log("Started!");
if (clipboardAccess > 0) {
e.preventDefault();
showCopying();
tryCopyAsync(e).then(() =>
hideCopying(),
console.log('Copied! (Async)')
);
} else if (e.clipboardData) {
e.preventDefault();
console.log('Not async...');
try {
showCopying();
tryCopy(e);
console.log('Copied! (Not async)');
hideCopying();
} catch (error) {
console.log(error.message);
}
} else {
console.log('Not async fallback...');
try {
tryCopyFallback();
console.log('Copied! (Fallback)');
} catch (error) {
console.log(error.message);
}
hideCopying();
}
} else {
fallbackCopy = 0;
}
});
});
function notUnsupportedBrowser() {
if (typeof InstallTrigger !== 'undefined') {
return false;
} else {
return true;
}
}
function tryCopyFallback() {
var copyEl = document.createElement
var body = document.body;
var input = document.createElement("textarea");
var text = getText();
input.setAttribute('readonly', '');
input.style.position = 'absolute';
input.style.top = '-10000px';
input.style.left = '-10000px';
input.innerHTML = text;
body.appendChild(input);
input.focus();
input.select();
fallbackCopy = 1;
document.execCommand("copy");
}
function hideCopying() {
el("main").style.backgroundColor = '#59BBB7';
el("status").innerHTML = 'Idle... Try copying';
}
function showCopying() {
el("status").innerHTML = 'Copying, do not leave page.';
el("main").style.backgroundColor = '#BB595C';
}
function el(a) {
return document.getElementById(a);
}
function tryCopy(e) {
e.clipboardData.setData("text/html", getText());
e.clipboardData.setData("text/plain", getText());
}
function getText() {
var html = '';
var row = '<div></div>';
for (i = 0; i < 1000000; i++) {
html += row;
}
return html;
}
async function tryCopyAsync(e) {
navigator.clipboard.write(await getTextAsync());
}
async function getTextAsync() {
var html = '';
var row = '<div></div>';
await waitNextFrame();
for (i = 0; i < 1000000; i++) {
html += row;
}
await waitNextFrame();
html = [new ClipboardItem({"text/html": new Blob([html], {type: 'text/html'}),"text/plain": new Blob([html], {type: 'text/plain'})})];
return html;
}
//Credit: https://stackoverflow.com/a/66165276/7872728
function waitNextFrame() {
return new Promise(postTask);
}
function postTask(task) {
const channel = postTask.channel || new MessageChannel();
channel.port1.addEventListener("message", () => task(), {
once: true
});
channel.port2.postMessage("");
channel.port1.start();
}
#main {
width: 500px;
height: 200px;
background: gray;
background: rgba(0, 0, 0, 0.4);
color: white;
font-weight: bold;
margin-left: calc(50% - 250px);
margin-top: calc(50vh - 100px);
border-radius: 12px;
border: 3px solid #fff;
border: 3px solid rgba(0, 0, 0, 0.4);
box-shadow: 5px 5px 50px -15px #000;
box-shadow: 20px 20px 50px 15px rgba(0, 0, 0, 0.3);
}
#status {
text-align: center;
line-height: 180px;
vertical-align: middle;
font-size: 16pt;
}
body {
background: lightgrey;
background: linear-gradient(325deg, rgba(81, 158, 155, 1) 0%, rgba(157, 76, 79, 1) 100%);
font-family: arial;
height: 100vh;
padding: 0;
margin: 0;
overflow: hidden;
}
@media only screen and (max-width: 700px) {
#main {
width: 100%;
height: 100vh;
border: 0;
border-radius: 0;
margin: 0;
}
#status {
line-height: calc(100vh - 20px);
}
}
<!DOCTYPE html>
<html>
<head>
<title>Clipboard Test</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta charset='UTF-8'>
</head>
<body>
<div id='main'>
<div id='status'>Click the webpage to start.</div>
</div>
</body>
</html>
DEMO webpage: Here is my DEMO
HELPFUL LINKS
- web.dev/async-clipboard/
- alligator.io/js/async-clipboard-api/
- developer.mozilla.org/...
- googlechrome.github.io/samples/async-clipboard/
- caniuse.com/?search=clipboard
- sitepoint.com/clipboard-api/
- codepen.io alternative clipboard functions
- w3schools.com (the old way)
回答2:
From the the execCommand method it says
Depending on the browser, this may not work. On Firefox, it will not work, and you'll see a message like this in your console:
document.execCommand(‘cut’/‘copy’) was denied because it was not called from inside a short running user-generated event handler.
To enable this use case, you need to ask for the "clipboardWrite" permission. So: "clipboardWrite" enables you to write to the clipboard outside a short-lived event handler for a user action.
So your data preparation may take as long as you want, but the call to execCommand('copy')
must be executed soon after the user generated the event whose handler is running it.
Apparently it may be any event handler, not only a copy event.
copyFormatted
to perform the copy.genHtml
function produces the HTML data assynchronously.enableCopy
assigns todelegateCopy
a function created in a context where copy is allowed, and that expires in after one second (assigning null todelegateCopy
)
It was mentioned the possibility of using clipboardData
while this interface is more programatic, it requires recent user interaction as well, that was the problem I focused. Of course using setData
has the advantage of not requiring to parse the HTML and create a DOM for the copied data that in the motivation example is a large amount of data. Furthermore ClipboardData is marked as experimental.
The following snipped shows a solution that (1) runs asynchronously, (2) request user interaction if deemed necessary, (3) use setData if possible, (3) if setData
not available then uses the innerHTML -> select. copy approach.
// This function expects an HTML string and copies it as rich text.
// https://stackoverflow.com/a/34192073/12750353
function copyFormatted (html) {
// Create container for the HTML
console.log('copyFormatted')
var container = document.createElement('div')
let hasClipboardData = true;
const selectContainer = () => {
const range = document.createRange()
range.selectNode(container)
window.getSelection().addRange(range)
}
const copyHandler = (event) => {
console.log('copyHandler')
event.stopPropagation()
if(hasClipboardData){
if(event.clipboardData && event.clipboardData.setData){
console.log('setData skip html rendering')
event.clipboardData.setData('text/html', html);
event.preventDefault();
} else {
hasClipboardData = false;
container.innerHTML = html;
selectContainer();
document.execCommand('copy');
return; // don't remove the element yet
}
}
document.body.removeChild(container);
document.removeEventListener('copy', copyHandler)
}
// Mount the container to the DOM to make `contentWindow` available
document.body.appendChild(container)
document.addEventListener('copy', copyHandler);
selectContainer();
document.execCommand('copy')
}
function sleepFor( sleepDuration ){
// sleep asynchronously
return new Promise((resolve) => setTimeout(resolve, sleepDuration))
}
async function genHtml(NSECONDS=10, NROWS=10){
var sall='<html><table>'
var srow='<tr><td ><div style="text-align: right"><span style="color: #060606; ">1</span></div></td><td ><div style="text-align: right"><span style="color: #060606; ">Feb 27, 2018</span></div></td><td ><div style="text-align: right"><span style="color: #060606; ">315965</span></div></td><td ><div style="text-align: left"><span style="color: #060606; ">CA</span></div></td><td ><div style="text-align: left"><span style="color: #060606; ">SDBUY</span></div></td><td ><div style="text-align: right"><span style="color: #060606; ">9.99</span></div></td><td ><div style="text-align: left"><span style="color: #060606; ">CAD</span></div></td><td ><div style="text-align: right"><span style="color: #060606; ">7.88</span></div></td></tr>'
BATCH = Math.max(1, Math.floor(NROWS / 10000))
for (i=0; i<NROWS; i++) {
sall += srow; // process a chunk of data
if(i % BATCH === 0){
updateProgress((i+1) / NROWS);
}
await sleepFor(1000 * NSECONDS / NROWS);
if (i==(1e6-1)) console.log('Done')
}
sall += '</table></html>'
return sall;
}
let lastProgress = '';
function updateProgress(progress){
const progressText = (100 * progress).toFixed(2) + '%';
// prevents updating UI very frequently
if(progressText !== lastProgress){
const progressElement = document.querySelector('#progress');
progressElement.textContent = lastProgress = progressText
}
}
let delegateCopy = null;
function enableCopy(){
// we are inside an event handler, thus, a function in this
// context can copy.
// What I will do is to export to the variable delegateCopy
// a function that will run in this context.
delegateCopy = (html) => copyFormatted(html)
// But this function expires soon
COPY_TIMEOUT=1.0; // one second to be called
setTimeout(() => delegateCopy = null, 1000 * COPY_TIMEOUT)
}
function showOkButton(){
document.querySelector('#confirm-copy').style.display = 'inline-block';
}
function hideOkButton() {
document.querySelector('#confirm-copy').style.display = 'none';
}
// now copy to clipboard
async function doCopy(NSECONDS=10, NROWS=10){
enableCopy()
const html = await genHtml(NSECONDS, NROWS)
// if the copy delegate expired show the ok button
if(delegateCopy === null){
showOkButton();
// wait for some event that will enable copy
while(delegateCopy === null){
await sleepFor(100);
}
}
delegateCopy(html);
hideOkButton()
}
document.querySelector('#copy-0p5s').addEventListener('click', (event) => {doCopy(0.5, 10)})
document.querySelector('#copy-2s').addEventListener('click', (event) => {doCopy(2, 10)})
document.querySelector('#copy-10s').addEventListener('click', (event) => {doCopy(10, 10)})
document.querySelector('#copy-30s').addEventListener('click', (event) => {doCopy(30, 1000)})
document.querySelector('#copy-60s').addEventListener('click', (event) => {doCopy(60, 1e5)})
document.querySelector('#confirm-copy').addEventListener('click', () => enableCopy())
<button id="copy-0p5s">Copy in 0.5s</button>
<button id="copy-2s">Copy in 2s</button>
<button id="copy-10s">Copy in 10s</button>
<button id="copy-30s">Copy in 30s</button>
<button id="copy-60s">Copy in 1 min (large data)</button>
<div id="progress"></div>
<button id="confirm-copy" style="display: none;">OK</button>
<hr>
Paste here if you want.
<div contenteditable="true" style="background: #f0f0f0; margin: 5px; border-radius: 5px; border: 1px solid black;"> </div>
// This function expects an HTML string and copies it as rich text.
// https://stackoverflow.com/a/34192073/12750353
function copyFormatted (html) {
// Create container for the HTML
// [1]
var container = document.createElement('div')
container.innerHTML = html
// Hide element
// [2]
container.style.position = 'fixed'
container.style.pointerEvents = 'none'
container.style.opacity = 0
// Detect all style sheets of the page
var activeSheets = Array.prototype.slice.call(document.styleSheets)
.filter(function (sheet) {
return !sheet.disabled
})
// Mount the container to the DOM to make `contentWindow` available
// [3]
document.body.appendChild(container)
// Copy to clipboard
// [4]
window.getSelection().removeAllRanges()
var range = document.createRange()
range.selectNode(container)
window.getSelection().addRange(range)
// [5.1]
document.execCommand('copy')
// [5.2]
for (var i = 0; i < activeSheets.length; i++) activeSheets[i].disabled = true
// [5.3]
document.execCommand('copy')
// [5.4]
for (var i = 0; i < activeSheets.length; i++) activeSheets[i].disabled = false
// Remove the container
// [6]
document.body.removeChild(container)
}
function sleepFor( sleepDuration ){
// sleep asynchronously
return new Promise((resolve) => setTimeout(resolve, sleepDuration))
}
async function genHtml(NSECONDS=10, NROWS=10){
var sall='<html><table>'
var srow='<tr><td ><div style="text-align: right"><span style="color: #060606; ">1</span></div></td><td ><div style="text-align: right"><span style="color: #060606; ">Feb 27, 2018</span></div></td><td ><div style="text-align: right"><span style="color: #060606; ">315965</span></div></td><td ><div style="text-align: left"><span style="color: #060606; ">CA</span></div></td><td ><div style="text-align: left"><span style="color: #060606; ">SDBUY</span></div></td><td ><div style="text-align: right"><span style="color: #060606; ">9.99</span></div></td><td ><div style="text-align: left"><span style="color: #060606; ">CAD</span></div></td><td ><div style="text-align: right"><span style="color: #060606; ">7.88</span></div></td></tr>'
BATCH = Math.max(1, Math.floor(NROWS / 10000))
for (i=0; i<NROWS; i++) {
sall += srow; // process a chunk of data
if(i % BATCH === 0){
updateProgress((i+1) / NROWS);
}
await sleepFor(1000 * NSECONDS / NROWS);
if (i==(1e6-1)) console.log('Done')
}
sall += '</table></html>'
return sall;
}
let lastProgress = '';
function updateProgress(progress){
const progressText = (100 * progress).toFixed(2) + '%';
// prevents updating UI very frequently
if(progressText !== lastProgress){
const progressElement = document.querySelector('#progress');
progressElement.textContent = lastProgress = progressText
}
}
let delegateCopy = null;
function enableCopy(){
// we are inside an event handler, thus, a function in this
// context can copy.
// What I will do is to export to the variable delegateCopy
// a function that will run in this context.
delegateCopy = (html) => copyFormatted(html)
// But this function expires soon
COPY_TIMEOUT=1.0; // one second to be called
setTimeout(() => delegateCopy = null, 1000 * COPY_TIMEOUT)
}
function showOkButton(){
document.querySelector('#confirm-copy').style.display = 'inline-block';
}
function hideOkButton() {
document.querySelector('#confirm-copy').style.display = 'none';
}
// now copy to clipboard
async function doCopy(NSECONDS=10, NROWS=10){
enableCopy()
const html = await genHtml(NSECONDS, NROWS)
// if the copy delegate expired show the ok button
if(delegateCopy === null){
showOkButton();
// wait for some event that will enable copy
while(delegateCopy === null){
await sleepFor(100);
}
}
delegateCopy(html);
hideOkButton()
}
document.querySelector('#copy-0p5s').addEventListener('click', (event) => {doCopy(0.5, 10)})
document.querySelector('#copy-2s').addEventListener('click', (event) => {doCopy(2, 10)})
document.querySelector('#copy-10s').addEventListener('click', (event) => {doCopy(10, 10)})
document.querySelector('#copy-30s').addEventListener('click', (event) => {doCopy(30, 1000)})
document.querySelector('#copy-60s').addEventListener('click', (event) => {doCopy(60, 1e5)})
document.querySelector('#confirm-copy').addEventListener('click', () => enableCopy())
<button id="copy-0p5s">Copy in 0.5s</button>
<button id="copy-2s">Copy in 2s</button>
<button id="copy-10s">Copy in 10s</button>
<button id="copy-30s">Copy in 30s</button>
<button id="copy-60s">Copy in 1 min (large data)</button>
<div id="progress"></div>
<button id="confirm-copy" style="display: none;">OK</button>
<hr>
Paste here if you want.
<div contenteditable="true" style="background: #f0f0f0; margin: 5px; border-radius: 5px; border: 1px solid black;"> </div>
The solution above requires the user to click OK
after the data is finished to perform the copy. I know this is not what you want, but the browser requires a user intervention.
In the sequence I have a modified version where I used the mousemove
event to refresh the copyDelegate, if the mouse moved less than one second from the end of the data preparation the OK button won't show. You could also use keypress
or any other frequent user generated event.
// This function expects an HTML string and copies it as rich text.
// https://stackoverflow.com/a/34192073/12750353
function copyFormatted (html) {
// Create container for the HTML
// [1]
var container = document.createElement('div')
container.innerHTML = html
// Hide element
// [2]
container.style.position = 'fixed'
container.style.pointerEvents = 'none'
container.style.opacity = 0
// Detect all style sheets of the page
var activeSheets = Array.prototype.slice.call(document.styleSheets)
.filter(function (sheet) {
return !sheet.disabled
})
// Mount the container to the DOM to make `contentWindow` available
// [3]
document.body.appendChild(container)
// Copy to clipboard
// [4]
window.getSelection().removeAllRanges()
var range = document.createRange()
range.selectNode(container)
window.getSelection().addRange(range)
// [5.1]
document.execCommand('copy')
// [5.2]
for (var i = 0; i < activeSheets.length; i++) activeSheets[i].disabled = true
// [5.3]
document.execCommand('copy')
// [5.4]
for (var i = 0; i < activeSheets.length; i++) activeSheets[i].disabled = false
// Remove the container
// [6]
document.body.removeChild(container)
}
function sleepFor( sleepDuration ){
// sleep asynchronously
return new Promise((resolve) => setTimeout(resolve, sleepDuration))
}
async function genHtml(NSECONDS=10, NROWS=10){
var sall='<html><table>'
var srow='<tr><td ><div style="text-align: right"><span style="color: #060606; ">1</span></div></td><td ><div style="text-align: right"><span style="color: #060606; ">Feb 27, 2018</span></div></td><td ><div style="text-align: right"><span style="color: #060606; ">315965</span></div></td><td ><div style="text-align: left"><span style="color: #060606; ">CA</span></div></td><td ><div style="text-align: left"><span style="color: #060606; ">SDBUY</span></div></td><td ><div style="text-align: right"><span style="color: #060606; ">9.99</span></div></td><td ><div style="text-align: left"><span style="color: #060606; ">CAD</span></div></td><td ><div style="text-align: right"><span style="color: #060606; ">7.88</span></div></td></tr>'
BATCH = Math.max(1, Math.floor(NROWS / 10000))
for (i=0; i<NROWS; i++) {
sall += srow; // process a chunk of data
if(i % BATCH === 0){
updateProgress((i+1) / NROWS);
}
await sleepFor(1000 * NSECONDS / NROWS);
if (i==(1e6-1)) console.log('Done')
}
sall += '</table></html>'
return sall;
}
let lastProgress = '';
function updateProgress(progress){
const progressText = (100 * progress).toFixed(2) + '%';
// prevents updating UI very frequently
if(progressText !== lastProgress){
const progressElement = document.querySelector('#progress');
progressElement.textContent = lastProgress = progressText
}
}
let delegateCopy = null;
function enableCopy(){
// we are inside an event handler, thus, a function in this
// context can copy.
// What I will do is to export to the variable delegateCopy
// a function that will run in this context.
delegateCopy = (html) => copyFormatted(html)
// But this function expires soon
COPY_TIMEOUT=1.0; // one second to be called
setTimeout(() => delegateCopy = null, 1000 * COPY_TIMEOUT)
}
function showOkButton(){
document.querySelector('#confirm-copy').style.display = 'inline-block';
}
function hideOkButton() {
document.querySelector('#confirm-copy').style.display = 'none';
}
// now copy to clipboard
async function doCopy(NSECONDS=10, NROWS=10){
enableCopy()
const html = await genHtml(NSECONDS, NROWS)
// if the copy delegate expired show the ok button
if(delegateCopy === null){
showOkButton();
// wait for some event that will enable copy
while(delegateCopy === null){
await sleepFor(100);
}
}
delegateCopy(html);
hideOkButton()
}
document.querySelector('#copy-0p5s').addEventListener('click', (event) => {doCopy(0.5, 10)})
document.querySelector('#copy-2s').addEventListener('click', (event) => {doCopy(2, 10)})
document.querySelector('#copy-10s').addEventListener('click', (event) => {doCopy(10, 10)})
document.querySelector('#copy-30s').addEventListener('click', (event) => {doCopy(30, 1000)})
document.querySelector('#copy-60s').addEventListener('click', (event) => {doCopy(60, 1e5)})
document.querySelector('#confirm-copy').addEventListener('click', () => enableCopy())
// mouse move happens all the time this prevents the OK button from appearing
document.addEventListener('mousemove', () => enableCopy())
<button id="copy-0p5s">Copy in 0.5s</button>
<button id="copy-2s">Copy in 2s</button>
<button id="copy-10s">Copy in 10s</button>
<button id="copy-30s">Copy in 30s</button>
<button id="copy-60s">Copy in 1 min (large data)</button>
<div id="progress"></div>
<button id="confirm-copy" style="display: none;">OK</button>
<hr>
Paste here if you want.
<div contenteditable="true" style="background: #f0f0f0; margin: 5px; border-radius: 5px; border: 1px solid black;"> </div>
clipboardData.setData
For ClipboardEvents you have access to the clipboard, in particular when you copy it producess a ClipboardEvent that you can use the method setData
with format text/html
. The limitation with this method is that it the setData must run synchronously, after the event handler returns it is disabled, so you cannot show a progress bar or these things.
document.body.addEventListener('copy', (event) =>{
const t = Number(document.querySelector('#delay').value)
const copyNow = () => {
console.log('Delay of ' + (t / 1000) + ' second')
event.clipboardData.setData('text/html',
'<table><tr><td>Hello</td><td>' + t / 1000 +'s delay</td></tr>' +
'<td></td><td>clipboard</td></tr></table>')
}
if(t === 0){
copyNow()
}else{
setTimeout(copyNow, t)
}
event.preventDefault()
})
Perform copy after
<select id="delay">
<option value="0" selected="true">immediately</option>
<option value="1">1 millisecond</option>
<option value="500">0.5 second</option>
<option value="1000">1 second</option>
<option value="2000">2 second</option>
<option value="10000">10 second</option>
<option value="20000">20 second</option>
<option value="20000">30 second</option>
</select>
<p>
Paste here if you want.
</p>
<div contenteditable="true" style="background: #f0f0f0; margin: 5px; border-radius: 5px; border: 1px solid black;"> </div>
It is true that you can take advantage of the initial user generated event to start a new copy event. And when you are in the event handler you check if the data is ready and write it to clipboard. But the browser is smart enough to prevent you from copying if the code is not running in a handler of an event recently generated.
function DelayedClipboardAccess() {
let data = null;
let format = 'text/html';
let timeout = 0;
const copyHandler = (event) => {
// this will be invoked on copy
// if there is data to be copied then it will
// it will set clipboard data, otherwise it will fire
// another copy in the near future.
if(timeout > 0){
const delay = Math.min(100, timeout);
setTimeout( () => {
this.countdown(timeout -= delay)
document.execCommand('copy')
}, delay);
event.preventDefault()
}else if(data) {
console.log('setData')
event.clipboardData.setData(format, data);
event.preventDefault()
data = null;
}else{
console.log({timeout, data})
}
}
document.addEventListener('copy', copyHandler)
this.countdown = (time) => {}
this.delayedCopy = (_data, _timeout, _format) => {
format = _format || 'text/html';
data = _data;
timeout = _timeout;
document.execCommand('copy');
}
}
const countdownEl = document.querySelector('#countdown')
const delayEl = document.querySelector('#delay')
const copyAgain = document.querySelector('#copy-again')
const clipboard = new DelayedClipboardAccess()
function delayedCopy() {
const t = Number(delayEl.value)
const data = '<table><tr><td>Hello</td><td>' + t / 1000 +'s delay</td></tr>' +
'<td></td><td>clipboard</td></tr></table>';
clipboard.delayedCopy(data, t, 'text/html')
}
clipboard.countdown = (t) => countdownEl.textContent = t;
delayEl.addEventListener('change', delayedCopy)
copyAgain.addEventListener('click', delayedCopy)
Perform copy after
<select id="delay">
<option value="0" selected="true">immediately</option>
<option value="1">1 millisecond</option>
<option value="500">0.5 second</option>
<option value="1000">1 second</option>
<option value="2000">2 second</option>
<option value="10000">10 second</option>
<option value="20000">20 second</option>
<option value="30000">30 second</option>
</select>
<button id="copy-again">Copy again</button>
<div id="countdown"></div>
<p>
Paste here if you want.
</p>
<div contenteditable="true" style="background: #f0f0f0; margin: 5px; border-radius: 5px; border: 1px solid black;"> </div>
In the above snipped there is an interesting effect that is if you copy something while the countdown is running you are going to start a new chain that will accelerate the countdown.
In my browser the countdown stops after around 5 seconds. If I press Ctrl+C after the chain of copy handlers was broken, the copy handler is invoked again by a user generated event then it goes for 5 seconds again.
回答3:
From the same link :
function updateClipboard(newClip) {
navigator.clipboard.writeText(newClip).then(function() {
/* clipboard successfully set */
}, function() {
// your timeout function handler
/* clipboard write failed */
});
}
回答4:
To be honest, I'm not seeing the same behavior. (Edit: I will note we are using slightly different copy commands) When I take your HTML generation function as is, I get a memory limit error. Specifically, "Uncaught RangeError: Invalid string length" at the line in the loop that appends the row.
If I tone down your loop (to i<1e4
) it does not run out of memory, takes just over 10 seconds to complete, and does not throw an error.
Here is the code I am using for reference.
const generateLargeHTMLChunk = () => {
function sleepFor( sleepDuration ){
var now = new Date().getTime();
while(new Date().getTime() < now + sleepDuration){ /* do nothing */ }
}
var sall='<html><table>'
var srow='<tr><td ><div style="text-align: right"><span style="color: #060606; ">1</span></div></td><td ><div style="text-align: right"><span style="color: #060606; ">Feb 27, 2018</span></div></td><td ><div style="text-align: right"><span style="color: #060606; ">315965</span></div></td><td ><div style="text-align: left"><span style="color: #060606; ">CA</span></div></td><td ><div style="text-align: left"><span style="color: #060606; ">SDBUY</span></div></td><td ><div style="text-align: right"><span style="color: #060606; ">9.99</span></div></td><td ><div style="text-align: left"><span style="color: #060606; ">CAD</span></div></td><td ><div style="text-align: right"><span style="color: #060606; ">7.88</span></div></td></tr>'
for (i=0; i<1e4; i++) {
sall += srow;
if (i%1e3==0) sleepFor(1000); // simulate a 10 second operation...
if (i==(1e4-1)) console.log('Done')
}
sall += '</table></html>'
// now copy to clipboard
return sall;
}
document.addEventListener('copy', function(e) {
const timestamp = (date) => `${date.getHours()}:${date.getMinutes()}:${date.getSeconds()}.${date.getMilliseconds()}`;
const start = new Date();
console.log(`Starting at ${timestamp(start)}`);
const largeHTML = generateLargeHTMLChunk();
e.clipboardData.setData('text/plain', largeHTML);
const end = new Date();
console.log(`Ending at ${timestamp(end)}`);
console.log(`Duration of ${end-start} ms.`); // ~10000 in my tests
e.preventDefault();
});
I doubt this fixes your actual problem, but this is too much to type in a comment. I hope whatever causes the difference in the the behavior we're seeing does help, though.
来源:https://stackoverflow.com/questions/66058639/how-to-copy-a-large-amount-of-html-content-to-clipboard-in-javascript-without-ti