Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to copy a large amount of html content to clipboard in javascript without timeout

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.

like image 733
David542 Avatar asked Feb 05 '21 06:02

David542


People also ask

How do you copy HTML to clipboard?

see jsfiddle where you can enter html segment into the textarea and copy to the clipboard with ctrl+c.

How do I copy a large file to the clipboard?

Use the step for copy and paste : Send HotKey : Ctrl + a. Use activity Copy selected Text and store the result into one variable. where ever you want to paste the data just pass that Variable.

How to copy text to clipboard using JavaScript?

Copy Text to Clipboard Step 1) Add HTML: Example The text field --> <input type="text" value="Hello World" id="myInput"> The button used to copy the text --> <button onclick="myFunction()">Copy text</button> Step 2) Add JavaScript:

Is there a way to copy HTML segments to the clipboard?

see jsfiddle where you can enter html segment into the textarea and copy to the clipboard with ctrl+c. Show activity on this post. Currently (Sep 2021), The problem is that Firefox doesn't support this method.

How to copy the text from the text field in HTML?

Click on the button to copy the text from the text field. Copy to clipboardCopy text Copy Text to Clipboard Step 1) Add HTML: Example The text field --> <input type="text" value="Hello World" id="myInput"> The button used to copy the text --> <button onclick="myFunction()">Copy text</button>

How to replace the current clipboard contents with currently selected data?

The document.execCommand () can be used to trigger the "cut" and "copy" actions, which replaces the current contents of the clipboard with the currently selected data. Have a look at the following piece of code:


Video Answer


3 Answers

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)
like image 136
Seth B Avatar answered Oct 20 '22 09:10

Seth B


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.

  1. copyFormatted to perform the copy.

  2. genHtml function produces the HTML data assynchronously.

  3. enableCopy assigns to delegateCopy a function created in a context where copy is allowed, and that expires in after one second (assigning null to delegateCopy)

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;">&nbsp;</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;">&nbsp;</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;">&nbsp;</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;">&nbsp;</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;">&nbsp;</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.

like image 38
Bob Avatar answered Oct 20 '22 11:10

Bob


From the same link :

function updateClipboard(newClip) {
  navigator.clipboard.writeText(newClip).then(function() {
    /* clipboard successfully set */
  }, function() {
    // your timeout function handler
    /* clipboard write failed */
  });
}
like image 33
justcodin Avatar answered Oct 20 '22 09:10

justcodin