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