如何创建一个高度自适应的textarea

我注意到现在SF的评论框已经是高度自适应了,也就是说无论你输入多少个字也不会有那个讨厌的滚动条了,这点很好。我大概看了一下它的技术实现,好像是把textarea替换成一个div,然后把当前的div的contentEditable设置为true。这个实现跟quora的实现比较像。

但是这种实现有个问题,就是对浏览器的支持不是太好,当然SF是明确不支持IE6的,其它版本的IE我手头没有,不知道支持情况如何。就通常情况来说,ie对contentEditable需要做特别多hack才能保持兼容。另一个浏览器就是opera,opera对div的contentEditable支持是最近才开始的。

另一种实现就是,你在网上搜索就会得到的答案,用代码来说话就是

$('textarea').keyup(function () {
    $(this).height(this.scrollHeight);
});

基本上所有的jquery插件核心都是这段代码,但是实际上它的效果非常坑爹

  1. 它响应的是keyup事件,因此也就是说肯定会有延迟。其视觉表现就是,先出现一个滚动条,然后这个文本框再被拉长。这样的体验非常别扭。
  2. 它也有兼容性问题,再某些浏览器上(比如safari),它的scrollHeight会莫名奇妙地多出一些,看起来非常奇怪。

以上就是我对这个功能的分析,我想要的就是一个普通的文本框,在大部分浏览器(可以忽略ie6)下都能敏捷地响应拉伸。如果你是用contentEditable实现,你需要支持以下功能

  1. 拷贝文字时只拷贝纯文本,html会被过滤掉
  2. 换行支持良好
  3. 支持undo和redo

这种文本框体验上其实非常好,如果能在这里讨论出一种比较好地解决方案,也可以造福很多前端开发者。

阅读 73.6k
13 个回答

终极答案

前些日子有过textarea高度自适应的需求,找到一个插件flexText,
虽然没有用上去,但是的精简的代码很吸引我。

它是原理是这样的,HTML结构如下:

<div class="expandingArea">
    <pre><span></span><br></pre>
    <textarea placeholder="输入文字"></textarea>
</div>

其中的expandingArea的样式仅有

.expandingArea{
    position:relative;
}

目的是用于textarea相对于expandingArea绝对定位:

textarea{
    position:absolute;
    top:0;
    left:0;
    height:100%;
}

通过这样的样式设置,textArea的高度会始终等于expandingArea的高度,要让textarea的高度变化也只需要调整
expadingArea的高度即可。那么怎么样让expandingArea的高度变化随内容高度变化而变化呢?pre是比较重要的
东西。

pre{
    display:block;
    visibility:hidden;
}

pre以块形式存在,并且不可见,但是是占用空间的,不像display:none;什么空间也不占。这时需要把textarea中的内容实时同步到pre里的span标签中,因为pre没有postion:absolute所以它的高度会一直影响expandingArea的高度。总结原理就是:pre会随内容的高度变化而变化,expandingArea的高度又随pre变化,因为textarea的高度100% textarea的高度会随expandingArea变化,只要同步textarea的内容到pre中,就达到一个textarea随内容高度变化的目的了。

关于这个方法的兼容性问题 在这个方法的创始人博客有提到NEIL JENKINS。个人觉得这个方法是牛逼的,没有通过计算,逻辑上它像上思维推导,代码实现不复杂,轻松愉快。在这个例子中又看到了一次合理的结构可以简化代码的案例:)。

确实我在考虑这个功能的时候,已经测试了很多方案,除了你说的两个方案我还使用了一个方案,这个方案我正在测试,但是如果能改进一些小毛病的话,体验肯定比当前的要好。

我是这样考虑的,既然scrollHeight不可信,那么我们就要寻找一个可信的height标准。我们另外创建一个div,让它的css完全继承自这个textarea,因为div的height是可以自由浮动伸缩的,所以我们截获textarea的keyup事件,然后把它的内容发送到div里,然后我们通过获取div的高度来定义textarea的高度。

在实现这个功能之前,还有个需要实现的功能,那就是拷贝css到另一个元素,索性网上已经有现成的jQuery解决方案

// 获取一个元素的所有css属性的patch, $(el).css()
jQuery.fn.css2 = jQuery.fn.css;
jQuery.fn.css = function() {
    if (arguments.length) return jQuery.fn.css2.apply(this, arguments);
    var attr = ['font-family','font-size','font-weight','font-style','color',
        'text-transform','text-decoration','letter-spacing', 'box-shadow',
        'line-height','text-align','vertical-align','direction','background-color',
        'background-image','background-repeat','background-position',
        'background-attachment','opacity','width','height','top','right','bottom',
        'left','margin-top','margin-right','margin-bottom','margin-left',
        'padding-top','padding-right','padding-bottom','padding-left',
        'border-top-width','border-right-width','border-bottom-width',
        'border-left-width','border-top-color','border-right-color',
        'border-bottom-color','border-left-color','border-top-style',
        'border-right-style','border-bottom-style','border-left-style','position',
        'display','visibility','z-index','overflow-x','overflow-y','white-space',
        'clip','float','clear','cursor','list-style-image','list-style-position',
        'list-style-type','marker-offset'];
    var len = attr.length, obj = {};
    for (var i = 0; i < len; i++) 
        obj[attr[i]] = jQuery.fn.css2.call(this, attr[i]);
    return obj;
};

有了这两个代码,我们就可以来实现了

$('textarea').keyup(function () {
    var t = $(this);
    
    if (!this.justifyDoc) {
        this.justifyDoc = $(document.createElement('div'));

        // copy css
        this.justifyDoc.css(t.css()).css({
            'display'   :   'block',        // you can change to none
            'word-wrap' :   'break-word',
            'min-height':   t.height(),
            'height'    :   'auto'
        }).insertAfter(t.css('overflow-y', 'hidden'));
    }

    var html = t.val().replace(/&/g, '&amp;')
        .replace(/</g, '&lt;')
        .replace(/>/g, '&gt;')
        .replace(/'/g, '&#039;')
        .replace(/"/g, '&quot;')
        .replace(/ /g, '&nbsp;')
        .replace(/((&nbsp;)*)&nbsp;/g, '$1 ')
        .replace(/\n/g, '<br />')
        .replace(/<br \/>[ ]*$/, '<br />-')
        .replace(/<br \/> /g, '<br />&nbsp;');

    this.justifyDoc.html(html);
    t.height(this.justifyDoc.height());
});

它的运行效果如下

运行截图

它只有一个毛病,那就是换行的时候有一点延迟,也就是字符先换行了,然后才会被拉伸,因为我们响应的是keyup事件。实际上我想延迟是无法克服的,但能否想一个办法,让这个过程不要那么突兀呢?

看了你们的回答感觉都太复杂了,我的解决方法:获取textarea换行符数量,然后再更新其行高即可

$('textarea').on('input propertychange', function() {
            var v = $(this).val();
            var arr = v.split('\n');
            var len = arr.length;
            $(this).height(len*20);//20为行高;
        });
        

当内容行数较少时,如果要保持设定的行数,那么可以这样:

$('textarea').on('input propertychange', function() {
            var v = $(this).val();
            var arr = v.split('\n');
            var len = arr.length;
            var min=$(this)[0].rows;
            if(len>min){
                  $(this).height(len*20);//20为行高;
            }
        });

可以添加一个隐藏的textArea(类似1楼的方法),取这个textArea的scrollHeight然后设置给输入的textArea,不过这里还是要处理取值后的一些兼容性问题。关于keyup事件的问题,最好是用oninput事件和onpropertychange事件来替代,这样可以处理用鼠标添加或删除文字的问题,不过貌似ie9下使用backspace键删除文字不会触发onpropertychange事件,需要单独处理一下

表示回车的时候增加一字行高就应该不会出现滚动条闪烁的问题了吧?

/** fake code **/
textarea.keydown = function(e){
    if e.keycode == 13 { //忘了回车的 key code 是不是13了
        textarea.height = textarea.height + 1em;
    }
}
新手上路,请多包涵

试一下

/** fake code **/
textarea.keydown = function(e){
    if e.keycode == 13 { //忘了回车的 key code 是不是13了
        textarea.height = textarea.height + 1em;
    }
}

我的思路是,用一个div实时去获取textarea的内容,因为div高度是根据内容变化而变化的。我们可以得到div的高度,再反过来把div的高度设置给textarea。

下面不行你来问我~

<textarea class="form-control" style="padding: 0; margin: 0;box-sizing: content-box;" oninput="this.style.height = this.scrollHeight+'px'">我是自适应文本域哦  不行多输入点~~~</textarea>
推荐问题
宣传栏