JSFiddle - React, Tailwind, and code Playground
by Imri Paloja
HTML
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/skeleton/2.0.4/skeleton.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/normalize/8.0.1/normalize.min.css">
<div class="container">
<button onclick="undo()">Undo</button>
<button onclick="redo()">Redo</button>
<button onclick="unlinkSelection()">UNLINK</button>
<div id="editor" spellcheck="false" contenteditable="true" onchange="saveState()">
<h1>Editor</h1>
<p>EuroCMS is an early stage CMS like
<a href="https://eurobytes.eu/" download="" hreflang="" ping="https://eurocms.eu/trackpings" referrerpolicy="origin" rel="external" target="_blank" type="">WordPress</a>,
Joomla, Drupal and the likes.</p>
</div>
</div>
CSS
html,
body {
color: #f9f9f9 !important;
background: #202020;
}
.container {
margin-top: 5%;
}
.container #editor h1 {
text-align: center;
}
input,
textarea,
button {
color: inherit;
}
JavaScript
const editor = document.getElementById('editor');
let undoStack = [];
let redoStack = [];
let isRestoring = false;
const MAX_HISTORY = 50;
// Save state (call after every meaningful change)
function saveState() {
if (isRestoring) return;
undoStack.push(editor.innerHTML);
if (undoStack.length > MAX_HISTORY) undoStack.shift();
redoStack.length = 0;
}
// Undo last change
function undo() {
if (undoStack.length === 0) return;
isRestoring = true;
redoStack.push(editor.innerHTML);
const previousState = undoStack.pop();
editor.innerHTML = previousState;
restoreCaretToEnd();
isRestoring = false;
}
// Redo last undone change
function redo() {
if (redoStack.length === 0) return;
isRestoring = true;
undoStack.push(editor.innerHTML);
const nextState = redoStack.pop();
editor.innerHTML = nextState;
restoreCaretToEnd();
isRestoring = false;
}
// Keep cursor usable after restore
function restoreCaretToEnd() {
const range = document.createRange();
const selection = window.getSelection();
range.selectNodeContents(editor);
range.collapse(false);
selection.removeAllRanges();
selection.addRange(range);
}
editor.addEventListener('input', saveState);
function unlinkSelection() {
// ... your existing unlink logic ...
saveState();
}
function unlinkSelection() {
const selection = window.getSelection();
if (!selection || selection.rangeCount === 0) return;
const range = selection.getRangeAt(0);
let node = range.commonAncestorContainer;
// If it's a text node, move up to element node
if (node.nodeType === Node.TEXT_NODE) {
node = node.parentNode;
}
// Find nearest <a> ancestor
const link = node.closest && node.closest('a');
if (!link) {
console.log('No link selected.');
return;
}
// Replace <a> with its text content
const textNode = document.createTextNode(link.textContent);
...