如何在 javascript 中将大量 html 内容复制到剪贴板而不超时
How to copy a large amount of html content to clipboard in javascript without timeout
我注意到当 运行 在后台时,document.execCommand('copy')
命令在大约 5 秒后超时。有没有办法绕过这个限制,或者如果需要更长的时间,也许可以回退?
这是我一直用于 Clipboard docs 的页面。例如,我有一个函数 'prepares' 数据(从表格数据生成 html),然后是第二个函数,它使用一些额外的标记将它复制到剪贴板。在大型表上,从用户按下 Cmd-C 到生成 html 并能够被复制,这通常可能需要十秒钟。
此外,我注意到 Google 表格允许 复制 操作超过五秒,所以我很好奇他们会怎么做:
# still works after 25 seconds!
[Violation] 'copy' handler took 25257ms 2217559571-waffle_js_prod_core.js:337
代码是 minified/obfuscated,因此很难阅读,但这是上面的文件: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。
来自同一个 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();
});
我怀疑这是否能解决您的实际问题,但输入评论的内容太多了。不过,我希望任何导致我们所看到的行为差异的因素都能有所帮助。
来自 execCommand method 它说
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.
因此您的数据准备可能需要多长时间,但是对 execCommand('copy')
的调用必须在用户生成其处理程序为 运行 的事件后立即执行。
显然它可以是任何事件处理程序,而不仅仅是复制事件。
copyFormatted
执行复制。
genHtml
函数异步生成 HTML 数据。
enableCopy
将在允许复制的上下文中创建的函数分配给 delegateCopy
,该函数将在一秒后过期(将 null 分配给 delegateCopy
)
有人提到使用 clipboardData
的可能性,虽然这个界面更具编程性,但它也需要最近的用户交互,这是我关注的问题。当然,使用 setData
的好处是不需要解析 HTML 并为复制的数据创建 DOM,在动机示例中是大量数据。此外 ClipboardData 被标记为实验性的。
下面的片段显示了一个解决方案,(1) 运行 是异步的,(2) 如果认为有必要,请求用户交互,(3) 如果可能,使用 setData,(3) 如果 setData
不可用则使用内部 HTML -> select。复制方法。
// This function expects an HTML string and copies it as rich text.
//
function copyFormatted (html) {
// Create container for the HTML
console.log('copyFormatted')
var container = document.createElement('div')
let hasClipboardData = true;
const selectContainer = () => {
const range = document.createRange()
range.selectNode(container)
window.getSelection().addRange(range)
}
const copyHandler = (event) => {
console.log('copyHandler')
event.stopPropagation()
if(hasClipboardData){
if(event.clipboardData && event.clipboardData.setData){
console.log('setData skip html rendering')
event.clipboardData.setData('text/html', html);
event.preventDefault();
} else {
hasClipboardData = false;
container.innerHTML = html;
selectContainer();
document.execCommand('copy');
return; // don't remove the element yet
}
}
document.body.removeChild(container);
document.removeEventListener('copy', copyHandler)
}
// Mount the container to the DOM to make `contentWindow` available
document.body.appendChild(container)
document.addEventListener('copy', copyHandler);
selectContainer();
document.execCommand('copy')
}
function sleepFor( sleepDuration ){
// sleep asynchronously
return new Promise((resolve) => setTimeout(resolve, sleepDuration))
}
async function genHtml(NSECONDS=10, NROWS=10){
var sall='<html><table>'
var srow='<tr><td ><div style="text-align: right"><span style="color: #060606; ">1</span></div></td><td ><div style="text-align: right"><span style="color: #060606; ">Feb 27, 2018</span></div></td><td ><div style="text-align: right"><span style="color: #060606; ">315965</span></div></td><td ><div style="text-align: left"><span style="color: #060606; ">CA</span></div></td><td ><div style="text-align: left"><span style="color: #060606; ">SDBUY</span></div></td><td ><div style="text-align: right"><span style="color: #060606; ">9.99</span></div></td><td ><div style="text-align: left"><span style="color: #060606; ">CAD</span></div></td><td ><div style="text-align: right"><span style="color: #060606; ">7.88</span></div></td></tr>'
BATCH = Math.max(1, Math.floor(NROWS / 10000))
for (i=0; i<NROWS; i++) {
sall += srow; // process a chunk of data
if(i % BATCH === 0){
updateProgress((i+1) / NROWS);
}
await sleepFor(1000 * NSECONDS / NROWS);
if (i==(1e6-1)) console.log('Done')
}
sall += '</table></html>'
return sall;
}
let lastProgress = '';
function updateProgress(progress){
const progressText = (100 * progress).toFixed(2) + '%';
// prevents updating UI very frequently
if(progressText !== lastProgress){
const progressElement = document.querySelector('#progress');
progressElement.textContent = lastProgress = progressText
}
}
let delegateCopy = null;
function enableCopy(){
// we are inside an event handler, thus, a function in this
// context can copy.
// What I will do is to export to the variable delegateCopy
// a function that will run in this context.
delegateCopy = (html) => copyFormatted(html)
// But this function expires soon
COPY_TIMEOUT=1.0; // one second to be called
setTimeout(() => delegateCopy = null, 1000 * COPY_TIMEOUT)
}
function showOkButton(){
document.querySelector('#confirm-copy').style.display = 'inline-block';
}
function hideOkButton() {
document.querySelector('#confirm-copy').style.display = 'none';
}
// now copy to clipboard
async function doCopy(NSECONDS=10, NROWS=10){
enableCopy()
const html = await genHtml(NSECONDS, NROWS)
// if the copy delegate expired show the ok button
if(delegateCopy === null){
showOkButton();
// wait for some event that will enable copy
while(delegateCopy === null){
await sleepFor(100);
}
}
delegateCopy(html);
hideOkButton()
}
document.querySelector('#copy-0p5s').addEventListener('click', (event) => {doCopy(0.5, 10)})
document.querySelector('#copy-2s').addEventListener('click', (event) => {doCopy(2, 10)})
document.querySelector('#copy-10s').addEventListener('click', (event) => {doCopy(10, 10)})
document.querySelector('#copy-30s').addEventListener('click', (event) => {doCopy(30, 1000)})
document.querySelector('#copy-60s').addEventListener('click', (event) => {doCopy(60, 1e5)})
document.querySelector('#confirm-copy').addEventListener('click', () => enableCopy())
<button id="copy-0p5s">Copy in 0.5s</button>
<button id="copy-2s">Copy in 2s</button>
<button id="copy-10s">Copy in 10s</button>
<button id="copy-30s">Copy in 30s</button>
<button id="copy-60s">Copy in 1 min (large data)</button>
<div id="progress"></div>
<button id="confirm-copy" style="display: none;">OK</button>
<hr>
Paste here if you want.
<div contenteditable="true" style="background: #f0f0f0; margin: 5px; border-radius: 5px; border: 1px solid black;"> </div>
// This function expects an HTML string and copies it as rich text.
//
function copyFormatted (html) {
// Create container for the HTML
// [1]
var container = document.createElement('div')
container.innerHTML = html
// Hide element
// [2]
container.style.position = 'fixed'
container.style.pointerEvents = 'none'
container.style.opacity = 0
// Detect all style sheets of the page
var activeSheets = Array.prototype.slice.call(document.styleSheets)
.filter(function (sheet) {
return !sheet.disabled
})
// Mount the container to the DOM to make `contentWindow` available
// [3]
document.body.appendChild(container)
// Copy to clipboard
// [4]
window.getSelection().removeAllRanges()
var range = document.createRange()
range.selectNode(container)
window.getSelection().addRange(range)
// [5.1]
document.execCommand('copy')
// [5.2]
for (var i = 0; i < activeSheets.length; i++) activeSheets[i].disabled = true
// [5.3]
document.execCommand('copy')
// [5.4]
for (var i = 0; i < activeSheets.length; i++) activeSheets[i].disabled = false
// Remove the container
// [6]
document.body.removeChild(container)
}
function sleepFor( sleepDuration ){
// sleep asynchronously
return new Promise((resolve) => setTimeout(resolve, sleepDuration))
}
async function genHtml(NSECONDS=10, NROWS=10){
var sall='<html><table>'
var srow='<tr><td ><div style="text-align: right"><span style="color: #060606; ">1</span></div></td><td ><div style="text-align: right"><span style="color: #060606; ">Feb 27, 2018</span></div></td><td ><div style="text-align: right"><span style="color: #060606; ">315965</span></div></td><td ><div style="text-align: left"><span style="color: #060606; ">CA</span></div></td><td ><div style="text-align: left"><span style="color: #060606; ">SDBUY</span></div></td><td ><div style="text-align: right"><span style="color: #060606; ">9.99</span></div></td><td ><div style="text-align: left"><span style="color: #060606; ">CAD</span></div></td><td ><div style="text-align: right"><span style="color: #060606; ">7.88</span></div></td></tr>'
BATCH = Math.max(1, Math.floor(NROWS / 10000))
for (i=0; i<NROWS; i++) {
sall += srow; // process a chunk of data
if(i % BATCH === 0){
updateProgress((i+1) / NROWS);
}
await sleepFor(1000 * NSECONDS / NROWS);
if (i==(1e6-1)) console.log('Done')
}
sall += '</table></html>'
return sall;
}
let lastProgress = '';
function updateProgress(progress){
const progressText = (100 * progress).toFixed(2) + '%';
// prevents updating UI very frequently
if(progressText !== lastProgress){
const progressElement = document.querySelector('#progress');
progressElement.textContent = lastProgress = progressText
}
}
let delegateCopy = null;
function enableCopy(){
// we are inside an event handler, thus, a function in this
// context can copy.
// What I will do is to export to the variable delegateCopy
// a function that will run in this context.
delegateCopy = (html) => copyFormatted(html)
// But this function expires soon
COPY_TIMEOUT=1.0; // one second to be called
setTimeout(() => delegateCopy = null, 1000 * COPY_TIMEOUT)
}
function showOkButton(){
document.querySelector('#confirm-copy').style.display = 'inline-block';
}
function hideOkButton() {
document.querySelector('#confirm-copy').style.display = 'none';
}
// now copy to clipboard
async function doCopy(NSECONDS=10, NROWS=10){
enableCopy()
const html = await genHtml(NSECONDS, NROWS)
// if the copy delegate expired show the ok button
if(delegateCopy === null){
showOkButton();
// wait for some event that will enable copy
while(delegateCopy === null){
await sleepFor(100);
}
}
delegateCopy(html);
hideOkButton()
}
document.querySelector('#copy-0p5s').addEventListener('click', (event) => {doCopy(0.5, 10)})
document.querySelector('#copy-2s').addEventListener('click', (event) => {doCopy(2, 10)})
document.querySelector('#copy-10s').addEventListener('click', (event) => {doCopy(10, 10)})
document.querySelector('#copy-30s').addEventListener('click', (event) => {doCopy(30, 1000)})
document.querySelector('#copy-60s').addEventListener('click', (event) => {doCopy(60, 1e5)})
document.querySelector('#confirm-copy').addEventListener('click', () => enableCopy())
<button id="copy-0p5s">Copy in 0.5s</button>
<button id="copy-2s">Copy in 2s</button>
<button id="copy-10s">Copy in 10s</button>
<button id="copy-30s">Copy in 30s</button>
<button id="copy-60s">Copy in 1 min (large data)</button>
<div id="progress"></div>
<button id="confirm-copy" style="display: none;">OK</button>
<hr>
Paste here if you want.
<div contenteditable="true" style="background: #f0f0f0; margin: 5px; border-radius: 5px; border: 1px solid black;"> </div>
上述解决方案需要用户在数据完成后点击OK
来执行复制。我知道这不是你想要的,但浏览器需要用户干预。
在序列中我有一个修改版本,我使用 mousemove
事件刷新 copyDelegate,如果鼠标从数据准备结束移动不到一秒,OK 按钮将不会显示.您还可以使用 keypress
或任何其他频繁用户生成的事件。
// This function expects an HTML string and copies it as rich text.
//
function copyFormatted (html) {
// Create container for the HTML
// [1]
var container = document.createElement('div')
container.innerHTML = html
// Hide element
// [2]
container.style.position = 'fixed'
container.style.pointerEvents = 'none'
container.style.opacity = 0
// Detect all style sheets of the page
var activeSheets = Array.prototype.slice.call(document.styleSheets)
.filter(function (sheet) {
return !sheet.disabled
})
// Mount the container to the DOM to make `contentWindow` available
// [3]
document.body.appendChild(container)
// Copy to clipboard
// [4]
window.getSelection().removeAllRanges()
var range = document.createRange()
range.selectNode(container)
window.getSelection().addRange(range)
// [5.1]
document.execCommand('copy')
// [5.2]
for (var i = 0; i < activeSheets.length; i++) activeSheets[i].disabled = true
// [5.3]
document.execCommand('copy')
// [5.4]
for (var i = 0; i < activeSheets.length; i++) activeSheets[i].disabled = false
// Remove the container
// [6]
document.body.removeChild(container)
}
function sleepFor( sleepDuration ){
// sleep asynchronously
return new Promise((resolve) => setTimeout(resolve, sleepDuration))
}
async function genHtml(NSECONDS=10, NROWS=10){
var sall='<html><table>'
var srow='<tr><td ><div style="text-align: right"><span style="color: #060606; ">1</span></div></td><td ><div style="text-align: right"><span style="color: #060606; ">Feb 27, 2018</span></div></td><td ><div style="text-align: right"><span style="color: #060606; ">315965</span></div></td><td ><div style="text-align: left"><span style="color: #060606; ">CA</span></div></td><td ><div style="text-align: left"><span style="color: #060606; ">SDBUY</span></div></td><td ><div style="text-align: right"><span style="color: #060606; ">9.99</span></div></td><td ><div style="text-align: left"><span style="color: #060606; ">CAD</span></div></td><td ><div style="text-align: right"><span style="color: #060606; ">7.88</span></div></td></tr>'
BATCH = Math.max(1, Math.floor(NROWS / 10000))
for (i=0; i<NROWS; i++) {
sall += srow; // process a chunk of data
if(i % BATCH === 0){
updateProgress((i+1) / NROWS);
}
await sleepFor(1000 * NSECONDS / NROWS);
if (i==(1e6-1)) console.log('Done')
}
sall += '</table></html>'
return sall;
}
let lastProgress = '';
function updateProgress(progress){
const progressText = (100 * progress).toFixed(2) + '%';
// prevents updating UI very frequently
if(progressText !== lastProgress){
const progressElement = document.querySelector('#progress');
progressElement.textContent = lastProgress = progressText
}
}
let delegateCopy = null;
function enableCopy(){
// we are inside an event handler, thus, a function in this
// context can copy.
// What I will do is to export to the variable delegateCopy
// a function that will run in this context.
delegateCopy = (html) => copyFormatted(html)
// But this function expires soon
COPY_TIMEOUT=1.0; // one second to be called
setTimeout(() => delegateCopy = null, 1000 * COPY_TIMEOUT)
}
function showOkButton(){
document.querySelector('#confirm-copy').style.display = 'inline-block';
}
function hideOkButton() {
document.querySelector('#confirm-copy').style.display = 'none';
}
// now copy to clipboard
async function doCopy(NSECONDS=10, NROWS=10){
enableCopy()
const html = await genHtml(NSECONDS, NROWS)
// if the copy delegate expired show the ok button
if(delegateCopy === null){
showOkButton();
// wait for some event that will enable copy
while(delegateCopy === null){
await sleepFor(100);
}
}
delegateCopy(html);
hideOkButton()
}
document.querySelector('#copy-0p5s').addEventListener('click', (event) => {doCopy(0.5, 10)})
document.querySelector('#copy-2s').addEventListener('click', (event) => {doCopy(2, 10)})
document.querySelector('#copy-10s').addEventListener('click', (event) => {doCopy(10, 10)})
document.querySelector('#copy-30s').addEventListener('click', (event) => {doCopy(30, 1000)})
document.querySelector('#copy-60s').addEventListener('click', (event) => {doCopy(60, 1e5)})
document.querySelector('#confirm-copy').addEventListener('click', () => enableCopy())
// mouse move happens all the time this prevents the OK button from appearing
document.addEventListener('mousemove', () => enableCopy())
<button id="copy-0p5s">Copy in 0.5s</button>
<button id="copy-2s">Copy in 2s</button>
<button id="copy-10s">Copy in 10s</button>
<button id="copy-30s">Copy in 30s</button>
<button id="copy-60s">Copy in 1 min (large data)</button>
<div id="progress"></div>
<button id="confirm-copy" style="display: none;">OK</button>
<hr>
Paste here if you want.
<div contenteditable="true" style="background: #f0f0f0; margin: 5px; border-radius: 5px; border: 1px solid black;"> </div>
clipboardData.setData
对于 ClipboardEvents,您可以访问剪贴板,特别是当您复制它时会生成一个 ClipboardEvent,您可以使用格式 text/html
的方法 setData
。此方法的局限性在于它 setData 必须 运行 同步 ,在事件处理程序 returns 之后它被禁用,因此您无法显示进度条或这些东西。
document.body.addEventListener('copy', (event) =>{
const t = Number(document.querySelector('#delay').value)
const copyNow = () => {
console.log('Delay of ' + (t / 1000) + ' second')
event.clipboardData.setData('text/html',
'<table><tr><td>Hello</td><td>' + t / 1000 +'s delay</td></tr>' +
'<td></td><td>clipboard</td></tr></table>')
}
if(t === 0){
copyNow()
}else{
setTimeout(copyNow, t)
}
event.preventDefault()
})
Perform copy after
<select id="delay">
<option value="0" selected="true">immediately</option>
<option value="1">1 millisecond</option>
<option value="500">0.5 second</option>
<option value="1000">1 second</option>
<option value="2000">2 second</option>
<option value="10000">10 second</option>
<option value="20000">20 second</option>
<option value="20000">30 second</option>
</select>
<p>
Paste here if you want.
</p>
<div contenteditable="true" style="background: #f0f0f0; margin: 5px; border-radius: 5px; border: 1px solid black;"> </div>
您确实可以利用最初的用户生成事件来启动新的复制事件。当您在事件处理程序中时,您检查数据是否准备就绪并将其写入剪贴板。但是浏览器足够智能,可以在代码未 运行 最近生成的事件处理程序中阻止您复制。
function DelayedClipboardAccess() {
let data = null;
let format = 'text/html';
let timeout = 0;
const copyHandler = (event) => {
// this will be invoked on copy
// if there is data to be copied then it will
// it will set clipboard data, otherwise it will fire
// another copy in the near future.
if(timeout > 0){
const delay = Math.min(100, timeout);
setTimeout( () => {
this.countdown(timeout -= delay)
document.execCommand('copy')
}, delay);
event.preventDefault()
}else if(data) {
console.log('setData')
event.clipboardData.setData(format, data);
event.preventDefault()
data = null;
}else{
console.log({timeout, data})
}
}
document.addEventListener('copy', copyHandler)
this.countdown = (time) => {}
this.delayedCopy = (_data, _timeout, _format) => {
format = _format || 'text/html';
data = _data;
timeout = _timeout;
document.execCommand('copy');
}
}
const countdownEl = document.querySelector('#countdown')
const delayEl = document.querySelector('#delay')
const copyAgain = document.querySelector('#copy-again')
const clipboard = new DelayedClipboardAccess()
function delayedCopy() {
const t = Number(delayEl.value)
const data = '<table><tr><td>Hello</td><td>' + t / 1000 +'s delay</td></tr>' +
'<td></td><td>clipboard</td></tr></table>';
clipboard.delayedCopy(data, t, 'text/html')
}
clipboard.countdown = (t) => countdownEl.textContent = t;
delayEl.addEventListener('change', delayedCopy)
copyAgain.addEventListener('click', delayedCopy)
Perform copy after
<select id="delay">
<option value="0" selected="true">immediately</option>
<option value="1">1 millisecond</option>
<option value="500">0.5 second</option>
<option value="1000">1 second</option>
<option value="2000">2 second</option>
<option value="10000">10 second</option>
<option value="20000">20 second</option>
<option value="30000">30 second</option>
</select>
<button id="copy-again">Copy again</button>
<div id="countdown"></div>
<p>
Paste here if you want.
</p>
<div contenteditable="true" style="background: #f0f0f0; margin: 5px; border-radius: 5px; border: 1px solid black;"> </div>
在上面的片段中有一个有趣的效果,如果你在倒计时 运行ning 时复制一些东西,你将开始一个新的链来加速倒计时。
在我的浏览器中,倒计时在大约 5 秒后停止。如果我在复制处理程序链中断后按 Ctrl+C,复制处理程序将由用户生成的事件再次调用,然后再次持续 5 秒。
这是我发现的:
在这个脚本中:
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
我select all and copy时用到这个函数:
脚本: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")
,它只会复制您当前的 selection,就像您按下 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 Whosebug, but works on my website.
//Use this to disable it for testing on Whosebug
//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:
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:
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
有用的链接
我注意到当 运行 在后台时,document.execCommand('copy')
命令在大约 5 秒后超时。有没有办法绕过这个限制,或者如果需要更长的时间,也许可以回退?
这是我一直用于 Clipboard docs 的页面。例如,我有一个函数 'prepares' 数据(从表格数据生成 html),然后是第二个函数,它使用一些额外的标记将它复制到剪贴板。在大型表上,从用户按下 Cmd-C 到生成 html 并能够被复制,这通常可能需要十秒钟。
此外,我注意到 Google 表格允许 复制 操作超过五秒,所以我很好奇他们会怎么做:
# still works after 25 seconds!
[Violation] 'copy' handler took 25257ms 2217559571-waffle_js_prod_core.js:337
代码是 minified/obfuscated,因此很难阅读,但这是上面的文件: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。
来自同一个 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();
});
我怀疑这是否能解决您的实际问题,但输入评论的内容太多了。不过,我希望任何导致我们所看到的行为差异的因素都能有所帮助。
来自 execCommand method 它说
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.
因此您的数据准备可能需要多长时间,但是对 execCommand('copy')
的调用必须在用户生成其处理程序为 运行 的事件后立即执行。
显然它可以是任何事件处理程序,而不仅仅是复制事件。
copyFormatted
执行复制。genHtml
函数异步生成 HTML 数据。enableCopy
将在允许复制的上下文中创建的函数分配给delegateCopy
,该函数将在一秒后过期(将 null 分配给delegateCopy
)
有人提到使用 clipboardData
的可能性,虽然这个界面更具编程性,但它也需要最近的用户交互,这是我关注的问题。当然,使用 setData
的好处是不需要解析 HTML 并为复制的数据创建 DOM,在动机示例中是大量数据。此外 ClipboardData 被标记为实验性的。
下面的片段显示了一个解决方案,(1) 运行 是异步的,(2) 如果认为有必要,请求用户交互,(3) 如果可能,使用 setData,(3) 如果 setData
不可用则使用内部 HTML -> select。复制方法。
// This function expects an HTML string and copies it as rich text.
//
function copyFormatted (html) {
// Create container for the HTML
console.log('copyFormatted')
var container = document.createElement('div')
let hasClipboardData = true;
const selectContainer = () => {
const range = document.createRange()
range.selectNode(container)
window.getSelection().addRange(range)
}
const copyHandler = (event) => {
console.log('copyHandler')
event.stopPropagation()
if(hasClipboardData){
if(event.clipboardData && event.clipboardData.setData){
console.log('setData skip html rendering')
event.clipboardData.setData('text/html', html);
event.preventDefault();
} else {
hasClipboardData = false;
container.innerHTML = html;
selectContainer();
document.execCommand('copy');
return; // don't remove the element yet
}
}
document.body.removeChild(container);
document.removeEventListener('copy', copyHandler)
}
// Mount the container to the DOM to make `contentWindow` available
document.body.appendChild(container)
document.addEventListener('copy', copyHandler);
selectContainer();
document.execCommand('copy')
}
function sleepFor( sleepDuration ){
// sleep asynchronously
return new Promise((resolve) => setTimeout(resolve, sleepDuration))
}
async function genHtml(NSECONDS=10, NROWS=10){
var sall='<html><table>'
var srow='<tr><td ><div style="text-align: right"><span style="color: #060606; ">1</span></div></td><td ><div style="text-align: right"><span style="color: #060606; ">Feb 27, 2018</span></div></td><td ><div style="text-align: right"><span style="color: #060606; ">315965</span></div></td><td ><div style="text-align: left"><span style="color: #060606; ">CA</span></div></td><td ><div style="text-align: left"><span style="color: #060606; ">SDBUY</span></div></td><td ><div style="text-align: right"><span style="color: #060606; ">9.99</span></div></td><td ><div style="text-align: left"><span style="color: #060606; ">CAD</span></div></td><td ><div style="text-align: right"><span style="color: #060606; ">7.88</span></div></td></tr>'
BATCH = Math.max(1, Math.floor(NROWS / 10000))
for (i=0; i<NROWS; i++) {
sall += srow; // process a chunk of data
if(i % BATCH === 0){
updateProgress((i+1) / NROWS);
}
await sleepFor(1000 * NSECONDS / NROWS);
if (i==(1e6-1)) console.log('Done')
}
sall += '</table></html>'
return sall;
}
let lastProgress = '';
function updateProgress(progress){
const progressText = (100 * progress).toFixed(2) + '%';
// prevents updating UI very frequently
if(progressText !== lastProgress){
const progressElement = document.querySelector('#progress');
progressElement.textContent = lastProgress = progressText
}
}
let delegateCopy = null;
function enableCopy(){
// we are inside an event handler, thus, a function in this
// context can copy.
// What I will do is to export to the variable delegateCopy
// a function that will run in this context.
delegateCopy = (html) => copyFormatted(html)
// But this function expires soon
COPY_TIMEOUT=1.0; // one second to be called
setTimeout(() => delegateCopy = null, 1000 * COPY_TIMEOUT)
}
function showOkButton(){
document.querySelector('#confirm-copy').style.display = 'inline-block';
}
function hideOkButton() {
document.querySelector('#confirm-copy').style.display = 'none';
}
// now copy to clipboard
async function doCopy(NSECONDS=10, NROWS=10){
enableCopy()
const html = await genHtml(NSECONDS, NROWS)
// if the copy delegate expired show the ok button
if(delegateCopy === null){
showOkButton();
// wait for some event that will enable copy
while(delegateCopy === null){
await sleepFor(100);
}
}
delegateCopy(html);
hideOkButton()
}
document.querySelector('#copy-0p5s').addEventListener('click', (event) => {doCopy(0.5, 10)})
document.querySelector('#copy-2s').addEventListener('click', (event) => {doCopy(2, 10)})
document.querySelector('#copy-10s').addEventListener('click', (event) => {doCopy(10, 10)})
document.querySelector('#copy-30s').addEventListener('click', (event) => {doCopy(30, 1000)})
document.querySelector('#copy-60s').addEventListener('click', (event) => {doCopy(60, 1e5)})
document.querySelector('#confirm-copy').addEventListener('click', () => enableCopy())
<button id="copy-0p5s">Copy in 0.5s</button>
<button id="copy-2s">Copy in 2s</button>
<button id="copy-10s">Copy in 10s</button>
<button id="copy-30s">Copy in 30s</button>
<button id="copy-60s">Copy in 1 min (large data)</button>
<div id="progress"></div>
<button id="confirm-copy" style="display: none;">OK</button>
<hr>
Paste here if you want.
<div contenteditable="true" style="background: #f0f0f0; margin: 5px; border-radius: 5px; border: 1px solid black;"> </div>
// This function expects an HTML string and copies it as rich text.
//
function copyFormatted (html) {
// Create container for the HTML
// [1]
var container = document.createElement('div')
container.innerHTML = html
// Hide element
// [2]
container.style.position = 'fixed'
container.style.pointerEvents = 'none'
container.style.opacity = 0
// Detect all style sheets of the page
var activeSheets = Array.prototype.slice.call(document.styleSheets)
.filter(function (sheet) {
return !sheet.disabled
})
// Mount the container to the DOM to make `contentWindow` available
// [3]
document.body.appendChild(container)
// Copy to clipboard
// [4]
window.getSelection().removeAllRanges()
var range = document.createRange()
range.selectNode(container)
window.getSelection().addRange(range)
// [5.1]
document.execCommand('copy')
// [5.2]
for (var i = 0; i < activeSheets.length; i++) activeSheets[i].disabled = true
// [5.3]
document.execCommand('copy')
// [5.4]
for (var i = 0; i < activeSheets.length; i++) activeSheets[i].disabled = false
// Remove the container
// [6]
document.body.removeChild(container)
}
function sleepFor( sleepDuration ){
// sleep asynchronously
return new Promise((resolve) => setTimeout(resolve, sleepDuration))
}
async function genHtml(NSECONDS=10, NROWS=10){
var sall='<html><table>'
var srow='<tr><td ><div style="text-align: right"><span style="color: #060606; ">1</span></div></td><td ><div style="text-align: right"><span style="color: #060606; ">Feb 27, 2018</span></div></td><td ><div style="text-align: right"><span style="color: #060606; ">315965</span></div></td><td ><div style="text-align: left"><span style="color: #060606; ">CA</span></div></td><td ><div style="text-align: left"><span style="color: #060606; ">SDBUY</span></div></td><td ><div style="text-align: right"><span style="color: #060606; ">9.99</span></div></td><td ><div style="text-align: left"><span style="color: #060606; ">CAD</span></div></td><td ><div style="text-align: right"><span style="color: #060606; ">7.88</span></div></td></tr>'
BATCH = Math.max(1, Math.floor(NROWS / 10000))
for (i=0; i<NROWS; i++) {
sall += srow; // process a chunk of data
if(i % BATCH === 0){
updateProgress((i+1) / NROWS);
}
await sleepFor(1000 * NSECONDS / NROWS);
if (i==(1e6-1)) console.log('Done')
}
sall += '</table></html>'
return sall;
}
let lastProgress = '';
function updateProgress(progress){
const progressText = (100 * progress).toFixed(2) + '%';
// prevents updating UI very frequently
if(progressText !== lastProgress){
const progressElement = document.querySelector('#progress');
progressElement.textContent = lastProgress = progressText
}
}
let delegateCopy = null;
function enableCopy(){
// we are inside an event handler, thus, a function in this
// context can copy.
// What I will do is to export to the variable delegateCopy
// a function that will run in this context.
delegateCopy = (html) => copyFormatted(html)
// But this function expires soon
COPY_TIMEOUT=1.0; // one second to be called
setTimeout(() => delegateCopy = null, 1000 * COPY_TIMEOUT)
}
function showOkButton(){
document.querySelector('#confirm-copy').style.display = 'inline-block';
}
function hideOkButton() {
document.querySelector('#confirm-copy').style.display = 'none';
}
// now copy to clipboard
async function doCopy(NSECONDS=10, NROWS=10){
enableCopy()
const html = await genHtml(NSECONDS, NROWS)
// if the copy delegate expired show the ok button
if(delegateCopy === null){
showOkButton();
// wait for some event that will enable copy
while(delegateCopy === null){
await sleepFor(100);
}
}
delegateCopy(html);
hideOkButton()
}
document.querySelector('#copy-0p5s').addEventListener('click', (event) => {doCopy(0.5, 10)})
document.querySelector('#copy-2s').addEventListener('click', (event) => {doCopy(2, 10)})
document.querySelector('#copy-10s').addEventListener('click', (event) => {doCopy(10, 10)})
document.querySelector('#copy-30s').addEventListener('click', (event) => {doCopy(30, 1000)})
document.querySelector('#copy-60s').addEventListener('click', (event) => {doCopy(60, 1e5)})
document.querySelector('#confirm-copy').addEventListener('click', () => enableCopy())
<button id="copy-0p5s">Copy in 0.5s</button>
<button id="copy-2s">Copy in 2s</button>
<button id="copy-10s">Copy in 10s</button>
<button id="copy-30s">Copy in 30s</button>
<button id="copy-60s">Copy in 1 min (large data)</button>
<div id="progress"></div>
<button id="confirm-copy" style="display: none;">OK</button>
<hr>
Paste here if you want.
<div contenteditable="true" style="background: #f0f0f0; margin: 5px; border-radius: 5px; border: 1px solid black;"> </div>
上述解决方案需要用户在数据完成后点击OK
来执行复制。我知道这不是你想要的,但浏览器需要用户干预。
在序列中我有一个修改版本,我使用 mousemove
事件刷新 copyDelegate,如果鼠标从数据准备结束移动不到一秒,OK 按钮将不会显示.您还可以使用 keypress
或任何其他频繁用户生成的事件。
// This function expects an HTML string and copies it as rich text.
//
function copyFormatted (html) {
// Create container for the HTML
// [1]
var container = document.createElement('div')
container.innerHTML = html
// Hide element
// [2]
container.style.position = 'fixed'
container.style.pointerEvents = 'none'
container.style.opacity = 0
// Detect all style sheets of the page
var activeSheets = Array.prototype.slice.call(document.styleSheets)
.filter(function (sheet) {
return !sheet.disabled
})
// Mount the container to the DOM to make `contentWindow` available
// [3]
document.body.appendChild(container)
// Copy to clipboard
// [4]
window.getSelection().removeAllRanges()
var range = document.createRange()
range.selectNode(container)
window.getSelection().addRange(range)
// [5.1]
document.execCommand('copy')
// [5.2]
for (var i = 0; i < activeSheets.length; i++) activeSheets[i].disabled = true
// [5.3]
document.execCommand('copy')
// [5.4]
for (var i = 0; i < activeSheets.length; i++) activeSheets[i].disabled = false
// Remove the container
// [6]
document.body.removeChild(container)
}
function sleepFor( sleepDuration ){
// sleep asynchronously
return new Promise((resolve) => setTimeout(resolve, sleepDuration))
}
async function genHtml(NSECONDS=10, NROWS=10){
var sall='<html><table>'
var srow='<tr><td ><div style="text-align: right"><span style="color: #060606; ">1</span></div></td><td ><div style="text-align: right"><span style="color: #060606; ">Feb 27, 2018</span></div></td><td ><div style="text-align: right"><span style="color: #060606; ">315965</span></div></td><td ><div style="text-align: left"><span style="color: #060606; ">CA</span></div></td><td ><div style="text-align: left"><span style="color: #060606; ">SDBUY</span></div></td><td ><div style="text-align: right"><span style="color: #060606; ">9.99</span></div></td><td ><div style="text-align: left"><span style="color: #060606; ">CAD</span></div></td><td ><div style="text-align: right"><span style="color: #060606; ">7.88</span></div></td></tr>'
BATCH = Math.max(1, Math.floor(NROWS / 10000))
for (i=0; i<NROWS; i++) {
sall += srow; // process a chunk of data
if(i % BATCH === 0){
updateProgress((i+1) / NROWS);
}
await sleepFor(1000 * NSECONDS / NROWS);
if (i==(1e6-1)) console.log('Done')
}
sall += '</table></html>'
return sall;
}
let lastProgress = '';
function updateProgress(progress){
const progressText = (100 * progress).toFixed(2) + '%';
// prevents updating UI very frequently
if(progressText !== lastProgress){
const progressElement = document.querySelector('#progress');
progressElement.textContent = lastProgress = progressText
}
}
let delegateCopy = null;
function enableCopy(){
// we are inside an event handler, thus, a function in this
// context can copy.
// What I will do is to export to the variable delegateCopy
// a function that will run in this context.
delegateCopy = (html) => copyFormatted(html)
// But this function expires soon
COPY_TIMEOUT=1.0; // one second to be called
setTimeout(() => delegateCopy = null, 1000 * COPY_TIMEOUT)
}
function showOkButton(){
document.querySelector('#confirm-copy').style.display = 'inline-block';
}
function hideOkButton() {
document.querySelector('#confirm-copy').style.display = 'none';
}
// now copy to clipboard
async function doCopy(NSECONDS=10, NROWS=10){
enableCopy()
const html = await genHtml(NSECONDS, NROWS)
// if the copy delegate expired show the ok button
if(delegateCopy === null){
showOkButton();
// wait for some event that will enable copy
while(delegateCopy === null){
await sleepFor(100);
}
}
delegateCopy(html);
hideOkButton()
}
document.querySelector('#copy-0p5s').addEventListener('click', (event) => {doCopy(0.5, 10)})
document.querySelector('#copy-2s').addEventListener('click', (event) => {doCopy(2, 10)})
document.querySelector('#copy-10s').addEventListener('click', (event) => {doCopy(10, 10)})
document.querySelector('#copy-30s').addEventListener('click', (event) => {doCopy(30, 1000)})
document.querySelector('#copy-60s').addEventListener('click', (event) => {doCopy(60, 1e5)})
document.querySelector('#confirm-copy').addEventListener('click', () => enableCopy())
// mouse move happens all the time this prevents the OK button from appearing
document.addEventListener('mousemove', () => enableCopy())
<button id="copy-0p5s">Copy in 0.5s</button>
<button id="copy-2s">Copy in 2s</button>
<button id="copy-10s">Copy in 10s</button>
<button id="copy-30s">Copy in 30s</button>
<button id="copy-60s">Copy in 1 min (large data)</button>
<div id="progress"></div>
<button id="confirm-copy" style="display: none;">OK</button>
<hr>
Paste here if you want.
<div contenteditable="true" style="background: #f0f0f0; margin: 5px; border-radius: 5px; border: 1px solid black;"> </div>
clipboardData.setData
对于 ClipboardEvents,您可以访问剪贴板,特别是当您复制它时会生成一个 ClipboardEvent,您可以使用格式 text/html
的方法 setData
。此方法的局限性在于它 setData 必须 运行 同步 ,在事件处理程序 returns 之后它被禁用,因此您无法显示进度条或这些东西。
document.body.addEventListener('copy', (event) =>{
const t = Number(document.querySelector('#delay').value)
const copyNow = () => {
console.log('Delay of ' + (t / 1000) + ' second')
event.clipboardData.setData('text/html',
'<table><tr><td>Hello</td><td>' + t / 1000 +'s delay</td></tr>' +
'<td></td><td>clipboard</td></tr></table>')
}
if(t === 0){
copyNow()
}else{
setTimeout(copyNow, t)
}
event.preventDefault()
})
Perform copy after
<select id="delay">
<option value="0" selected="true">immediately</option>
<option value="1">1 millisecond</option>
<option value="500">0.5 second</option>
<option value="1000">1 second</option>
<option value="2000">2 second</option>
<option value="10000">10 second</option>
<option value="20000">20 second</option>
<option value="20000">30 second</option>
</select>
<p>
Paste here if you want.
</p>
<div contenteditable="true" style="background: #f0f0f0; margin: 5px; border-radius: 5px; border: 1px solid black;"> </div>
您确实可以利用最初的用户生成事件来启动新的复制事件。当您在事件处理程序中时,您检查数据是否准备就绪并将其写入剪贴板。但是浏览器足够智能,可以在代码未 运行 最近生成的事件处理程序中阻止您复制。
function DelayedClipboardAccess() {
let data = null;
let format = 'text/html';
let timeout = 0;
const copyHandler = (event) => {
// this will be invoked on copy
// if there is data to be copied then it will
// it will set clipboard data, otherwise it will fire
// another copy in the near future.
if(timeout > 0){
const delay = Math.min(100, timeout);
setTimeout( () => {
this.countdown(timeout -= delay)
document.execCommand('copy')
}, delay);
event.preventDefault()
}else if(data) {
console.log('setData')
event.clipboardData.setData(format, data);
event.preventDefault()
data = null;
}else{
console.log({timeout, data})
}
}
document.addEventListener('copy', copyHandler)
this.countdown = (time) => {}
this.delayedCopy = (_data, _timeout, _format) => {
format = _format || 'text/html';
data = _data;
timeout = _timeout;
document.execCommand('copy');
}
}
const countdownEl = document.querySelector('#countdown')
const delayEl = document.querySelector('#delay')
const copyAgain = document.querySelector('#copy-again')
const clipboard = new DelayedClipboardAccess()
function delayedCopy() {
const t = Number(delayEl.value)
const data = '<table><tr><td>Hello</td><td>' + t / 1000 +'s delay</td></tr>' +
'<td></td><td>clipboard</td></tr></table>';
clipboard.delayedCopy(data, t, 'text/html')
}
clipboard.countdown = (t) => countdownEl.textContent = t;
delayEl.addEventListener('change', delayedCopy)
copyAgain.addEventListener('click', delayedCopy)
Perform copy after
<select id="delay">
<option value="0" selected="true">immediately</option>
<option value="1">1 millisecond</option>
<option value="500">0.5 second</option>
<option value="1000">1 second</option>
<option value="2000">2 second</option>
<option value="10000">10 second</option>
<option value="20000">20 second</option>
<option value="30000">30 second</option>
</select>
<button id="copy-again">Copy again</button>
<div id="countdown"></div>
<p>
Paste here if you want.
</p>
<div contenteditable="true" style="background: #f0f0f0; margin: 5px; border-radius: 5px; border: 1px solid black;"> </div>
在上面的片段中有一个有趣的效果,如果你在倒计时 运行ning 时复制一些东西,你将开始一个新的链来加速倒计时。
在我的浏览器中,倒计时在大约 5 秒后停止。如果我在复制处理程序链中断后按 Ctrl+C,复制处理程序将由用户生成的事件再次调用,然后再次持续 5 秒。
这是我发现的:
在这个脚本中: 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
我select all and copy时用到这个函数:
脚本: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")
,它只会复制您当前的 selection,就像您按下 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 Whosebug, but works on my website.
//Use this to disable it for testing on Whosebug
//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:
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:
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
有用的链接