如何在没有超时的情况下在javascript中将大量html内容复制到剪贴板

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

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

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

# 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(即使我们使用 apreventDefault并在后台触发复制事件。


您可以使用以下内容作为“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><span>1</span></div></td><td  ><div><span>Feb 27, 2018</span></div></td><td  ><div><span>315965</span></div></td><td  ><div><span>CA</span></div></td><td  ><div><span>SDBUY</span></div></td><td  ><div><span>9.99</span></div></td><td  ><div><span>CAD</span></div></td><td  ><div><span>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, 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)
}

新发现

我发现谷歌工作表加载了这个脚本:

(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。相反,只需使用一大块文本。

此脚本无需超时即可工作。

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

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

完整演示

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

演示网页:这是我的演示

有用的网址

  • web.dev/异步剪贴板/
  • 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 替代剪贴板功能
  • w3schools.com(旧方式)

以上是如何在没有超时的情况下在javascript中将大量html内容复制到剪贴板的全部内容。
THE END
分享
二维码
< <上一篇
下一篇>>