Skip to content

Instantly share code, notes, and snippets.

@thesephist
Last active March 22, 2023 22:34
Show Gist options
  • Save thesephist/e762e7a38de56c21642dcd9714597f89 to your computer and use it in GitHub Desktop.
Save thesephist/e762e7a38de56c21642dcd9714597f89 to your computer and use it in GitHub Desktop.
<Editable />

Linus's auto-growing textarea trick

The DOM structure:

<div className="textarea-group">
  <textarea value={value}
            onChange={handleChange}
            className="textarea-itself" />´
  <div className="textarea-annotations">
    {valueWithHighlightedSpans}
  </div>
  <div className="textarea-shadow">{value}</div>
</div>
  • .textarea-shadow sizes the outer container, and is the div in which I measure selection rects
  • .textarea-annotations has color: transparent but renders <span> elements used for highlighting/underlining
  • .textarea-itself is, obviously, the textarea.

.textarea-shadow sizes the .textarea-group, and the other elements are position: absolute with top/left/bottom/right: 0.

/* auto-expanding textarea trick */
.textarea-group {
position: relative;
}
body.webkit .textarea-group,
body.webkit .textarea-itself {
/* When path highlight <mark>s are placed into .text-annotations, they
* disrupt kerning between letters and punctuation around words in a way
* that can't be replicated in a <textarea>. To make text layout in both
* environments identical, we turn off kerning. */
font-kerning: none;
}
.textarea-shadow {
visibility: hidden;
}
.textarea-shadow,
.textarea-annotations,
.textarea-itself {
border: 0;
padding: 0;
outline: 0;
box-sizing: border-box;
line-height: 1.5em;
min-height: 1.5em; /* line-height */
color: inherit;
text-decoration: inherit;
width: 100%;
margin: 0;
background: transparent;
word-wrap: break-word;
white-space: pre-wrap;
-webkit-text-size-adjust: none;
}
.textarea-annotations,
.textarea-itself {
resize: none;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
/* Firefox doesn't extend this block to the bottom of .textarea-group
* unless height is set to 100%. */
height: 100%;
}
.textarea-annotations {
pointer-events: none;
color: transparent;
animation: appear .5s;
}
.textarea-annotations mark {
border-radius: 3px;
box-shadow: 0 0 0 2px var(--hover-bg);
}
.textarea-shadow.extra-height {
padding-bottom: 1.5em;
}
// Code to measure selection rects
// NOTE: this isn't actually JS... it's Oak. But I think the code is readable if you know JS :/
// NOTE: I *think* this event listener only works when attached to `document`.
with document.addEventListener('selectionchange') fn handleSelectionChange {
if active := document.activeElement {
? -> ?
_ -> if active.classList.contains('textarea-itself') {
false -> ?
_ -> {
{
value: value
selectionStart: start
selectionEnd: end
} := active
fullSelection? := start = 0 & end = len(value)
selection := value |> slice(start, end)
if selection |> trim() {
'' -> ?
_ -> {
// NOTE: at least in Chrome/Blink,
// HTMLTextAreaElement::getClientRects returns invalid (all-zero)
// coordinates, so instead, we use a trick of duplicating the
// selection in an identically-rendered shadow element
// (.textarea-shadow that also happens to lay out the textarea) to
// measure the text selection bounding rects.
shadowRange := document.createRange()
shadowSpan := document.activeElement.parentNode.querySelector('.textarea-shadow').firstChild
shadowRange.setStart(shadowSpan, start)
shadowRange.setEnd(shadowSpan, end)
rects := shadowRange.getClientRects() |> Array.from()
{
top: top
bottom: bottom
left: left
right: right
} := mergeRects(rects)
// DO STUFF WITH top/bottom/left/right
}
}
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment