Copy rich text to the clipboard
I’ve been experimenting with a tool for generating the content for a weekly Substack newsletter by querying the Datasette API for my blog and assembling HTML for the last week of content.
I haven’t started sending this out yet, but I figured out how to write rich text to the clipboard as part of my initial prototype.
Substack allows you to paste in rich text (e.g. copied-and-pasted rendered HTML), so it’s useful to be able to programatically add rich text to the user’s clipboard in order to conveniently paste into Substack.
Initially I tried to get this working using the new Clipboard.write(), but I spotted this warning on the Interact with the clipboard page of MDN:
However, while
navigator.clipboard.readText()
andnavigator.clipboard.writeText()
work on all browsers,navigator.clipboard.read()
andnavigator.clipboard.write()
do not. For example, on Firefox at the time of writing,navigator.clipboard.read()
andnavigator.clipboard.write()
are not fully implemented, such that to:
- work with images use
browser.clipboard.setImageData()
to write images to the clipboard anddocument.execCommand("paste")
to paste images to a webpage.- write rich content (such as, HTML, rich text including images, etc.) to the clipboard, use
document.execCommand("copy")
ordocument.execCommand("cut")
. Then, eithernavigator.clipboard.read()
(recommended) ordocument.execCommand("paste")
to read the content from the clipboard.
This is a bit tough to read, but the TLDR version is that for rich text copying in Firefox the .write()
method doesn’t work properly yet.
I actually pasted the above code into ChatGPT as a clue and got it to write me the following code, which I then tidied up and added the document.body.appendChild()
and document.body.removeChild()
lines (it failed without them):
function copyRichText(html) { const htmlContent = html; // Create a temporary element to hold the HTML content const tempElement = document.createElement("div"); tempElement.innerHTML = htmlContent; document.body.appendChild(tempElement); // Select the HTML content const range = document.createRange(); range.selectNode(tempElement); // Copy the selected HTML content to the clipboard const selection = window.getSelection(); selection.removeAllRanges(); selection.addRange(range); document.execCommand("copy"); selection.removeAllRanges(); document.body.removeChild(tempElement);}
In an Observable notebook
I used this to add a “copy” button to my Observable notebook like this:
Object.assign(html`<button>Copy rich text newsletter to clipboard`, { onclick: () => { const htmlContent = newsletterHTML; // Create a temporary element to hold the HTML content const tempElement = document.createElement("div"); tempElement.innerHTML = htmlContent; document.body.appendChild(tempElement); // Select the HTML content const range = document.createRange(); range.selectNode(tempElement); // Copy the selected HTML content to the clipboard const selection = window.getSelection(); selection.removeAllRanges(); selection.addRange(range); document.execCommand("copy"); selection.removeAllRanges(); document.body.removeChild(tempElement); }})
This depends on some other cell defining newsletterHTML
as a string of HTML.
Here’s the notebook that uses that.