实现一个自动高度的输入框
前言
大概在上个月的时候我就看了对应的一些资料,但是一直拖着,因为这个功能其实vue的框架就有提供,比如element ui的input组件,它的这个功能叫自适应文本域,属性为autosize
。
所以本文也不过多讲解具体实现,主要还是它的原理层的东西。
这个功能可以用在哪呢?比如移动端的聊天输入框的高度判断,拿我们的QQ来讲,在没有内容的时候它就只有1行的高度,如果内容过高,就会自动变高,然后也会有一个max的最大高度阈值,这个高度一般是几行,比如最大6行高度,多了就滚动条处理。
那么就开始吧!
最基本的实现变高原理
其实非常简单,假设我现在有一个textarea元素用于用户的输入,然后我希望它在用户内容过多时自动加高,element是这么实现的,他会创建一份用户不可见的textarea元素,我们称之为影子textarea;然后监听用户输入的内容,将内容赋值给影子textarea。
此时影子textarea与真实textarea内容相同,那么我们只需要计算出影子textarea的高度,然后将这个高度赋值给真实textarea,不就可以实现自动变高的需求了。
<div>
<textarea id="real-textarea"> </textarea>
</div>
<div style="margin-top: 20px">
<textarea id="shadow-textarea"> </textarea>
</div>
textarea {
box-sizing: border-box;
height: 20px;
}
这里我们创建两个textarea元素,为了省事我使用了box-sizing: border-box;
css,由于是demo演示,所以不用尽善尽美,后续也会讲这块怎么处理。
const realTextarea = document.getElementById("real-textarea");
const shadowTextarea = document.getElementById("shadow-textarea");
realTextarea.addEventListener("input", (e) => {
shadowTextarea.value = e.target.value;
//计算高度
let height = shadowTextarea.scrollHeight;
realTextarea.style.height = height + "px";
});
此时我们的原理就已经展示完毕了,scrollHeight
获取到textarea元素的内容区域的实际大小;包括不在页面中的可滚动部分(内容和内边距)。
也就是说我们这里拿到的是:content高度 + 上下padding高度 + scroll滚动区域高度
如果没有滚动条,那么滚动区域高度为0。
但是其实这里会有一个小问题,我们看图:
你会发现真实的输入框它的高度虽然变了,但是会有滚动条,这说明我们的高度计算有问题,为什么?
细心的同学相信已经知道为什么了,我们的scrollHeight获得的高度不是完整的textarea高度,一个textarea它的高度还需要上下border的高度。
所以我们的代码需要这么完善一下:
const realTextarea = document.getElementById("real-textarea");
const shadowTextarea = document.getElementById("shadow-textarea");
realTextarea.addEventListener("input", (e) => {
shadowTextarea.value = e.target.value;
//计算高度
let height = shadowTextarea.scrollHeight;
const style = window.getComputedStyle(e.target);
height += Number.parseFloat(style.getPropertyValue("border-bottom-width"));
height += Number.parseFloat(style.getPropertyValue("border-top-width"));
realTextarea.style.height = height + "px";
});
需要注意getComputedStyle
是用于计算当前元素实际渲染的大小,所以这个元素绝对不能被display:none;
,切记!!!
由于getPropertyValue
得到的是一个带px单位的值,所以我们需要利用parseFloat
方法解析这个字符串得到浮点数。
此时我们再去看看效果图:
问题已经解决。
样式带来的问题
从上面最基本的实现中我们可以联想到,元素的样式是可以影响到高度的,除了padding,border,甚至受box-sizing、字体大小、字体缩进,字体间距,行高,等等...。
非常多的css会影响元素高度,所以我们的影子textarea,它需要将真实textarea中,会影响到高度的样式自己也复制一份,这样才能保证两个元素的基建是相同的。这样计算得到的高度才是正确的。
那么我们也不需要自己去想有哪些会影响,在element中提供了一个常量数组,它里面保存了能影响到高度计算的所有样式。
const CONTEXT_STYLE = [
'letter-spacing',
'line-height',
'padding-top',
'padding-bottom',
'font-family',
'font-weight',
'font-size',
'text-rendering',
'text-transform',
'width',
'text-indent',
'padding-left',
'padding-right',
'border-width',
'box-sizing',
]
有了这份样式,我们就可以这么来进行计算:
function calculateNodeStyling(targetElement) {
const style = window.getComputedStyle(targetElement);
const boxSizing = style.getPropertyValue("box-sizing");
const paddingSize =
Number.parseFloat(style.getPropertyValue("padding-bottom")) +
Number.parseFloat(style.getPropertyValue("padding-top"));
const borderSize =
Number.parseFloat(style.getPropertyValue("border-bottom-width")) +
Number.parseFloat(style.getPropertyValue("border-top-width"));
const contextStyle = CONTEXT_STYLE.map((name) => `${name}:${style.getPropertyValue(name)}`).join(";");
return {
contextStyle,
paddingSize,
borderSize,
boxSizing
};
}
element中有这么一个计算函数,它返回了一些我们将会用到的属性:
- contextStyle 真实元素的样式属性,用于给影子textarea赋值样式用;
- paddingSize padding上下的大小
- borderSize 上下边框的大小
- boxSizing 盒子模型,用于针对不同盒子模型的计算
realTextarea.addEventListener("input", (e) => {
shadowTextarea.value = e.target.value;
//计算高度
const {
contextStyle,
paddingSize,
borderSize,
boxSizing
} = calculateNodeStyling(e.target);
//影子复制样式
shadowTextarea.setAttribute("style", contextStyle);
//设置高度
let height = shadowTextarea.scrollHeight;
if (boxSizing === "border-box") {
height = height + borderSize;
} else if (boxSizing === "content-box") {
height = height - paddingSize;
}
realTextarea.style.height = height + "px";
});
这里boxSizing的if判断我照搬过来了:
- 如果是"border-box",那自然是得加上边框border的高度;
- 如果是"content-box",元素的高度就是content高度,scrollHeight中包含padding高度,所以需要减去
此时我们得到的就是一个非常正确的高度了。
如何影藏影子textarea
element也提供了一个常量样式,我们可以在复制样式的时候同时将其使用:
const HIDDEN_STYLE = `
height:0 !important;
visibility:hidden !important;
overflow:hidden !important;
position:absolute !important;
z-index:-1000 !important;
top:0 !important;
right:0 !important;
`
realTextarea.addEventListener("input", (e) => {
shadowTextarea.value = e.target.value;
//计算高度
const {
contextStyle,
paddingSize,
borderSize,
boxSizing
} = calculateNodeStyling(e.target);
//影子复制样式
shadowTextarea.setAttribute("style", `${contextStyle};${HIDDEN_STYLE}`);
//设置高度
let height = shadowTextarea.scrollHeight;
if (boxSizing === "border-box") {
height = height + borderSize;
} else if (boxSizing === "content-box") {
height = height - paddingSize;
}
realTextarea.style.height = height + "px";
});
最大最小行数高度限制
最低行数高度
有时候我们希望textarea在不满足多少行时,默认有指定行数的高度,假设我们要求3行高度。
const minRows = 3;
realTextarea.addEventListener("input", (e) => {
shadowTextarea.value = e.target.value;
//计算高度
const {
contextStyle,
paddingSize,
borderSize,
boxSizing
} = calculateNodeStyling(e.target);
//影子复制样式
shadowTextarea.setAttribute("style", `${contextStyle};${HIDDEN_STYLE}`);
//设置高度
let height = shadowTextarea.scrollHeight;
if (boxSizing === "border-box") {
height = height + borderSize;
} else if (boxSizing === "content-box") {
height = height - paddingSize;
}
//最小3行
shadowTextarea.value = "";
const singleRowHeight = shadowTextarea.scrollHeight - paddingSize; //得到单行内容高度
if (typeof minRows === "number") {
let miniHeight = singleRowHeight * minRows; //3行的高度
if (boxSizing === "border-box") {
miniHeight = miniHeight + paddingSize + borderSize;
}
height = Math.max(height, miniHeight); //取最大值,如果实际高度小于3行高度,就取3行高度
}
realTextarea.style.height = height + "px";
});
原理也很简单,由于我们的height已经算好了,现在只需要处理最大最小行数高度就行了,那么直接将影子textarea的value复制空字符,这样他就一定会是一行的内容了。
此时再去获取scrollHeight
的高度,再减去padding的大小,得到的就是纯行号的高度,这样就可以省去判断boxSizing === "content-box"了。
此时我们在乘以最小行数得到纯内容的最小高度,再通过判断boxSizing得到实际元素的高度。
此时再通过Math.max
去一个最大值即可。
最大行数高度
做法和最小是一样的,只是变动了一点点。假设我们最大是6行的高度。
const minRows = 3;
const maxRows = 6;
realTextarea.addEventListener("input", (e) => {
shadowTextarea.value = e.target.value;
//计算高度
const {
contextStyle,
paddingSize,
borderSize,
boxSizing
} = calculateNodeStyling(e.target);
//影子复制样式
shadowTextarea.setAttribute("style", `${contextStyle};${HIDDEN_STYLE}`);
//设置高度
let height = shadowTextarea.scrollHeight;
if (boxSizing === "border-box") {
height = height + borderSize;
} else if (boxSizing === "content-box") {
height = height - paddingSize;
}
//最小3行
shadowTextarea.value = "";
const singleRowHeight = shadowTextarea.scrollHeight - paddingSize; //得到单行内容高度
if (typeof minRows === "number") {
let miniHeight = singleRowHeight * minRows; //3行的高度
if (boxSizing === "border-box") {
miniHeight = miniHeight + paddingSize + borderSize;
}
height = Math.max(height, miniHeight); //取最大值,如果实际高度小于3行高度,就取3行高度
}
//最大6行
if (typeof maxRows === "number") {
let maxHeight = singleRowHeight * maxRows; //6行的高度
if (boxSizing === "border-box") {
maxHeight = maxHeight + paddingSize + borderSize;
}
height = Math.min(height, maxHeight); //取最小值,如果实际高度大于6行高度,就取6行高度
}
realTextarea.style.height = height + "px";
});
效果展示
由于只是demo,所以一些展示逻辑没有完善,但是主要原理已全部阐述明白,不再过多赘述。
可以看到,我们在input事件触发后,影子textarea就会消失,因为我们给他的style赋值了HIDDEN_STYLE
常量的值,然后高度也突然从1行变成3行的高度,当内容不断填充时,超出6行高度后,高度不再变化了。
此时完全符合我们的代码逻辑,展示没问题。
element源码
核心代码:input.ts
本文系作者 @木灵鱼儿 原创发布在木灵鱼儿站点。未经许可,禁止转载。
暂无评论数据