如何在不超时的情况下在javascript中将大量html内容复制到剪贴板 clipboardData.setData

如何解决如何在不超时的情况下在javascript中将大量html内容复制到剪贴板 clipboardData.setData

我注意到 document.execCommand('copy') 命令在后台运行时会在大约 5 秒后超时。有没有办法绕过这个限制,或者如果需要更长的时间,也许可以回退?

这是我一直用于 Clipboard docs 的页面。例如,我有一个“准备”数据的函数(从表格数据生成 html),然后是第二个函数将它复制到带有一些附加标记的剪贴板。在大型表格上,从用户按下 Cmd-C 到生成 html 并可以复制这通常需要 10 秒钟。

此外,我注意到 Google 表格允许复制操作超过五秒,所以我很好奇他们会怎么做:

# still works after 25 seconds!
[Violation] 'copy' handler took 25257ms     2217559571-waffle_js_prod_core.js:337 

代码被缩小/混淆,因此很难阅读,但这里是上面的文件:https://docs.google.com/static/spreadsheets2/client/js/2217559571-waffle_js_prod_core.js

作为参考,正在复制的数据量约为 50MB。请在复制操作上使用约 10 秒的延迟来模拟这个长时间运行的过程。


对于赏金,我希望有人可以展示一个执行单个 Cmd-C 的工作示例:

  • 是否可以在后台进行长时间运行的复制操作(即异步),例如使用网络工作者?
  • 如果它必须同步完成,一个执行复制操作的示例,显示一些进展——例如,复制操作可能在每 10k 行左右后发出一个事件。

它必须生成 html 并且必须只涉及单个 Cmd-C(即使我们使用 preventDefault 并在后台触发复制事件。


您可以使用以下内容作为“html-generation”功能应该如何工作的模板:

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

如果有助于重现真实的复制事件:https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Interact_with_the_clipboard

解决方法

这是我发现的:

在这个脚本中: https://docs.google.com/static/spreadsheets2/client/js/1150385833-codemirror.js

我找到了这个功能:

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,sel_dontScroll);
        cm.replaceSelection("","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)
}

新发现

我发现 Google 表格加载了这个脚本:

(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);

这是绑定到他们自己的扩展

新发现 2

当我粘贴到一个单元格时,它使用这两个功能:

脚本: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)
}

新发现 3

当我全选并复制时使用此功能:

脚本: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
  }
}

回答您的评论

e.clipboardData.setData()execCommand("copy") 的区别如下:

e.clipboardData.setData() 用于操作进入剪贴板的数据。

execCommand("copy") 以编程方式调用 CMD/CTRL + C

如果您调用 execCommand("copy"),它只会复制您当前的选择,就像您按下 CMD/CTRL + C 一样。您也可以将此函数与 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!");
}

新发现 3(可能的答案)

不要使用 setTimeout 来模拟长文本,因为它会冻结 UI。相反,只需使用大量文本。

此脚本无需超时即可运行。

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>

要进行测试,请确保在复制之前单击代码段内部。

完整演示

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",{
    once: true
  });
  channel.port2.postMessage("");
  channel.port1.start();
}
#main {
  width: 500px;
  height: 200px;
  background: gray;
  background: rgba(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.4);
  box-shadow: 5px 5px 50px -15px #000;
  box-shadow: 20px 20px 50px 15px rgba(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>

演示网页:Here is my DEMO

有用的链接

,

execCommand method它说

根据浏览器的不同,这可能不起作用。在 Firefox 上,它不起作用,您将在控制台中看到如下消息:

document.execCommand(‘cut’/‘copy’) was denied because it was not called from inside a short running user-generated event handler.

要启用此用例,您需要请求“clipboardWrite”权限。所以:“clipboardWrite”使您能够在用户操作的短期事件处理程序之外写入剪贴板。

因此,您的数据准备可能需要您想要的时间,但必须在用户生成其处理程序正在运行它的事件后立即执行对 execCommand('copy') 的调用。 显然它可以是任何事件处理程序,而不仅仅是复制事件。

  1. copyFormatted 执行复制。

  2. genHtml 函数异步生成 HTML 数据。

  3. enableCopy 将在允许复制的上下文中创建的函数分配给 delegateCopy,并在一秒后过期(将 null 分配给 delegateCopy

有人提到了使用 clipboardData 的可能性,虽然此界面更具程序性,但它也需要最近的用户交互,这是我关注的问题。当然,使用 setData 的优点是不需要解析 HTML 并为复制的数据创建 DOM,在动机示例中是大量数据。此外,ClipboardData 被标记为实验性的。

以下截图显示了一个解决方案,(1) 异步运行,(2) 如果认为必要,请求用户交互,(3) 如果可能,使用 setData,(3) 如果 setData 不可用,则使用 innerHTML - > 选择。复制方法。

// 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,() => 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>

上述方案需要用户在数据完成后点击OK进行复制。我知道这不是您想要的,但浏览器需要用户干预。

在序列中,我有一个修改版本,我使用 mousemove 事件刷新 copyDelegate,如果鼠标从数据准备结束移动不到一秒,则不会显示 OK 按钮。您还可以使用 keypress 或任何其他频繁的用户生成事件。

// 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,() => 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

对于 ClipboardEvents,您可以访问剪贴板,特别是当您复制它时会生成一个 ClipboardEvent,您可以使用格式为 setData 的方法 text/html。此方法的限制是 setData 必须同步运行,在事件处理程序返回后它被禁用,因此您无法显示进度条或这些东西。

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>

确实可以利用初始用户生成的事件来启动新的复制事件。当您在事件处理程序中时,您检查数据是否准备就绪并将其写入剪贴板。但是,如果代码未在最近生成的事件的处理程序中运行,浏览器足够智能以防止您进行复制。

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>

在上面的剪辑中有一个有趣的效果,如果你在倒计时运行时复制一些东西,你将开始一个新的链,这将加速倒计时。

在我的浏览器中,倒计时在大约 5 秒后停止。如果我在复制处理程序链中断后按 Ctrl+C,用户生成的事件会再次调用复制处理程序,然后它会再次运行 5 秒。

,

来自同一个 link :

function updateClipboard(newClip) {
  navigator.clipboard.writeText(newClip).then(function() {
    /* clipboard successfully set */
  },function() {
    // your timeout function handler
    /* clipboard write failed */
  });
}
,

老实说,我没有看到相同的行为。 (编辑:我会注意到我们使用的复制命令略有不同)当我按原样使用 HTML 生成功能时,出现内存限制错误。具体来说,“Uncaught RangeError: Invalid string length”在循环中追加行的行。

如果我将您的循环调低(到 i<1e4),它不会耗尽内存,只需 10 多秒即可完成,并且不会引发错误。

这是我用来参考的代码。

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();
});

我怀疑这是否能解决您的实际问题,但是输入评论太多了。不过,我希望导致我们看到的行为差异的原因确实有所帮助。

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


依赖报错 idea导入项目后依赖报错,解决方案:https://blog.csdn.net/weixin_42420249/article/details/81191861 依赖版本报错:更换其他版本 无法下载依赖可参考:https://blog.csdn.net/weixin_42628809/a
错误1:代码生成器依赖和mybatis依赖冲突 启动项目时报错如下 2021-12-03 13:33:33.927 ERROR 7228 [ main] o.s.b.d.LoggingFailureAnalysisReporter : *************************** APPL
错误1:gradle项目控制台输出为乱码 # 解决方案:https://blog.csdn.net/weixin_43501566/article/details/112482302 # 在gradle-wrapper.properties 添加以下内容 org.gradle.jvmargs=-Df
错误还原:在查询的过程中,传入的workType为0时,该条件不起作用 &lt;select id=&quot;xxx&quot;&gt; SELECT di.id, di.name, di.work_type, di.updated... &lt;where&gt; &lt;if test=&qu
报错如下,gcc版本太低 ^ server.c:5346:31: 错误:‘struct redisServer’没有名为‘server_cpulist’的成员 redisSetCpuAffinity(server.server_cpulist); ^ server.c: 在函数‘hasActiveC
解决方案1 1、改项目中.idea/workspace.xml配置文件,增加dynamic.classpath参数 2、搜索PropertiesComponent,添加如下 &lt;property name=&quot;dynamic.classpath&quot; value=&quot;tru
删除根组件app.vue中的默认代码后报错:Module Error (from ./node_modules/eslint-loader/index.js): 解决方案:关闭ESlint代码检测,在项目根目录创建vue.config.js,在文件中添加 module.exports = { lin
查看spark默认的python版本 [root@master day27]# pyspark /home/software/spark-2.3.4-bin-hadoop2.7/conf/spark-env.sh: line 2: /usr/local/hadoop/bin/hadoop: No s
使用本地python环境可以成功执行 import pandas as pd import matplotlib.pyplot as plt # 设置字体 plt.rcParams[&#39;font.sans-serif&#39;] = [&#39;SimHei&#39;] # 能正确显示负号 p
错误1:Request method ‘DELETE‘ not supported 错误还原:controller层有一个接口,访问该接口时报错:Request method ‘DELETE‘ not supported 错误原因:没有接收到前端传入的参数,修改为如下 参考 错误2:cannot r
错误1:启动docker镜像时报错:Error response from daemon: driver failed programming external connectivity on endpoint quirky_allen 解决方法:重启docker -&gt; systemctl r
错误1:private field ‘xxx‘ is never assigned 按Altʾnter快捷键,选择第2项 参考:https://blog.csdn.net/shi_hong_fei_hei/article/details/88814070 错误2:启动时报错,不能找到主启动类 #
报错如下,通过源不能下载,最后警告pip需升级版本 Requirement already satisfied: pip in c:\users\ychen\appdata\local\programs\python\python310\lib\site-packages (22.0.4) Coll
错误1:maven打包报错 错误还原:使用maven打包项目时报错如下 [ERROR] Failed to execute goal org.apache.maven.plugins:maven-resources-plugin:3.2.0:resources (default-resources)
错误1:服务调用时报错 服务消费者模块assess通过openFeign调用服务提供者模块hires 如下为服务提供者模块hires的控制层接口 @RestController @RequestMapping(&quot;/hires&quot;) public class FeignControl
错误1:运行项目后报如下错误 解决方案 报错2:Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.8.1:compile (default-compile) on project sb 解决方案:在pom.
参考 错误原因 过滤器或拦截器在生效时,redisTemplate还没有注入 解决方案:在注入容器时就生效 @Component //项目运行时就注入Spring容器 public class RedisBean { @Resource private RedisTemplate&lt;String
使用vite构建项目报错 C:\Users\ychen\work&gt;npm init @vitejs/app @vitejs/create-app is deprecated, use npm init vite instead C:\Users\ychen\AppData\Local\npm-