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