开发者

Auto-expanding textarea

I'm trying to do a simple auto-expanding textarea. This is my code:

开发者_如何学JAVA
textarea.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

  1. Forces resize and all height properties to auto/none/0 etc to prevent interference with the event code.
  2. Resets the rows attribute to 1 to get accurate scrollHeight.
  3. Sets overflow to hidden and hard locks the current computed width (minus left/right border width and left/right padding widths), then forces box-sizing to content-box to get an accurate line-height and scrollHeight reading. border-width and padding-inline is also removed to keep the textarea width consistent when switching box-sizing. This all helps keep the math accurate when dealing with text wrapping.
  4. Grabs the computed line-height and top/bottom-padding pixel values.
  5. Obtains the scrollHeight pixel value (rounded since chrome rounds and we're hoping to handle all browsers consistently).
  6. Removes overflow, box-sizing, width, padding-inline and border-width overrides.
  7. Subtracts block_padding from scroll_height then divides that by the line_height to get the needed rows. The rows value is rounded to the nearest integer since it will always be within ~.1 of the correct whole number.
  8. The calculated rows value is applied as the rows attribute unless the row_limit is smaller, then the row_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.

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜