How to copy a large amount of html content to clipboard in javascript without timeout
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)
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.