有学有练才叫学习:学而不思则罔,思而不学则殆:学而不习,纸上谈兵,习而不进,画地为牢!

textarea 高度自适应(实现高度内容自适应的 textarea)实现方式

代码编程 炮渣日记 3周前 (11-12) 18次浏览 已收录 0个评论 扫描二维码

背景介绍

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

textarea 的宽高是如何决定的? 参考张鑫旭的文章 HTML textarea cols,rows属性和宽度高度关系研究

那么, 我们今天的任务就是来思考如何创建一个 高度内容自适应的 textarea 组件,我将介绍三种思路实现 高度内容自适应的 textarea,具体代码 textareaAutoSizeSolutions

方案概要

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

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

textarea 的 onchange 触发 resize 方法,下面是 resize 方法的逻辑

textarea.style.height = 'auto';// 1. 让 textarea 的高度恢复默认
textarea.style.height = textarea.scrollHeight + 'px';// 2. textarea.scrollHeight 表示 *textarea* 内容的实际高度

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

textarea 构建时创建 ghostTextarea, onchange 触发 resize 方法:

  1. 创建 textarea 的时候, 同时创建一个一模一样的隐藏 ghostTextarea;
  2. ghostTextarea 的属性全部克隆自 textarea, 但是 ghostTextarea 是 隐藏 的, 并且 ghostTextarea.style.height = 0; 也就是说 ghostTextarea.scrollHeight 就是 textarea 中内容的真是高度。

resize 方法处理流程:

  1. textarea.value 先设置给 ghostTextarea,
  2. 拿到 ghostTextarea.scrollHeight
  3. 将 textarea.style.height = ghostTextarea.scrollHeight

方案三: 使用 (div | p | …).contenteditable 代替 textarea 作为输入框

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

方案对比

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

textarea 高度自适应(实现高度内容自适应的 textarea)实现方式

毫无疑问方案一是最优选择, 多加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. resize 事件的实现
// 重新计算 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 是最安全的。

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

const SIZING_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 的隐藏属性列表:

const HIDDEN_TEXTAREA_STYLE = {
	'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 事件

handleChange(e) {
	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;

textarea 高度自适应(实现高度内容自适应的 textarea)实现方式

更多优化思路

  • 页面存在多个 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 会出现以下两种问题:

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

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

placeholder 的实现:

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

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

优化点

去除支持富文本

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

textarea 高度自适应(实现高度内容自适应的 textarea)实现方式

监听 div 的 onPaste 事件

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

handlePaste 的更多兼容性处理

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

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

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

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

微博: 采用方案二

未输入时

textarea 高度自适应(实现高度内容自适应的 textarea)实现方式

输入后

textarea 高度自适应(实现高度内容自适应的 textarea)实现方式

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

textarea 高度自适应(实现高度内容自适应的 textarea)实现方式

ant.design: 采用方案二

体验超级棒哦

textarea 高度自适应(实现高度内容自适应的 textarea)实现方式

知乎: 采用方案三

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

textarea 高度自适应(实现高度内容自适应的 textarea)实现方式

希望本文能帮助到您!

喜欢 (0)
炮渣日记
关于作者:
发表我的评论
取消评论
表情 贴图 加粗 删除线 居中 斜体 签到

Hi,您需要填写昵称和邮箱!

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址