Auto-expanding textarea
I'm trying to do a simple auto-expanding textarea. This is my code:
开发者_如何学JAVAtextarea.onkeyup = function () {
textarea.style.height = textarea.clientHeight + 'px';
}
But the textarea just keeps growing indefinitely as you type...
I know there is Dojo and a jQuery plugin for this, but would rather not have to use them. I looked at their implementation, and was initially using scrollHeight
but that did the same thing.
You can start answering and play with the textarea for your answer to play with.
Reset the height before Using scrollHeight
to expand/shrink the textarea correctly. Math.min()
can be used to set a limit on the textarea's height.
Code:
var textarea = document.getElementById("textarea");
var heightLimit = 200; /* Maximum height: 200px */
textarea.oninput = function() {
textarea.style.height = ""; /* Reset the height*/
textarea.style.height = Math.min(textarea.scrollHeight, heightLimit) + "px";
};
Fiddle: http://jsfiddle.net/gjqWy/155
Note: The input
event is not supported by IE8 and earlier. Use keydown
or keyup
with onpaste
and/or oncut
if you want to support this ancient browser as well.
I've wanted to have the auto-expanding area to be limited by rows number (e.g 5 rows). I've considered using "em" units, for Rob's solution however, this is error-prone and wouldn't take account stuff like padding, etc.
So this is what I came up with:
var textarea = document.getElementById("textarea");
var limitRows = 5;
var messageLastScrollHeight = textarea.scrollHeight;
textarea.oninput = function() {
var rows = parseInt(textarea.getAttribute("rows"));
// If we don't decrease the amount of rows, the scrollHeight would show the scrollHeight for all the rows
// even if there is no text.
textarea.setAttribute("rows", "1");
if (rows < limitRows && textarea.scrollHeight > messageLastScrollHeight) {
rows++;
} else if (rows > 1 && textarea.scrollHeight < messageLastScrollHeight) {
rows--;
}
messageLastScrollHeight = textarea.scrollHeight;
textarea.setAttribute("rows", rows);
};
Fiddle: http://jsfiddle.net/cgSj3/
For those interested in a jQuery version of Rob W's solution:
var textarea = jQuery('.textarea');
textarea.on("input", function () {
jQuery(this).css("height", ""); //reset the height
jQuery(this).css("height", Math.min(jQuery(this).prop('scrollHeight'), 200) + "px");
});
2022 Vanilla JS Solution
Note: I have only tested this in Chrome but it should work everywhere.
This will handle pasting, deleting, text-wrapping, manual returns, etc. & accounts for padding and box-sizing issues.
How it Works
- Forces resize and all height properties to auto/none/0 etc to prevent interference with the event code.
- Resets the rows attribute
to 1 to get accurate
scrollHeight
. - Sets
overflow
tohidden
and hard locks the current computed width (minus left/right border width and left/right padding widths), then forcesbox-sizing
tocontent-box
to get an accurateline-height
andscrollHeight
reading.border-width
andpadding-inline
is also removed to keep the textarea width consistent when switchingbox-sizing
. This all helps keep the math accurate when dealing with text wrapping. - Grabs the computed
line-height
andtop/bottom-padding
pixel values. - Obtains the
scrollHeight
pixel value (rounded since chrome rounds and we're hoping to handle all browsers consistently). - Removes
overflow
,box-sizing
,width
,padding-inline
andborder-width
overrides. - Subtracts
block_padding
fromscroll_height
then divides that by theline_height
to get the neededrows
. Therows
value is rounded to the nearest integer since it will always be within ~.1 of the correct whole number. - The calculated
rows
value is applied as therows
attribute unless therow_limit
is smaller, then therow_limit
is used instead.
Edit / Update Details
I removed the loop code that was used figure out the row count because I was able to verify the math on the division formula works out within about .1 of the row count needed. Therefore a simple Math.round()
ensures the row count is accurate. I was not able to break this in testing so if it turns out to be wrong please feel free to suggest a tweak.
I also ran into issues when line-height
is not explicitly set on the text area as in that case the computed value for line-height
comes back as "normal"
and not the actual computed value. This new version accounts for this eventuality and handles it properly as well.
Layout Shift Possibility
I did not set the textarea
to position: absolute;
while swapping it's box-sizing
out as I did not notice a need for it in my testing. It's worth mentioning because I suppose there could be a scenario where these minor changes might cause a layout shift depending on how the page is styled and if that happens you could add add that and then remove it along with the box-sizing override and removal.
Sample Code
(you only need the one JS function, everything else is just for the demo)
function autosize(textarea_id, row_limit) {
// Set default for row_limit parameter
row_limit = parseInt(row_limit ?? '5');
if (!row_limit) {
row_limit = 5;
}
// Get the element
const textarea = document.getElementById(textarea_id);
// Set required styles for this to function properly.
textarea.style.setProperty('resize', 'none');
textarea.style.setProperty('min-height', '0');
textarea.style.setProperty('max-height', 'none');
textarea.style.setProperty('height', 'auto');
// Set rows attribute to number of lines in content
textarea.oninput = function() {
// Reset rows attribute to get accurate scrollHeight
textarea.setAttribute('rows', '1');
// Get the computed values object reference
const cs = getComputedStyle(textarea);
// Force content-box for size accurate line-height calculation
// Remove scrollbars, lock width (subtract inline padding and inline border widths)
// and remove inline padding and borders to keep width consistent (for text wrapping accuracy)
const inline_padding = parseFloat(cs['padding-left']) + parseFloat(cs['padding-right']);
const inline_border_width = parseFloat(cs['border-left-width']) + parseFloat(cs['border-right-width']);
textarea.style.setProperty('overflow', 'hidden', 'important');
textarea.style.setProperty('width', (parseFloat(cs['width']) - inline_padding - inline_border_width) + 'px');
textarea.style.setProperty('box-sizing', 'content-box');
textarea.style.setProperty('padding-inline', '0');
textarea.style.setProperty('border-width', '0');
// Get the base line height, and top / bottom padding.
const block_padding = parseFloat(cs['padding-top']) + parseFloat(cs['padding-bottom']);
const line_height =
// If line-height is not explicitly set, use the computed height value (ignore padding due to content-box)
cs['line-height'] === 'normal' ? parseFloat(cs['height'])
// Otherwise (line-height is explicitly set), use the computed line-height value.
: parseFloat(cs['line-height']);
// Get the scroll height (rounding to be safe to ensure cross browser consistency)
const scroll_height = Math.round(textarea.scrollHeight);
// Undo overflow, width, border-width, box-sizing & inline padding overrides
textarea.style.removeProperty('width');
textarea.style.removeProperty('box-sizing');
textarea.style.removeProperty('padding-inline');
textarea.style.removeProperty('border-width');
textarea.style.removeProperty('overflow');
// Subtract block_padding from scroll_height and divide that by our line_height to get the row count.
// Round to nearest integer as it will always be within ~.1 of the correct whole number.
const rows = Math.round((scroll_height - block_padding) / line_height);
// Set the calculated rows attribute (limited by row_limit)
textarea.setAttribute("rows", "" + Math.min(rows, row_limit));
};
// Trigger the event to set the initial rows value
textarea.dispatchEvent(new Event('input', {
bubbles: true
}));
}
autosize('textarea');
* {
box-sizing: border-box;
}
textarea {
width: 100%;
max-width: 30rem;
font-family: sans-serif;
font-size: 1rem;
line-height: 1.5rem;
padding: .375rem;
}
<body>
<textarea id="textarea" placeholder="enter some text here :)"></textarea>
</body>
...and if you need an infinitely expanding textarea (as I did), just do this:
var textarea = document.getElementById("textarea");
textarea.oninput = function() {
textarea.style.height = ""; /* Reset the height*/
textarea.style.height = textarea.scrollHeight + "px";
};
using
<div contentEditable></div>
may also do the same work, expanding it self, and requires no js
Unlike the accepted answer, my function cares about padding-{top,bottom}
and border-{top,bottom}-width
. And it has many parameters. Note that it doesn't set window.addEventListener('resize')
Function:
// @author Arzet Ro, 2021 <arzeth0@gmail.com>
// @license CC0 (Creative Commons Zero v1.0 Universal) (i.e. Public Domain)
// @source https://stackoverflow.com/a/70341077/332012
// Useful for elements with overflow-y: scroll and <textarea>
// Tested only on <textarea> in desktop Firefox 95 and desktop Chromium 96.
export function autoResizeScrollableElement (
el: HTMLElement,
{
canShrink = true,
minHeightPx = 0,
maxHeightPx,
minLines,
maxLines,
}: {
canShrink?: boolean,
minHeightPx?: number,
maxHeightPx?: number,
minLines?: number,
maxLines?: number,
} = {}
): void
{
const FN_NAME = 'autoResizeScrollableElement'
if (
typeof minLines !== 'undefined'
&& minLines !== null
&& Number.isNaN(+minLines)
)
{
console.warn(
'%O(el=%O):: minLines (%O) as a number is NaN',
FN_NAME, el, minLines
)
}
if (
typeof maxLines !== 'undefined'
&& maxLines !== null
&& Number.isNaN(+maxLines)
)
{
console.warn(
'%O(el=%O):: maxLines (%O) as a number is NaN',
FN_NAME, el, maxLines
)
}
canShrink = (
canShrink === true
||
// @ts-ignore
canShrink === 1 || canShrink === void 0 || canShrink === null
)
const style = window.getComputedStyle(el)
const unpreparedLineHeight = style.getPropertyValue('line-height')
if (unpreparedLineHeight === 'normal')
{
console.error('%O(el=%O):: line-height is unset', FN_NAME, el)
}
const lineHeightPx: number = (
unpreparedLineHeight === 'normal'
? 1.15 * parseFloat(style.getPropertyValue('font-size')) // 1.15 is a wrong number
: parseFloat(unpreparedLineHeight)
)
// @ts-ignore
minHeightPx = parseFloat(minHeightPx || 0) || 0
//minHeight = Math.max(lineHeightPx, parseFloat(style.getPropertyValue('min-height')))
// @ts-ignore
maxHeightPx = parseFloat(maxHeightPx || 0) || Infinity
minLines = (
minLines
? (
Math.round(+minLines || 0) > 1
? Math.round(+minLines || 0)
: 1
)
: 1
)
maxLines = (
maxLines
? (Math.round(+maxLines || 0) || Infinity)
: Infinity
)
//console.log('%O:: old ov.x=%O ov.y=%O, ov=%O', FN_NAME, style.getPropertyValue('overflow-x'), style.getPropertyValue('overflow-y'), style.getPropertyValue('overflow'))
/*if (overflowY !== 'scroll' && overflowY === 'hidden')
{
console.warn('%O:: setting overflow-y to scroll', FN_NAME)
}*/
if (minLines > maxLines)
{
console.warn(
'%O(el=%O):: minLines (%O) > maxLines (%O), '
+ 'therefore both parameters are ignored',
FN_NAME, el, minLines, maxLines
)
minLines = 1
maxLines = Infinity
}
if (minHeightPx > maxHeightPx)
{
console.warn(
'%O(el=%O):: minHeightPx (%O) > maxHeightPx (%O), '
+ 'therefore both parameters are ignored',
FN_NAME, el, minHeightPx, maxHeightPx
)
minHeightPx = 0
maxHeightPx = Infinity
}
const topBottomBorderWidths: number = (
parseFloat(style.getPropertyValue('border-top-width'))
+ parseFloat(style.getPropertyValue('border-bottom-width'))
)
let verticalPaddings: number = 0
if (style.getPropertyValue('box-sizing') === 'border-box')
{
verticalPaddings += (
parseFloat(style.getPropertyValue('padding-top'))
+ parseFloat(style.getPropertyValue('padding-bottom'))
+ topBottomBorderWidths
)
}
else
{
console.warn(
'%O(el=%O):: has `box-sizing: content-box`'
+ ' which is untested; you should set it to border-box. Continuing anyway.',
FN_NAME, el
)
}
const oldHeightPx = parseFloat(style.height)
if (el.tagName === 'TEXTAREA')
{
el.setAttribute('rows', '1')
//el.style.overflowY = 'hidden'
}
// @ts-ignore
const oldScrollbarWidth: string|void = el.style.scrollbarWidth
el.style.height = ''
// Even when there is nothing to scroll,
// it causes an extra height at the bottom in the content area (tried Firefox 95).
// scrollbar-width is present only on Firefox 64+,
// other browsers use ::-webkit-scrollbar
// @ts-ignore
el.style.scrollbarWidth = 'none'
const maxHeightForMinLines = lineHeightPx * minLines + verticalPaddings // can be float
// .scrollHeight is always an integer unfortunately
const scrollHeight = el.scrollHeight + topBottomBorderWidths
/*console.log(
'%O:: lineHeightPx=%O * minLines=%O + verticalPaddings=%O, el.scrollHeight=%O, scrollHeight=%O',
FN_NAME, lineHeightPx, minLines, verticalPaddings,
el.scrollHeight, scrollHeight
)*/
const newHeightPx = Math.max(
canShrink === true ? minHeightPx : oldHeightPx,
Math.min(
maxHeightPx,
Math.max(
maxHeightForMinLines,
Math.min(
Math.max(scrollHeight, maxHeightForMinLines)
- Math.min(scrollHeight, maxHeightForMinLines) < 1
? maxHeightForMinLines
: scrollHeight,
(
maxLines > 0 && maxLines !== Infinity
? lineHeightPx * maxLines + verticalPaddings
: Infinity
)
)
)
)
)
// @ts-ignore
el.style.scrollbarWidth = oldScrollbarWidth
if (!Number.isFinite(newHeightPx) || newHeightPx < 0)
{
console.error(
'%O(el=%O):: BUG:: Invalid return value: `%O`',
FN_NAME, el, newHeightPx
)
return
}
el.style.height = newHeightPx + 'px'
//console.log('%O:: height: %O → %O', FN_NAME, oldHeightPx, newHeightPx)
/*if (el.tagName === 'TEXTAREA' && el.scrollHeight > newHeightPx)
{
el.style.overflowY = 'scroll'
}*/
}
Usage with React (TypeScript):
<textarea
onKeyDown={(e) => {
if (!(e.key === 'Enter' && !e.shiftKey)) return true
e.preventDefault()
// send the message, then this.scrollToTheBottom()
return false
}}
onChange={(e) => {
if (this.state.isSending)
{
e.preventDefault()
return false
}
this.setState({
pendingMessage: e.currentTarget.value
}, () => {
const el = this.chatSendMsgRef.current!
engine.autoResizeScrollableElement(el, {maxLines: 5})
})
return true
}}
/>
For React onChange
is like oninput
in HTML5, so if you don't use React, then use the input
event.
One of the answers uses rows
attribute (instead of CSS's height
as my code above does), here's an alternative implementation that doesn't use outside variables (BUT just like that answer there is a bug: because rows
is temporaily set to 1, something bad happens with <html>
's scrollTop when you input AND <html>
can be scrolled):
// @author Arzet Ro, 2021 <arzeth0@gmail.com>
// @license CC0 (Creative Commons Zero v1.0 Universal) (i.e. Public Domain)
// @source https://stackoverflow.com/a/70341077/332012
function autoResizeTextareaByChangingRows (
el,
{minLines, maxLines}
)
{
const FN_NAME = 'autoResizeTextareaByChangingRows'
if (
typeof minLines !== 'undefined'
&& minLines !== null
&& Number.isNaN(+minLines)
)
{
console.warn('%O:: minLines (%O) as a number is NaN', FN_NAME, minLines)
}
if (
typeof maxLines !== 'undefined'
&& maxLines !== null
&& Number.isNaN(+maxLines)
)
{
console.warn('%O:: maxLines (%O) as a number is NaN', FN_NAME, maxLines)
}
minLines = (
minLines
? (
Math.round(+minLines || 0) > 1
? Math.round(+minLines || 0)
: 1
)
: 1
)
maxLines = (
maxLines
? (Math.round(+maxLines || 0) || Infinity)
: Infinity
)
el.setAttribute(
'rows',
'1',
)
const style = window.getComputedStyle(el)
const unpreparedLineHeight = style.getPropertyValue('line-height')
if (unpreparedLineHeight === 'normal')
{
console.error('%O:: line-height is unset for %O', FN_NAME, el)
}
const rows = Math.max(minLines, Math.min(maxLines,
Math.round(
(
el.scrollHeight
- parseFloat(style.getPropertyValue('padding-top'))
- parseFloat(style.getPropertyValue('padding-bottom'))
) / (
unpreparedLineHeight === 'normal'
? 1.15 * parseFloat(style.getPropertyValue('font-size')) // 1.15 is a wrong number
: parseFloat(unpreparedLineHeight)
)
)
))
el.setAttribute(
'rows',
rows.toString()
)
}
const textarea = document.querySelector('textarea')
textarea.oninput = function ()
{
autoResizeTextareaByChangingRows(textarea, {maxLines: 5})
}
For those using Angular and having the same issue, use
<textarea cdkTextareaAutosize formControlName="description" name="description" matInput placeholder="Description"></textarea>
The key here is cdkTextareaAutosize
which will automatically resize the textarea to fit its content. Read more here.
I hope this helps someone.
精彩评论