美文网首页我爱编程
[2018-03-25]高度自适应的Textarea

[2018-03-25]高度自适应的Textarea

作者: 岑早早咯 | 来源:发表于2018-04-15 16:29 被阅读0次

    高度自适应的 textarea

    背景介绍

    正如我们所知道的 textarea 是一个行内块元素 display: inline-block 并且它的默认宽高由 cols & rows 决定, 也就是说 textarea 的 height 并不会自适应于内容长度.

    textarea 的宽高是如何决定的?

    参考文章: http://www.zhangxinxu.com/wordpress/2016/02/html-textarea-rows-height/

    那么, 我们今天的任务就是来思考如何创建一个 高度内容自适应的 textarea 组件.

    我将介绍三种思路实现 高度内容自适应的 textarea.

    这三种思路的 React 组件实现代码如下:

    https://github.com/teeeemoji/textareaAutoSizeSolutions

    所有参考链接(锚点失效, 参考链接在最后)

    方案概要

    这是三种方案的概述和实现思路的简介, 实现方案 & 遇到的坑 & 拓展知识点, 点击 (查看更多) or 直接看 teeeemoji 的 demo.

    • 方案一: 两次调整 textarea.style.height

      textarea 的 onchange 触发 resize 方法
      下面是 resize 方法的逻辑
      ① textarea.style.height = 'auto';// 让 textarea 的高度恢复默认
      ② textarea.style.height = textarea.scrollHeight + 'px';// textarea.scrollHeight 表示 textarea 内容的实际高度

    • 方案二: 利用一个 ghostTextarea 获得输入框内容高度, 再将这个高度设置给真实的 textarea

      textarea 构建时创建 ghostTextarea, onchange 触发 resize 方法
      ① 创建 textarea 的时候, 同时创建一个一模一样的隐藏 ghostTextarea;
      ② ghostTextarea 的属性全部克隆自 textarea, 但是 ghostTextarea 是隐藏的, 并且 ghostTextarea.style.height = 0; 也就是说 ghostTextarea.scrollHeight 就是 textarea 中内容的真是高度
      resize 方法处理流程

      • step-1: textarea.value 先设置给 ghostTextarea,
      • step-2: 拿到 ghostTextarea.scrollHeight
      • step-2: 将 textarea.style.height = ghostTextarea.scrollHeight
    • 方案三: 使用 (div | p | ...).contenteditable 代替 textarea 作为输入框

      div 是块级元素, 高度本身就是内容自适应的(除非设置 max-width or min-widht
      使用contenteditable 让 div 代替 textarea, 省去各种计算高度的逻辑

    方案对比

    满分3分, 三种方案通过优化, 在用户体验和兼容性上都能达到满分. 因此差别仅仅在于这几个方案的实现难度. (仅仅是基于 react 组件的实现复杂度).

    用户体验对比(在最后面, 简书对 markdown 内嵌 html 支持不友好, 锚点都不能用了)

    方案 用户体验 兼容性 易用性 综合评价
    方案一 3 3 3 10
    方案二 3 3 1 7
    方案三 3 3 2 8

    毫无疑问方案一是最优选择, 多加1分以示奖励;

    方案一: 两次调整 textarea.style.height

    实现思路

    1. 渲染一个 textarea 元素
    <textarea
        ref={this.bindRef}
        className={style['textarea'] + ' ' + className}
        placeholder={placeholder}
        value={value}
        onChange={this.handleChange} // 看这里
    />
    
    1. textarea 的 onChange 事件触发 resize
    handleChange(e) {
        this.props.onChange(e.target.value);
        this.resize();  // 看这里
    }
    
    1. reize 事件的实现
    // 重新计算 textarea 的高度
    resize() {
        if (this.inputRef) {
            console.log('resizing...')
            this.inputRef.style.height = 'auto';
            this.inputRef.style.height = this.inputRef.scrollHeight + 'px';
        }
    }
    
    1. 注意 componentDidMount 的时候, 执行一次 resize 方法, 初始化 textarea 的高度哦.

    优化点

    避免两次渲染,造成内容抖动

    在 react 中, 组件 receiveProps 的时候会 render一次, 直接调整 textarea 的 height 也会浏览器的重绘.那么就会造成两次重绘, 并且两次重绘的时候, textarea 的内容可能会发生抖动.

    优化思路

    先触发 resize 后触发 render **用最简单的思路完美解决问题

    方案二: 利用一个 ghostTextarea 获得输入框内容高度, 再将这个高度设置给真实的 textarea

    实现思路

    • 同时渲染两个 textarea, 一个真实 textarea 一个隐藏 textarea
    return (
        <div className={style['comp-textarea-with-ghost']}>
            <textarea // 这个是真的
                ref={this.bindRef}
                className={style['textarea'] + ' ' + className}
                placeholder={placeholder}
                value={value}
                onChange={this.handleChange}
                style={{height}}
            />
            <textarea // 这个是 ghostTextarea
                className={style['textarea-ghost']}
                ref={this.bindGhostRef}
                onChange={noop}
            />
        </div>
    )
    
    • 初始化的时候拷贝属性

      初始化必须使用工具方法将 textarea 的属性拷贝到 ghostTextarea 去. 因为 textarea 的样式再组件外也能控制, 因此初始化的时候 copy style 是最安全的

      这是所以要拷贝的属性的列表

      'letter-spacing',
      'line-height',
      'font-family',
      'font-weight',
      'font-size',
      'font-style',
      'tab-size',
      'text-rendering',
      'text-transform',
      'width',
      'text-indent',
      'padding-top',
      'padding-right',
      'padding-bottom',
      'padding-left',
      'border-top-width',
      'border-right-width',
      'border-bottom-width',
      'border-left-width',
      'box-sizing'
      ];
      

      这是 ghostTextarea 的隐藏属性列表

      'min-height': '0',
      'max-height': 'none',
      height: '0',
      visibility: 'hidden',
      overflow: 'hidden',
      position: 'absolute',
      'z-index': '-1000',
      top: '0',
      right: '0',
      };
      

      这是拷贝 style 的工具方法

      // 拿到真实 textarea 的所有 style
      function calculateNodeStyling(node) {
           const style = window.getComputedStyle(node);
           if (style === null) {
               return null;
           }
           return SIZING_STYLE.reduce((obj, name) => {
               obj[name] = style.getPropertyValue(name);
                   return obj;
           }, {});
       }
      // 拷贝 真实 textarea 的 style 到 ghostTextarea
      export const copyStyle = function (toNode, fromNode) {
          const nodeStyling = calculateNodeStyling(fromNode);
          if (nodeStyling === null) {
              return null;
          }
          Object.keys(nodeStyling).forEach(key => {
              toNode.style[key] = nodeStyling[key];
          });
          Object.keys(HIDDEN_TEXTAREA_STYLE).forEach(key => {
              toNode.style.setProperty(
                  key,
                  HIDDEN_TEXTAREA_STYLE[key],
                  'important',
              );
          });
      }
      
    • textarea 的 onChange 事件

      先 reize 再触发 change 事件

          this.resize();
          let value = e.target.value;
          this.props.onChange(value);
      }
      
    • textarea 的 resize 方法

      resize() {
          console.log('resizing...')
          const height = calculateGhostTextareaHeight(this.ghostRef, this.inputRef);
          this.setState({height});
      }
      
    • calculateGhostTextareaHeight 工具方法

      先将内容设置进 ghostTextarea, 再拿到 ghostTextarea.scrollHeight

      export const calculateGhostTextareaHeight = function (ghostTextarea, textarea) {
           if (!ghostTextarea) {
               return;
           }
           ghostTextarea.value = textarea.value || textarea.placeholder || 'x'
           return ghostTextarea.scrollHeight;
      }
      

    优化点

    避免两次渲染,造成内容抖动

    在 react 中, 组件 receiveProps 的时候会 render一次, 给 textarea 设置 height 属性也会浏览器的重绘.那么就会造成两次重绘, 并且两次重绘的时候, textarea 的内容可能会发生抖动.

    下面两种思路, 再 demo 中均有体现

    优化思路一: 合并祯渲染

    使用 window.requestAnimationFrame & window.cancelAnimationFrame 来取消第一祯的渲染, 而直接渲染高度已经调整好的 textarea;

    优化思路二: 减少渲染次数

    利用 react 批处理 setState 方法, 减少 rerender 的特性;
    textarea onChange 方法中同时触发两个 setState;

    123.png

    更多优化思路

    • 页面存在多个 textarea 的时候, 能不能考虑 复用同一个 ghostTextarea

    方案三: 使用 div.contenteditable 代替 textarea

    实现思路

    • 渲染一个 div.contenteditable=true

      return (
      <div className={style['comp-div-contenteditable']}>
          <div
              ref={this.bindRef}
              className={classname(style['textarea'], className, {[style['empty']]: !value})}
              onChange={this.handleChange}
              onPaste={this.handlePaste}
              placeholder={placeholder}
              contentEditable
          />
      </div>
      )
      
    • 获取 & 设置 编辑去呀的内容

      textarea 通过 textarea.value 来取值 or 设置值, 但换成了 div 之后, 就要使用 div.innerHTML or div.innerText 来取值 or 设置值.

      使用 div.innerHTML 会出现以下两种问题:

      • & 会被转码成 &amp ;
      • 空白符合并
        使用 div.innerText 在低版本 firfox 上要做兼容处理.

      因此使用哪种方式主要看需求.

    • placeholder 的实现

      div 的 placeholder 属性是无效, 不会显示出来的, 现存一种最简单的方式, 使用纯 css 的方式实现 div 的 placeholder

      .textarea[placeholder]:empty:before { /*empty & before 两个伪类*/
          content: attr(placeholder); /*attr 函数*/
          color: #555;
      }
      

    优化点

    去除支持富文本

    div.contenteditable 是默认支持富文本的, 可能会以 粘贴 or 拖拽 让输入框出现富文本;

    234.png

    监听 div 的 onPaste 事件

    handlePaste(e) {
        e.preventDefault();
        let text = e.clipboardData.getData('text/plain'); // 拿到纯文本
        document.execCommand('insertText', false, text); // 让浏览器执行插入文本操作
    }
    

    handlePaste 的更多兼容性处理

    几个大网站的高度自适应 textarea 对比

    我分别查看了微博, ant.design组件库, 知乎 的自适应输入框的实现.

    微博: 采用方案二

    未输入时

    5aa4b41fdf0082f1c9.png

    输入后

    5aa4b4517a668254df.png

    但是微博的实现存在用户体验上的缺陷, 会抖动!!!

    weibo.git.gif

    ant.design: 采用方案二

    体验超级棒哦

    antd.gif

    知乎: 采用方案三

    看上去竟然存在 bug , 其实上面的截图也有

    zhih.gif

    参考链接列表

    相关文章

      网友评论

        本文标题:[2018-03-25]高度自适应的Textarea

        本文链接:https://www.haomeiwen.com/subject/icxkcftx.html