Javascript - 将字符串复制到剪贴板作为文本/HTML

71
有没有一种方法可以在JavaScript中将HTML字符串(即<b>xx<b>)作为文本/ HTML复制到剪贴板中,以便可以在支持格式的邮件消息中(例如 Gmail消息)中粘贴(即加粗的xx)?
存在将内容复制为纯文本(text/plain)的解决方案,例如https://dev59.com/fHRC5IYBdhLWcg3wJNcN#30810322,但没有文本/ HTML的解决方案。
我需要一个不需要flash或jquery的解决方案,可以在IE11、FF42和Chrome上工作。
理想情况下,我希望将字符串的文本和HTML版本都存储在剪贴板中,以便根据目标是否支持HTML来选择正确的版本插入。

1
可能是重复的问题:JavaScript如何将富文本内容复制到剪贴板 - Alexander O'Mara
你确定吗?它看起来像是在询问如何将HTML的一部分复制为富文本,这也是你想要的。但是答案并不尽如人意。 - Alexander O'Mara
另一个问题的答案可能会有所帮助:https://dev59.com/RXVC5IYBdhLWcg3w-mZO#31945909 - Alexander O'Mara
1
干得好。由于我喜欢一些结构化的代码,我将你的工作重构为一个小的JavaScript类。请参见我的答案中的EDIT#3。 - Loilo
显示剩余3条评论
5个回答

65

由于这个答案引起了一些关注,我已经完全重写了混乱的原始内容,使其更易于理解。如果您想查看修订前的版本,可以在这里找到它。


问题简述:

我可以使用JavaScript将一些HTML代码的格式化输出复制到用户剪贴板中吗?


答案:

是的,在某些限制下,你可以做到。


解决方案:

以下是一个可以实现此目的的函数。我已经在所需的浏览器中进行了测试,它们都可以正常工作。但是,IE 11会要求确认该操作。

如何工作的说明可以在下面找到,并且您可以在此jsFiddle上交互式地测试该函数。

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

说明:

查看上面代码中的注释,以了解您当前在以下过程中所处的位置:

  1. 我们创建一个容器来放置我们的HTML代码。
  2. 我们将样式设置为隐藏并检测页面的活动样式表。原因很快就会解释。
  3. 我们将容器放入页面的DOM中。
  4. 我们删除可能存在的选择并选择我们容器的内容。
  5. 我们进行复制本身。这实际上是一个多步骤的过程: Chrome将按照其所见到的文本进行复制,应用CSS样式,而其他浏览器将使用浏览器的默认样式进行复制。 因此,为了获得最一致的结果,我们将在复制前禁用所有用户样式。

    1. 在此之前,我们会提前执行 copy 命令。 这是为IE11设计的技巧:在这个浏览器中,必须手动确认一次复制。直到用户点击“确认”按钮之前,IE用户将看到没有任何样式的页面。为了避免这种情况,我们先复制、等待确认,然后禁用样式再次复制。那时我们将不会得到确认对话框,因为IE记住了我们的上次选择。
    2. 我们实际上禁用了页面的样式。
    3. 现在我们再次执行 copy 命令。
    4. 我们重新启用样式表。
  6. 我们从页面的DOM中删除容器。

然后我们就完成了。


注意事项:

  • 不同浏览器下格式化的内容可能不会完全一致。

    如上所述,Chrome (即 Blink 引擎) 将使用与 Firefox 和 IE 不同的策略:Chrome将复制具有CSS样式的内容,但省略未定义的任何样式。

    另一方面,Firefox 和 IE 不会应用特定于页面的CSS,而是应用浏览器的默认样式。这也意味着它们将应用一些奇怪的样式,例如默认字体(通常是 Times New Roman)。

  • 出于安全原因,浏览器只允许函数作为用户交互的效果(例如单击、按键等)来执行。


这看起来很棒,Loilo!如果你能将它编写成一个函数,接受一个HTML字符串(例如'xx<b>bold</b>yy'),并将其作为富文本放入剪贴板,我会将其标记为答案。谢谢! - kofifus
1
谢谢!你觉得有没有办法同时将文本和HTML添加到剪贴板中,以便根据目标(即粘贴到记事本还是粘贴到Gmail)选择正确的版本? - kofifus
1
你只能一次将一件事复制到剪贴板上。记事本会去除样式部分。这也会以完全相同的方式发生在你复制的HTML上(尝试将从JSFiddle复制的内容粘贴到记事本中)。 - Loilo
代码仍然存在问题。我添加了一个文本区域并在按下Ctrl+C时进行复制,但是FF失败并显示“太多递归”!请参见http://jsfiddle.net/d740eo04/7/,只需在文本区域中按下ctrl+c即可。 - kofifus
如果你想像上面的例子一样做:记得区分OS X和Windows(不同的快捷键),如果你不想让用户感到困惑,复制后应该将焦点返回到文本区域,甚至恢复精确的选择。 - Loilo
显示剩余6条评论

29

有一个更简单的解决方案。复制您页面(元素)的一个部分,而不是复制HTML。

使用这个简单的函数,您可以将您页面上任何想要复制的内容(文本、图片、表格等)或整个文档复制到剪贴板上。 该函数接收元素的id或元素本身。

function copyElementToClipboard(element) {
  window.getSelection().removeAllRanges();
  let range = document.createRange();
  range.selectNode(typeof element === 'string' ? document.getElementById(element) : element);
  window.getSelection().addRange(range);
  document.execCommand('copy');
  window.getSelection().removeAllRanges();
}

如何使用:

copyElementToClipboard(document.body);
copyElementToClipboard('myImageId');

请问,能否复制几个元素(例如两个部分),或者使用循环进行复制?谢谢。 - Schroet
这个解决方案在 MacOS 上的 Chrome & Firefox 上表现良好。请注意,当我将按钮制作呼叫放在紧挨着拥有 3 个链接的 div 的按钮前面并且我从 Chrome 复制到 Gmail 时,会出现一个正方形框(按钮?)。在<button>之前添加 解决了这个问题。 - Raymond Naseef
@Schroet - 我尝试了以下几种方法,但都没有成功:[A] 对于每个元素使用selectNode创建一个范围,[B] 每个范围调用一次selection.addRange(),[C] 调用selection.addRange.apply(<selection>, ranges)。不确定为什么选择区似乎可以添加多个范围... - Raymond Naseef
这个非常好用。在Chrome和IE11中测试过了!谢谢! - Dylan Watson
然而,使用这种方法,Chrome会为您的链接添加一些额外的样式,例如字体大小:16px。 - addlistener
显示剩余2条评论

18

如果您正在寻找一种使用 ClipboardItem 进行操作的方法,即使已经将 dom.events.asyncClipboard.clipboardItem 设置为 true,仍无法使其工作,您可以在 nikouusitalo.com 中找到答案。

以下是我的可运行代码(在 Firefox 102 上测试过)。

    const clipboardItem = new
        ClipboardItem({'text/html':  new Blob([html],
                                              {type: 'text/html'}),
                       'text/plain': new Blob([html],
                                              {type: 'text/plain'})});
    navigator.clipboard.write([clipboardItem]).
            then(_ => console.log("clipboard.write() Ok"),
                 error => alert(error));

请确保您尝试将其粘贴到富文本编辑器(如 Gmail),而不是纯文本/Markdown 编辑器(如 StackOverflow)。


这是复制HTML类型内容的更好、更可靠的方法。 - Shakeel Ahmed
太好了!对于Firefox,您需要在about:config中启用:https://developer.mozilla.org/en-US/docs/Web/API/ClipboardItem - caram
1
为了支持Firefox,而不让用户自行启用此功能,您可以暂时使用这个polyfill:https://github.com/lgarron/clipboard-polyfill - undefined
节省了我好几个小时!谢谢您,先生。 - undefined

11
如果你想使用新的 Clipboard API,请像下面这样使用write方法
var type = "text/html";
var blob = new Blob([text], { type });
var data = [new ClipboardItem({ [type]: blob })];

navigator.clipboard.write(data).then(
    function () {
    /* success */
    },
    function () {
    /* failure */
    }
);

目前(2021年9月),问题在于Firefox不支持此方法


从版本87开始:此功能在dom.events.asyncClipboard.clipboardItem首选项后面(需要设置为“true”)。要在Firefox中更改首选项,请访问“about:config”。 - Pedro Lobito

9
我对Loilo上面的回答进行了一些修改:
  • 将焦点设置(然后恢复)到隐藏的div,可防止FF从textarea复制时进入无休止的递归

  • 将范围设置为div内部的子元素,可避免chrome在开头插入额外的<br>

  • getSelection()上的removeAllRanges可以防止追加到现有选择中(可能不需要)

  • try/catch围绕execCommand

  • 更好地隐藏复制div

在OSX上将无法工作。Safari不支持execCommand,chrome OSX有一个已知的错误 https://bugs.chromium.org/p/chromium/issues/detail?id=552975

代码:

clipboardDiv = document.createElement('div');
clipboardDiv.style.fontSize = '12pt'; // Prevent zooming on iOS
// Reset box model
clipboardDiv.style.border = '0';
clipboardDiv.style.padding = '0';
clipboardDiv.style.margin = '0';
// Move element out of screen 
clipboardDiv.style.position = 'fixed';
clipboardDiv.style['right'] = '-9999px';
clipboardDiv.style.top = (window.pageYOffset || document.documentElement.scrollTop) + 'px';
// more hiding
clipboardDiv.setAttribute('readonly', '');
clipboardDiv.style.opacity = 0;
clipboardDiv.style.pointerEvents = 'none';
clipboardDiv.style.zIndex = -1;
clipboardDiv.setAttribute('tabindex', '0'); // so it can be focused
clipboardDiv.innerHTML = '';
document.body.appendChild(clipboardDiv);

function copyHtmlToClipboard(html) {
  clipboardDiv.innerHTML=html;

  var focused=document.activeElement;
  clipboardDiv.focus();

  window.getSelection().removeAllRanges();  
  var range = document.createRange(); 
  range.setStartBefore(clipboardDiv.firstChild);
  range.setEndAfter(clipboardDiv.lastChild);
  window.getSelection().addRange(range);  

  var ok=false;
  try {
     if (document.execCommand('copy')) ok=true; else utils.log('execCommand returned false !');
  } catch (err) {
     utils.log('execCommand failed ! exception '+err);
  }

  focused.focus();
}

请查看jsfiddle,您可以在文本区域中输入HTML代码段,并使用ctrl+c复制到剪贴板。


1
不要将tabindex设置为0,否则它会在tab索引中,也就是说当用户按下tab键时它会被选中。为了使其可聚焦,但不受用户按tab键的影响,请将tabindex设置为-1。我建议这样进行编辑,但被拒绝了。 - Jools

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接