I am building a small custom editor using the contenteditable
attribute on a div.
By default, when the user pastes HTML content, the HTML is inserted in to the contenteditable
div, which I do not want.
To solve this issue, I used a custom paste handler like this question suggests:
Stop pasting html style in a contenteditable div only paste the plain text
editor.addEventListener("paste", (e) => {
e.preventDefault();
const text = e.clipboardData.getData('text/plain');
document.execCommand("insertText", false, text.replaceAll("\n", "<div><br></div>"));
})
So far so good. Because the execCommand
is deprecated, I used a replacement, which is to basically use ranges to insert the text (stripped of HTML) by hand.
execCommand() is now obsolete, what's the alternative?
replace selected text in contenteditable div
editor.addEventListener("paste", (e) => {
e.preventDefault();
const text = (e.originalEvent || e).clipboardData.getData("text/plain");
const selection = window.getSelection();
selection.deleteFromDocument();
const range = selection.getRangeAt(0);
range.insertNode(document.createTextNode(text));
range.collapse();
});
This worked well until I noticed, that the undo and redo commands were not working anymore after pasting something. This is because of the DOM modifications done in the custom paste event, which is a deal breaker.
In search of a solution I found the following question:
Allowing contenteditable to undo after dom modification
The first answer there suggests to use the execCommand
, which is deprecated, so it's not a good solution.
The second answer suggests building a custom undo redo stack to handle all these events by hand. So I opted for the second solution.
I build a custom undo redo handler using a simple array to store the versions. For that I used the beforeinput
event to listen for the undo and redo events.
editor.addEventListener("beforeinput", (e) => {
if (e.inputType === "historyUndo") {
e.preventDefault();
console.log("undo");
};
if (e.inputType === "historyRedo") {
e.preventDefault();
console.log("redo");
};
});
This works very well for undos, however, due to me preventing the default, the browsers undo/redo stack is never in a state to redo, so the beforeinput
listener is never fired when pressing cmd + z
or ctrl + z
on windows.
I've also tried this using the input
event with the same result.
I've searched for other solutions to handle the undo/redo events using JavaScript, but building a custom handler using keydown
is no option here, because every OS uses different shortcuts, especially mobile devices!
Question
Now, There's multiple possible solutions to solve my problem, so any of them that works is very welcome.
Is there a way to handle the paste event by hand and insert the pasted plaintext into the document while keeping the undo/redo functionality or implementing a replacement to such functionality by hand?
Example
Try to paste something and then undo, it wont work.
const editor = document.getElementById("editor");
editor.addEventListener("paste", (e) => {
e.preventDefault();
const text = (e.originalEvent || e).clipboardData.getData("text/plain").replaceAll("\n", "<div><br></div>");
const selection = window.getSelection();
selection.deleteFromDocument();
const range = selection.getRangeAt(0);
range.insertNode(document.createTextNode(text));
range.collapse();
});
#editor {
border: 1px solid black;
height: 100px;
}
<div id="editor" contenteditable="true"></div>
Edit
Modifying the ClipboardEvent
does not work because it's read only. Dispatching a new event also does not work because the event would be untrusted, so the browser does not paste the content.
const event = new ClipboardEvent("paste", {
clipboardData: new DataTransfer(),
});
event.clipboardData.setData("text/plain", "blah");
editor.dispatchEvent(event);