前言
距离上一次更新已经过去了两个月了,往常我都是两个周左右更新一次,断更的原因主要是加入了 Varlet,哈哈哈,认识了很多大佬;次要原因就是不想发很随便的文章了,因为评论区时有不好的声音,所以要斟酌一下。
实现思路
textarea 换行有两种情况,一种是用户自己打了换行符;另外一种是文本行数一行装不下,自动换到了下一行。我们如何获取行数信息呢?
手动换行
这种情况比较简单,直接拿到文本使用 split 按照换行符切割就行
const splitedTexts = text.split(/\r|\r\n|\n/)
const rows = splitedTexts.length
自动换行
这个就有点不太好做了,毕竟不像手动换行那样,有个换行符标识,好做处理。刚开始我去网上看了一圈,看到很多人说,可以通过文本框的高度除以一个文字的高度来得到行数,这种方法乍一看挺合理,可是仔细想想就会发现这个方法会有些问题
判断高度比
比如,textarea本身被设置了rows,高度已经撑起来了
这个时候你再去判断 offsetHeight 或者 scrollHeight 都无济于事
单独设置一个元素来存放输入内容
这个一想感觉还是有操作空间的,我自己试了下,遇到了很多问题,先贴出来我写的建议demo(感兴趣的掘友可以把这段代码拿去 Vue SFC Playground 试试)
<script setup lang="ts">
import { type Ref, ref, onMounted, watch } from 'vue';
const textareaRef: Ref<HTMLElement | null> = ref(null)
const copyRef: Ref<HTMLElement | null> = ref(null)
const inputText: Ref<string> = ref('')
const rowHeight: Ref<number> = ref(0)
onMounted(() => {
if(copyRef.value){
copyRef.value.textContent = 'a'
rowHeight.value = copyRef.value.offsetHeight
copyRef.value.textContent = ''
}
})
watch(
() => inputText.value,
(_newText) => {
console.log(copyRef.value.offsetHeight / rowHeight.value)
}
)
</script>
<template>
<div class="container">
<div>
<textarea ref="textareaRef" v-model="inputText" class="textarea" placeholder="请输入" rows="8"></textarea>
<div ref="copyRef" class="copy">
{{ inputText }}
</div>
</div>
</div>
</template>
<style scoped>
.container {
width: 100%;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
}
.textarea {
overflow: auto;
padding: 10px 14px 0px 14px;
caret-color: #36b59d;
word-break: break-all;
line-height: 1;
font: 400 normal 14px/14px sans-serif;
}
.copy {
width: 150px;
word-wrap: break-word;
background-color: aqua;
word-break: break-all;
overflow: hidden;
font: 400 normal 14px/14px sans-serif;
}
</style>
这里我遇到了很多问题,比如copy 的那个div框的width就是要hardcode,适用 calc 会让文本超出不换行、多引入一个标签会导致布局或者其他问题等等
使用canvas计算宽度
这个是问了耗子君QAQ 的个人主页 - 文章 - 掘金 (juejin.cn),耗子哥给的一种思路。
canvas 具备离屏渲染的能力,这样我们就不用在页面上添加元素了,然后利用其 measureText 计算出文本宽度。拿输入的文本按照换行切割,累加切割后的每一个文本的行数,最后加上换行符切割后的文本数量就是行数了。
<script setup lang="ts">
import { type Ref, ref, onMounted, watch } from 'vue'
const textareaRef: Ref<HTMLElement | null> = ref(null)
const inputText: Ref<string> = ref('')
const ctx: Ref<CanvasRenderingContext2D | null> = ref(null)
onMounted(() => {
const canvas = document.createElement('canvas')
ctx.value = canvas.getContext('2d')
ctx.value.font = '400 normal 14px/14px sans-serif'
})
watch(
() => inputText.value,
(newText) => {
// 减去padding
const textareaWidth = textareaRef.value.clientWidth - 28
const splitedTexts = newText.split(/\r|\r\n|\n/)
const lineCount = splitedTexts.reduce((pre: number, cur: string) => {
const width = ctx.value.measureText(cur).width
return pre + Math.trunc(width / textareaWidth)
}, 0)
const rows = lineCount + splitedTexts.length
console.log(rows)
}
)
</script>
<template>
<div class="container">
<div>
<textarea
ref="textareaRef"
v-model="inputText"
class="textarea"
placeholder="请输入"
rows="8"
></textarea>
</div>
</div>
</template>
<style scoped>
.container {
width: 100%;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
}
.textarea {
overflow: auto;
padding: 10px 14px 0px 14px;
caret-color: #36b59d;
word-break: break-all;
line-height: 1;
font: 400 normal 14px/14px sans-serif;
}
</style>
不足之处
该方法在输入文本的数量越来越多时,差异会越来越大,这里放上一张截图
tips
在移动端,一般产品要求输入框限制行数,都是限制换行符的数量,因为如果把自动换行也考虑在内的话,一些小尺寸的设备就很容易出现问题。
结语
如果错误,欢迎指正