尝试获取textarea行数

2,130 阅读2分钟

前言

距离上一次更新已经过去了两个月了,往常我都是两个周左右更新一次,断更的原因主要是加入了 Varlet,哈哈哈,认识了很多大佬;次要原因就是不想发很随便的文章了,因为评论区时有不好的声音,所以要斟酌一下。

实现思路

textarea 换行有两种情况,一种是用户自己打了换行符;另外一种是文本行数一行装不下,自动换到了下一行。我们如何获取行数信息呢?

手动换行

这种情况比较简单,直接拿到文本使用 split 按照换行符切割就行

const splitedTexts = text.split(/\r|\r\n|\n/)
const rows = splitedTexts.length

自动换行

这个就有点不太好做了,毕竟不像手动换行那样,有个换行符标识,好做处理。刚开始我去网上看了一圈,看到很多人说,可以通过文本框的高度除以一个文字的高度来得到行数,这种方法乍一看挺合理,可是仔细想想就会发现这个方法会有些问题

判断高度比

比如,textarea本身被设置了rows,高度已经撑起来了

image.png

这个时候你再去判断 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),耗子哥给的一种思路。

image.png

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>

不足之处

该方法在输入文本的数量越来越多时,差异会越来越大,这里放上一张截图 image.png

image.png

tips

在移动端,一般产品要求输入框限制行数,都是限制换行符的数量,因为如果把自动换行也考虑在内的话,一些小尺寸的设备就很容易出现问题。

结语

如果错误,欢迎指正