javascript - 剪贴板 API 和操作系统之间不一致的浏览器行为
问题描述
我正在尝试解决一个问题,即我们的 React 应用程序的 Windows 用户无法复制和粘贴非 Windows 用户可以复制和粘贴的内容。
我们在 react-modal 窗口中显示内容,带有一个按钮,单击该按钮时,将突出显示要复制的区域,并复制到剪贴板缓冲区以进行粘贴。
我们在按钮上的 click 事件触发的 onCopy 事件中运行以下内容:
const range = document.createRange();
range.selectNode(this.getCiteTextNode()); // function to get the node
window.getSelection().removeAllRanges();
window.getSelection().addRange(range);
const listener = document.addEventListener("copy", e => {
const outerHTML = this.getCitationTextWithoutStyle(); //function strips styling attributes
e.clipboardData.setData("text/html", outerHTML);
e.preventDefault();
});
document.execCommand("copy");
document.removeEventListener("copy", listener);
我观察到的是,虽然 MacOS 上的浏览器(Safari、Firefox、Chrome)将允许这种复制/粘贴操作发生,但在 Windows、Edge、FF 和 Chrome 中会静默失败(剪贴板缓冲区中没有任何内容),即使程序化在这些浏览器中已授予访问权限。更重要的是,任何复制粘贴请求(菜单、鼠标或键盘)似乎都无声无息地失败了;我可以手动突出显示模态的不同区域,选择复制,然后尝试粘贴到另一个文档中,它根本不会复制 - 没有任何东西进入缓冲区。
但是,这在 IE11 中确实有效 - 有提示 - 但当按钮单击事件被触发时,剪贴板访问行为正常。
进一步的实验表明,当我更改preventDefault
为stopPropagation
. 一旦我这样做了,内容就会正确地进入剪贴板。
有没有人遇到过这个?防止事件冒泡如何在仅限 Windows 的浏览器中“工作”?
PS - 了解 execCommand 的草稿状态。
谢谢
解决方案
序言:这个答案试图解释 OP 中的代码在做什么,他们可能面临什么,以及他们应该如何处理它,但是,Windows 和 macOS 对我来说实际上表现相同这一事实有点削弱了它。 ..
如果我们一步一步地采取你的代码,它是
- 在 DOM 中选择一个节点。
- 执行
copy
命令
[内部浏览器逻辑]
=> 触发copy
事件 - 处理
copy
事件 - 设置事件的 DataTransfer 数据。
=> 设置outerHTML
为text/html
剪贴板 - 阻止
copy
事件的默认操作。
如果没有阻止,“默认操作”应该是,
text/HTML
=> 像在剪贴板中 一样抓取活动选择的标记。
=>toString()
像text/plain
在剪贴板中一样抓取所选内容。
也许您已经看到您的代码缺少什么:
您没有设置text/plain
复制事件的值。
只有少数应用程序会尝试text/html
从剪贴板中获取数据,大多数应用程序只会搜索text/plain
值。
所以你也需要设置它。
这是一个小游乐场*,您可以在其中看到即使在网页中,也只有某些地方允许粘贴为text/html
,而其他地方将使用该text/plain
值。
btn.onclick = e => {
const range = document.createRange();
range.selectNode(copyme); // function to get the node
const selection = getSelection();
selection.removeAllRanges();
selection.addRange(range);
const listener = e => {
// make it green in the clipboard
const outerHTML = copyme.outerHTML.replace('red', 'green');
// add some text to show we have the control
const plaintext = selection.toString() + ' plaintext';
const dT = e.clipboardData;
dT.setData("text/html", outerHTML);
// IIRC IE does only support `"text"` MIME type
dT.setData("text", plaintext);
e.preventDefault();
};
document.addEventListener("copy", listener);
document.execCommand("copy");
document.removeEventListener("copy", listener);
};
[contenteditable] {border: 1px solid;}
<button id="btn">copy</button>
<span id="copyme" style="color:red">Hello</span>
<br>
<!-- contenteditable elements will grab the text/html data -->
<div contenteditable>paste HTML here<br> </div>
<!-- <textarea> and <input> will only grab the text/plain -->
<textarea>paste plain here
</textarea>
* 我从您的原始代码中修复了一些不相关的拼写错误,例如您问题的评论中指出的不正确的事件删除。
另请注意,这并不是stopPropagation
在做任何魔术,而只是您没有调用preventDefault
.
这样做,复制 as 返回的节点的标记及其 textContent as 的默认行为将this.getCiteTextNode()
覆盖text/html
您text/plain
自己的设置。
这是一个带有复选框的示例,用于控制是否应阻止默认行为。您可以看到,当它没有被阻止时,复制的富文本仍然是red,而不是我们在事件处理程序中设置的绿色,并且纯文本仍然是节点的 textContent,而不是我们编辑的文本。
btn.onclick = e => {
const range = document.createRange();
range.selectNode(copyme); // function to get the node
const selection = getSelection();
selection.removeAllRanges();
selection.addRange(range);
const listener = e => {
// make it green in the clipboard
const outerHTML = copyme.outerHTML.replace('red', 'green');
// add some text to show we have the control
const plaintext = selection.toString() + ' plaintext';
const dT = e.clipboardData;
dT.setData("text/html", outerHTML);
// IIRC IE does only support `"text"` MIME type
dT.setData("text", plaintext);
if(prev.checked) {
e.preventDefault();
}
};
document.addEventListener("copy", listener);
document.execCommand("copy");
document.removeEventListener("copy", listener);
};
[contenteditable] {border: 1px solid;}
<label>prevent default behavior<input type="checkbox" id="prev" checked></label><br>
<button id="btn">copy</button>
<span id="copyme" style="color:red">Hello</span>
<br>
<!-- contenteditable elements will grab the text/html data -->
<div contenteditable>paste HTML here<br> </div>
<!-- <textarea> and <input> will only grab the text/plain -->
<textarea>paste plain here
</textarea>