Created
July 17, 2024 20:25
-
-
Save razodactyl/b0441f7c5bce869f77a7d5d79d035175 to your computer and use it in GitHub Desktop.
HTML Textarea Selection Popup
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!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