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