selection-popover
selection-popover copied to clipboard
Textarea and Input support
Textarea and Input support
Overview
<textarea>
and <input>
text is not supported at the moment. This would be good for rich text editor support. I'm currently using this code locally to make it work but there could be another solution that doesn't involve adding and removing a div to get the coordinates.
const getDummyRect = (el: HTMLTextAreaElement | HTMLInputElement): DOMRect => {
const dummyDiv = document.createElement("div");
dummyDiv.textContent = el.value;
var computedStyle = window.getComputedStyle(el);
Array.from(computedStyle).forEach(function (key) {
return dummyDiv.style.setProperty(key, computedStyle.getPropertyValue(key), computedStyle.getPropertyPriority(key));
});
dummyDiv.style.position = "absolute";
dummyDiv.style.visibility = "hidden";
const textareaRect = el.getBoundingClientRect();
const scrolledTop = textareaRect.top + window.scrollY;
const scrolledLeft = textareaRect.left + window.scrollX;
dummyDiv.style.top = `${scrolledTop}px`;
dummyDiv.style.left = `${scrolledLeft}px`;
dummyDiv.style.width = `${textareaRect.width}px`;
dummyDiv.style.height = `${textareaRect.height}px`;
document.body.appendChild(dummyDiv);
const range = document.createRange();
range.setStart(dummyDiv.firstChild!, el.selectionStart || 0);
range.setEnd(dummyDiv.firstChild!, el.selectionEnd || 0);
const rect = range.getBoundingClientRect();
document.body.removeChild(dummyDiv);
return rect;
}
const handleSelection = () => {
if (pointerTypeRef.current !== 'mouse') return
const selection = document.getSelection()
if (!selection) return
const node = ref.current
const wasSelectionInsideTrigger = node?.contains(selection.anchorNode)
if (!wasSelectionInsideTrigger) {
hasOpenedRef.current = false
return
}
const activeEl = document.activeElement as HTMLInputElement | HTMLTextAreaElement;
let isCollapsed = true;
let selectedText = "";
if (activeEl && (activeEl.tagName === 'TEXTAREA' || activeEl.tagName === 'INPUT')) {
const start = activeEl.selectionStart ?? 0;
const end = activeEl.selectionEnd ?? 0;
isCollapsed = start === end;
selectedText = activeEl.value.substring(start, end);
} else {
isCollapsed = selection.isCollapsed;
selectedText = selection.toString();
}
if (isCollapsed) {
hasOpenedRef.current = false
return
}
const hasTextSelected = selectedText.trim() !== ''
if (hasTextSelected) {
if (!hasOpenedRef.current) onOpen(() => onOpenChange(true))
hasOpenedRef.current = true
if (activeEl && (activeEl.tagName === 'TEXTAREA' || activeEl.tagName === 'INPUT')) {
const rect = getDummyRect(activeEl)
onVirtualRefChange({
getBoundingClientRect: () => rect,
getClientRects: () => ({
0: rect,
length: 1,
item: (index: number) => index === 0 ? rect : null,
[Symbol.iterator]: function* () {
yield rect;
}
}),
});
} else {
const range = selection?.getRangeAt(0)
onVirtualRefChange({
getBoundingClientRect: () => range.getBoundingClientRect(),
getClientRects: () => range.getClientRects(),
})
}
}
}
The above code is for whileSelected. Here would be the updates for the default functionality.
context.onOpen(() => {
const selection = document.getSelection()
if (!selection) return
const trigger = ref.current
const wasSelectionInsideTrigger = trigger?.contains(selection.anchorNode)
if (!wasSelectionInsideTrigger) return
const activeEl = document.activeElement as HTMLInputElement | HTMLTextAreaElement
let isCollapsed = true
let selectedText = ''
if (activeEl && (activeEl.tagName === 'TEXTAREA' || activeEl.tagName === 'INPUT')) {
const start = activeEl.selectionStart ?? 0
const end = activeEl.selectionEnd ?? 0
isCollapsed = start === end
selectedText = activeEl.value.substring(start, end)
} else {
isCollapsed = selection.isCollapsed
selectedText = selection.toString()
}
if (isCollapsed) return
if (selectedText.trim() === '') return
context.onOpenChange(true)
if (activeEl && (activeEl.tagName === 'TEXTAREA' || activeEl.tagName === 'INPUT')) {
const rect = getDummyRect(activeEl)
context.onVirtualRefChange({
getBoundingClientRect: () => rect,
getClientRects: () => ({
0: rect,
length: 1,
item: (index: number) => (index === 0 ? rect : null),
[Symbol.iterator]: function* () {
yield rect
},
}),
})
} else {
const range = selection.getRangeAt(0)
context.onVirtualRefChange({
getBoundingClientRect: () => range.getBoundingClientRect(),
getClientRects: () => range.getClientRects(),
})
}
})
It would be a good addition to the library!
but there could be another solution that doesn't involve adding and removing a div to get the coordinates.
I don't know if it's possible to get the selection rect in the input without using this approach. Do you have anything else in mind?
No, I don't I am just doing the solution above to work with textarea and text inputs.
@slowshi can you please share a codesandbox of your solution in action?