JSFiddle - React, Tailwind, and code Playground

by Imri Paloja

HTML

<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="main container">

  <button onclick="smartToggleBold()">B</button>
  <button onclick="smartToggleBold">I</button>
  <button onclick="smartToggleBold">U</button>

<div id="editor" contenteditable="true">
      Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque vel
    erat in elit feugiat varius nec eu dui. Sed risus risus, hendrerit nec risus
    non, ultrices convallis elit. Pellentesque risus purus, ullamcorper ac erat
    at, tempor pretium ipsum. Morbi accumsan libero et nisl facilisis, et
    efficitur ipsum fringilla. Quisque a mollis mauris. In id risus malesuada,
    consectetur risus non, vulputate purus. Maecenas tristique sem risus, quis
    tincidunt odio feugiat nec. Vestibulum condimentum nisi sed ultricies
    tristique. Aliquam id lacinia elit. Donec rutrum metus libero, sit amet
    dictum mi maximus nec. Morbi luctus, turpis vitae varius facilisis, risus
    quam faucibus erat, sodales facilisis metus ipsum non lacus. Maecenas elit
    velit, luctus sit amet dui id, auctor sagittis libero. Donec posuere eu
    ipsum quis viverra.
  </div>
</div>

CSS

html,
body {
  color: #454545;
}

.main.container {
  margin-top: 1%;
}

#editor {
  background: #eeeeee;
  border: 1px solid #cccccc;
  border-radius: 5px;
  padding: 20px 30px;
  margin: 20px 3px;
}

JavaScript

/**
 * Smart toggle formatting command
 * - If selection is already fully wrapped in the tag → unwraps it
 * - If partially wrapped → usually applies to whole selection (common UX)
 * - If not wrapped → wraps selection
 * 
 * @param {string} tagName - 'b', 'strong', 'i', 'em', 'u', 's', 'sup', 'sub' etc.
 * @returns {boolean} success
 */
function toggleFormat(tagName) {
    const selection = document.getSelection();
    if (!selection.rangeCount || selection.isCollapsed) return false;

    tagName = tagName.toLowerCase();

    // 1. Check if selection is completely inside the desired tag
    if (isSelectionFullyWrappedIn(tagName)) {
        return unwrapSelection(tagName);
    }

    // 2. Otherwise → apply (wrap) the formatting
    return wrapSelection(tagName);
}

/**
 * Checks if ALL of the current selection is inside the given tag
 */
function isSelectionFullyWrappedIn(tagName) {
    const selection = document.getSelection();
    if (!selection.rangeCount) return false;

    const range = selection.getRangeAt(0);
    
    // Quick path: common ancestor is exactly the tag we want
    let common = range.commonAncestorContainer;
    if (common.nodeType === 1 && common.tagName.toLowerCase() === tagName) {
        return true;
    }

    // More accurate check: walk through all leaf nodes in selection
    const iterator = document.createNodeIterator(
        range.commonAncestorContainer,
        NodeFilter.SHOW_ELEMENT,
        null
    );

    let node;
    let allWrapped = true;
    let foundAnyText = false;

    while ((node = iterator.nextNode())) {
        if (range.intersectsNode(node)) {
            // Find nearest formatting ancestor of this node
            let current = node;
            let isWrapped = false;
            
            while (current && current !== document.body) {
                if (current.tagName?.toLowerCase() === tagName) {
                    isWrapped = true;
            ...