I want to be able to highlight matches in user editable text, so I decided to do the standard exercise of creating a <div>
that floats above the textarea and highlight the text in that div.
I got it almost perfect in about 30 minutes, so I am almost happy. The problem is, when you scroll all the way to right/bottom, the alignment breaks.
Try it, it’s quite neat except for that little bug. Once you scroll the textarea all the way down, the alignment between the text in the textarea and my overlay div breaks.
class TextareaOverlay {
/**
*
* @param {HTMLDivElement} container
*/
constructor(container) {
/** @type {HTMLTextAreaElement} */
this.textarea = container.querySelector("textarea");
/** @type {HTMLDivElement} **/
this.overlay = document.createElement("div");
this.overlay.classList.add("overlay");
container.appendChild(this.overlay);
this.textarea.addEventListener("input", this.inputHandler.bind(this));
this.textarea.addEventListener("scroll", this.scrollHandler.bind(this));
this.inputHandler();
this.sizeHandler();
this.scrollHandler();
this.resizeObserver = new ResizeObserver(this.sizeHandler.bind(this));
this.resizeObserver.observe(this.textarea);
}
inputHandler() {
const text = this.textarea.value;
this.overlay.textContent = text;
}
scrollHandler() {
this.overlay.scrollTop = this.textarea.scrollTop;
this.overlay.scrollLeft = this.textarea.scrollLeft;
}
sizeHandler() {
const rect = this.textarea.getBoundingClientRect();
this.overlay.style.width = rect.width+"px";
this.overlay.style.height = rect.height+"px";
}
};
const textareaHelper = new TextareaOverlay(document.querySelector(".textarea-overlay"));
div.textarea-overlay {
position: relative;
}
div.textarea-overlay textarea {
width: 100%;
height: 100%;
padding: 10px;
font-family: 'Courier New', Courier, monospace;
background-color: #f8f9fa;
margin: 0;
box-sizing: border-box;
overflow: scroll;
white-space: pre;
font-size: 1em;
}
div.textarea-overlay div.overlay {
margin: 0;
position: absolute;
font-size: 1em;
top: 0;
left: 0;
width: 100%;
height: 100%;
padding: 10px;
white-space: pre;
font-family: 'Courier New', Courier, monospace;
pointer-events: none;
overflow: hidden;
box-sizing: border-box;
border: 1px solid transparent;
}
<div class="textarea-overlay" style="width: 400px; height: 500px">
<textarea>
div.textarea-overlay {
position: relative;
}
div.textarea-overlay textarea {
width: 100%;
height: 100%;
padding: 10px;
font-family: 'Courier New', Courier, monospace;
background-color: #f8f9fa;
margin: 0;
box-sizing: border-box;
overflow: scroll;
white-space: pre;
font-size: 1em;
}
div.textarea-overlay div.overlay {
margin: 0;
position: absolute;
font-size: 1em;
top: 0;
left: 0;
width: 100%;
height: 100%;
padding: 10px;
white-space: pre;
font-family: 'Courier New', Courier, monospace;
pointer-events: none;
overflow: hidden;
box-sizing: border-box;
border: 1px solid transparent;
}
</textarea>
</div>
So the question is how to deal with the scrolling issue. Bonus points if you also know how would I go about displaying text cursor in the overlay in case I wanted to hide the original text completely.
2
Answers
There are 2 minor problems.
(1) The code sets the dimensions of the overlay using getBoundingClientRect. However, this makes the overlay about 18px larger than the textarea. You need to subtract the size of the scrollbar from the rect or else use the textarea’s clientHeight and clientWidth properties.
(2) The html adds an extra line to the textarea that is not mirrored in the overlay. This adds an additional 1rem to the height. To fix this problem, remove the carriage return in the html:
change this:
to this:
In the snippet, the textarea text is black and the overlay is in red to make any position mismatch more obvious.
Fixing Alignment
The
textarea
may have a different scroll area compared to adiv
, even when their contents are identical. This discrepancy varies across browsers and operating systems. Matching that would require dynamically adjusting the overlay’s dimensions in some way.Furthermore, when
div.overlay
hasoverflow: hidden
, it appears to prevent the scrolling (even programmatic) if there is at least one child HTML element. Only plain text is allowed, and as a result, it is not possible to highlight the words.Introducing an extra
div.content
within thediv.overlay
and moving it withabsolute
positioning instead of scrolling would provide more flexibility and allow for any inner tags.Highlighting Matches
In its most basic form, it wraps the matching text in tags (e.g.,
<em>
) by usingString.replaceAll()
.The value of a
textarea
must be HTML-escaped prior to highlighting, avoiding unintended code and maintaining the original alignment.Hiding Textarea
Setting the
color
,background
, andborder
totransparent
will hide the originaltextarea
. Optionally,outline: none;
andappearance: none;
conceal a few more features.Showing Input Cursor
To ensure the insertion cursor remains visible even when the text is transparent, the
caret-color
CSS property can be applied.The highlights may cover the caret if they have a solid background. To prevent this,
div.content
should have a lowerz-index
thantextarea
.Result