【问题标题】:How to copy a large amount of html content to clipboard in javascript without timeout如何在javascript中将大量html内容复制到剪贴板而不会超时
【发布时间】:2021-02-05 06:33:08
【问题描述】:

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

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

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

# 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 的工作示例:

  • 是否可以在后台进行长时间运行的复制操作(即异步),例如使用 Web Worker?
  • 如果必须同步完成,一个执行复制操作的示例,显示一些进度 - 例如,复制操作可能在每 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

【问题讨论】:

  • 我还没有花时间彻底搜索,但有几点:谷歌浏览器包含一些与谷歌页面交互的扩展,超出了我们通常可以使用 Web API 所做的。我不知道这里是否有这种情况,我知道他们是用 meet 来做的。可以通过使用其他浏览器进行测试来验证这一点。此外,一旦标记为已批准,如果站点是从安全上下文提供的,则异步剪贴板 API 不再需要用户手势。
  • @Kaiido 我已经更新了问题的示例代码,这有帮助吗?
  • Google 好像使用 e.clipboardData.setData("Text", content);用于复制
  • @SethB 谢谢,你怎么知道?你想发布一个答案,用一个基本的例子来说明他们是如何做到的吗?
  • @Kaiido 这里的两个答案对您的评论指针有帮助吗?

标签: javascript clipboard


【解决方案1】:

这是我发现的:

在这个脚本中: 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, 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)
}

新发现

我发现 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>

要进行测试,请确保在复制之前单击 sn-p 内部。

完整演示

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>

演示网页:Here is my DEMO

有用的链接

【讨论】:

  • 太棒了,非常感谢您的观看。 "Text" 是否也涵盖了 html 场景?另外,如果有帮助,这里是未缩小的代码(尽管我确信你已经有了这个):drive.google.com/file/d/1Nr5gvgD8coHNObLGJurRXnn44c2Ry2JX/… ... e.clipboardData.setData(...)document.execCommand(...) 之间有什么区别?
  • @David542 这个功能对你有用吗?
  • 现在公平了,恭喜@SethB!如果你没有注意到,我就是下一个答案的人。但我认为权限技巧是扩展程序独有的,我从未将它用于网页。
  • @David542 已修复。后备副本(旧浏览器)仍然复制为 text/plain
  • @David542 现在复制为 text/htmltext/plain(演示)
【解决方案2】:

来自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 -> select .复制方法。

// 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>

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

在序列中,我有一个修改版本,我使用mousemove 事件刷新copyDelegate,如果鼠标在数据准备结束后移动不到一秒,则不会显示“确定”按钮。您还可以使用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, 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>

剪贴板数据.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 秒。

【讨论】:

  • 很好的答案,谢谢。您知道 Google 表格是如何做到这一点的吗?它从来没有提示用户进行二次确认?
  • 我不知道 Google 表格是怎么做的。我的猜测是他们拥有我在答案开头添加的摘录中提到的“clipboardWrite”权限。您是否看到我将复制工作委托给 mousemove 事件的第二个实现?您可以委托给由用户交互触发的任何事件处理程序,并且在复杂的页面中有很多。如果您要粘贴到同一页面中,您甚至可以将其委托给粘贴事件。如果您想粘贴到外部应用程序,您可以使用模糊事件之类的东西。
  • Google 表格使用的其中一个 cmets 中的另一个用户似乎是 e.clipboardData.setData(..., content);:stackoverflow.com/a/66147911/651174。您想添加一个代码 sn-p 来说明 Google 表格是如何工作的,然后我就可以接受您的回答了吗?
  • 好的,涵盖了 setData 方法。实验(在答案末尾)表明它也需要短暂的事件处理程序,并且它具有需要同步执行的限制。但它具有不需要解析 HTML 数据的(重要)优势。
  • 我很难接受你的回答,因为它太棒了,但我真的很感谢你抽出时间回答这个问题,如果可以的话,我会在你和其他用户之间“分割”赏金。顺便问一下,您是否可以上班(兼职或全职)?
【解决方案3】:

来自同一个link

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

【讨论】:

  • @justcoding -- 它与 execCommand() 的工作方式有何不同?这是 Google Sheets 使用的技术,还是他们如何获得 25s-without-time?
  • 我从来没有尝试从网页复制大量数据,所以我不太清楚,但也许这个帖子可以帮助你:stackoverflow.com/a/7037330/14223224
  • 您检查了@David542 的更新答案吗?
【解决方案4】:

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

如果我调低你的循环(到i&lt;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();
});

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

【讨论】:

  • @tm -- 是的,我已经更新了问题。要进行复制,您不能直接从字符串中进行复制——我刚刚展示了这是为解决方案提供模板的最简单方法,但请查看我添加到问题代码中的注释,以及也许重现完整复制事件的最简单方法是从这里:developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/…
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2014-07-19
  • 1970-01-01
  • 2019-12-05
  • 1970-01-01
  • 2011-05-13
  • 2019-11-29
  • 2022-04-13
相关资源
最近更新 更多