Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save razodactyl/b0441f7c5bce869f77a7d5d79d035175 to your computer and use it in GitHub Desktop.
Save razodactyl/b0441f7c5bce869f77a7d5d79d035175 to your computer and use it in GitHub Desktop.
HTML Textarea Selection Popup
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Custom Text Selection Toolbar for Textarea (Accurate Positioning)</title>
<script src="https://cdn.tailwindcss.com"></script>
<style>
#mirrorDiv {
position: absolute;
visibility: hidden;
white-space: pre-wrap;
word-wrap: break-word;
overflow-wrap: break-word;
box-sizing: border-box;
}
</style>
</head>
<body class="bg-gray-100 p-4">
<div class="max-w-md mx-auto relative">
<textarea id="myTextarea" class="w-full h-40 p-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" placeholder="Select some text..."></textarea>
<div id="mirrorDiv"></div>
<!-- Custom toolbar -->
<div id="customToolbar" class="hidden fixed bg-white shadow-md rounded-md p-2 flex space-x-2 z-50">
<button class="bg-blue-500 text-white px-3 py-1 rounded-md hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-500">Bold</button>
<button class="bg-green-500 text-white px-3 py-1 rounded-md hover:bg-green-600 focus:outline-none focus:ring-2 focus:ring-green-500">Italic</button>
<button class="bg-red-500 text-white px-3 py-1 rounded-md hover:bg-red-600 focus:outline-none focus:ring-2 focus:ring-red-500">Underline</button>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
const textarea = document.getElementById('myTextarea');
const mirrorDiv = document.getElementById('mirrorDiv');
const toolbar = document.getElementById('customToolbar');
let isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
function syncMirrorDiv() {
const styles = window.getComputedStyle(textarea);
mirrorDiv.style.width = styles.width;
mirrorDiv.style.height = styles.height;
mirrorDiv.style.font = styles.font;
mirrorDiv.style.lineHeight = styles.lineHeight;
mirrorDiv.style.padding = styles.padding;
mirrorDiv.style.border = styles.border;
mirrorDiv.style.boxSizing = styles.boxSizing;
}
function showToolbar() {
const selection = textarea.value.substring(textarea.selectionStart, textarea.selectionEnd);
if (selection.length > 0) {
syncMirrorDiv();
const textareaRect = textarea.getBoundingClientRect();
const selectionCoords = getSelectionCoords(textarea);
toolbar.style.display = 'flex';
const toolbarWidth = toolbar.offsetWidth;
const toolbarHeight = toolbar.offsetHeight;
let top = textareaRect.top + selectionCoords.bottom - textarea.scrollTop + window.scrollY + 5;
let left = textareaRect.left + selectionCoords.left + (selectionCoords.width / 2) - (toolbarWidth / 2);
// Ensure the toolbar doesn't go off-screen
if (left < 0) left = 0;
if (left + toolbarWidth > window.innerWidth) left = window.innerWidth - toolbarWidth;
if (top + toolbarHeight > window.innerHeight) top = textareaRect.top + selectionCoords.top - textarea.scrollTop + window.scrollY - toolbarHeight - 5;
toolbar.style.top = `${top}px`;
toolbar.style.left = `${left}px`;
} else {
toolbar.style.display = 'none';
}
}
function getSelectionCoords(textarea) {
const start = textarea.selectionStart;
const end = textarea.selectionEnd;
const text = textarea.value;
mirrorDiv.textContent = text.substring(0, start);
const startSpan = document.createElement('span');
startSpan.textContent = text.substring(start, end);
mirrorDiv.appendChild(startSpan);
mirrorDiv.append(text.substring(end));
const startRect = startSpan.getBoundingClientRect();
const mirrorRect = mirrorDiv.getBoundingClientRect();
return {
top: startRect.top - mirrorRect.top,
left: startRect.left - mirrorRect.left,
bottom: startRect.bottom - mirrorRect.top,
width: startRect.width
};
}
function handleSelectionChange() {
if (isIOS) {
setTimeout(showToolbar, 0);
} else {
showToolbar();
}
}
textarea.addEventListener('mouseup', handleSelectionChange);
textarea.addEventListener('keyup', handleSelectionChange);
textarea.addEventListener('scroll', handleSelectionChange);
if (isIOS) {
textarea.addEventListener('touchend', handleSelectionChange);
textarea.addEventListener('touchstart', (e) => {
if (e.touches.length === 1) {
e.preventDefault();
}
}, { passive: false });
}
document.addEventListener('mousedown', (e) => {
if (!toolbar.contains(e.target) && e.target !== textarea) {
toolbar.style.display = 'none';
}
});
if (isIOS) {
document.addEventListener('touchstart', (e) => {
if (!toolbar.contains(e.target) && e.target !== textarea) {
toolbar.style.display = 'none';
}
});
}
// Initial sync
syncMirrorDiv();
});
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment