输入框相关,一篇文章总结所有前端文本输入的应用场景和实现方法,(包含源码,建议收藏)
前言
本篇文章所有的代码,都是在 vue + vite + ts 项目基础之上实现的,这样也是为了方便大家直接用源码,在开始之前建议大家阅读这篇《零基础搭建 vite项 目教程》。此项目就是这个教程搭建的,本篇文章关于输入框的相关代码是此项目的一个分支(content-input)。如果你没有时间阅读详细的教程,你也可以直接在 git 上克隆项目。
learn-vite: 搭建简单的vite+ts+vue框架
本篇文章的所有代码都在 content-input 分支上,基本内容大致如下(后续代码可能会有优化,建议以最新的代码为准):

把项目克隆之后切换到 content-input 分支,运行 pnpm install ,然后运行 npm run dev 直接访问 http://localhost:80/contentInput 就可以看到本篇文章涉及的全部内容。
注意,需要使用 pnpm 命令安装包,因为我的这个项目是使用 pnpm 构建的,否则会报错,如下图。

本篇文章的主要内容是文本输入,也就是输入文字的输入框的实现和应用场景。前端关于输入框的实现主要分为四种(1)input 标签(2)textarea 标签 (3)contenteditable 属性 (4)富文本编辑器插件。
除了文本输入,前端的输入形式还有文件、图片、视频、音频等,我们在本篇文章暂时不考虑。
一、单行文本 Input 标签
h5 的原生 input 标签是最常用的单行文本输入功能,一般简单的功能用原生标签就可以,但是我们在使用的过程中,除了要自己修改样式,还有一些注意事项。
1.1 原生 input 标签
- input 标签是自闭合标签
- input 标签是行内元素
- 内容只能展示一行,不能换行
- 当高度小于字体大小的时候,上下内容会隐藏,不能设置 overflow,因为它是行内元素
- 需要设置背景色,因为 input 有默认的白色背景
- 需要设置 outline: none,否则会有蓝色轮廓框
- input 标签还可以输入文件类型
1.2 组件库
有的时候我们需要给输入框增加前缀图标,或者是清空按钮,除了可以自己封装,还可以使用组件库,但是都需要自己调整样式。
1.3 源代码
<template>
<div class="page-container">
<h1 class="title">input 标签输入文本</h1>
<ol>
<li>自闭合标签</li>
<li>行内元素</li>
<li>内容只能展示一行,不能换行</li>
<li>当高度小于字体大小的时候,上下内容会隐藏,不能设置 overflow,因为他是行内元素</li>
<li>需要设置背景色,因为input 有默认的白色背景</li>
<li>需要设置 outline: none,否则会有蓝色轮廓框</li>
</ol>
<div class="sample-box">
<div class="sample-label">一个默认的输入框</div>
<input v-model="myName" placeholder="请输入名称" />
<div class="sample-label">设置了样式的输入框</div>
<input v-model="myName" class="input-ele" placeholder="请输入名称" />
<div class="sample-label">字体大于标签高度,上下内容会隐藏,不能设置 overflow</div>
<input v-model="myName" class="input-ele input-2" placeholder="请输入名称" />
</div>
</div>
</template>
<script lang="ts" setup>
import { onMounted, ref } from 'vue'
const myName = ref('你好')
</script>
<style lang="scss" scoped>
.page-container {
padding: 24px;
ol {
list-style: decimal;
padding-left: 14px;
}
.title {
margin: 10px 0;
}
.sample-box {
margin-top: 10px;
padding: 10px 20px;
width: 500px;
border-radius: 6px;
background: #eee;
.sample-label {
margin: 10px 0 5px;
}
.input-ele {
border: 1px solid;
width: 100%;
outline: none; // 输入框的轮廓特别明显
background: transparent; // 一般都需要设置背景色,要不然默认为白色
&.input-2 {
display: inline-block;
overflow: auto;
height: 32px;
font-size: 200px;
}
}
}
}
</style>
二、多行文本 textarea 标签
textarea 原生标签支持多行文本的输入,在使用常规功能的时候同样也有一些注意事项。
2.1 原生 textarea 标签
- 自闭合标签
- 块级元素
- 可以自动换行,可以展示多行文本
- 当高度小于字体大小的时候,默认会滚动,也可以设置 overflow:hidden,不让它滚动
- 需要设置背景色,因为 textarea 有默认的白色背景
- 需要设置 outline: none,否则会有蓝色轮廓框
2.2 让高度随着内容动态变化
原生的 textarea 标签默认文本很长的时候滚动展示,不会撑起高度,除非我们手动给 textarea 设置高度,但是也无法实现输入框的内容随着内容动态变化。
2.2.1 实现高度自适应
(1)基本思路
输入框高度动态变化的需求很常见,如:网站的评论,我们需要实现这个功能,基本思路是
- 给 textarea 元素设置一个 height 默认高度
- 给 textarea 元素设置一个 min-height、max-height 最大、最小高度
- 监听 input 事件
- input 事件触发,获取 event.srcElement.scrollHeight
- 根据上述的 scrollHeight 更新 textarea 元素的高度
但是这样写是有问题的
- 这样写有一个问题,event.srcElement.scrollHeight 并不是删除文字之后的高度,导致在选中大量的文字之后删除,textarea 元素的高度并不能更新为文字删除后的高度
- 除了使用 input 事件,还有一个办法是使用 vue 中的 watch 监听输入框值的变化,但是同样无法准确根据文字内容更新高度
- 因为我们高度的获取和设置都是针对同一个 textarea 的 dom
- 解决办法:克隆一个一摸一样的 textarea(第三章说)
(2)核心代码
<textarea ref="inputEle5" v-model="myName" class="input-ele input-4" placeholder="请输入名称" @input="input" />
const input = (e: any) => {
if (!inputEle5.value) {
return
}
const scrollHeight = e.srcElement.scrollHeight
inputEle5.value.style.height = `${scrollHeight}px`
}
这里面我们提到了 scrollHeight 即 文本内容的高度,我们应该知道 textarea 的scrollHeight 代表着所有文本内容的高度 + 上下 padding
2.2.2 最少 3 行,最多 5 行
(1)基本思路
- 可以参考上面 2.2.1 的办法
- 需要设置 min-height 和 max-height
- 最小高度 = 单行的高度 * 3 (最少行数)
- 最大高度 = 单行的高度 * 5(最多行数)
但是这样写同样有问题
- 除了 2.2.1 中的问题,还有个问题就是如何知道单行的高度?
- 单行的高度 = textarea 的 line-height
- 如果我们写死最小高度和最大高度,我们在改变 line-height 都需要重新改一下最小高度和最大高度,这很麻烦
- 解决办法:克隆一个一摸一样的 textarea(第三章说)
(2)核心代码
<textarea ref="inputEle6" v-model="myName" rows="3" class="input-ele input-6" placeholder="请输入名称" @input="input2" />
const input2 = (e: any) => {
console.log(e)
if (!inputEle5.value) {
return
}
const scrollHeight = e.srcElement.scrollHeight
inputEle6.value.style.height = `${scrollHeight}px`
}
&.input-6 {
width: 200px;
height: 72px;
min-height: 72px; // line-height: 24 ,3行 72px
max-height: 120px; // line-height: 24 ,5行 120px
}
2.3 组件库
组件库提供了很完善的多行文本输入功能,包括支持高度自适应,支持最少输入的行数、最大输入的行数,所以有了组件库我们就不用自己费劲实现多行文本输入框的高度自适应了。
2.4 源代码
<template>
<div class="page-container">
<h1 class="title">textarea 标签输入文本,常规功能</h1>
<ol>
<li>自闭合标签</li>
<li>块级元素</li>
<li>可以自动换行,可以展示多行文本</li>
<li>当高度小于字体大小的时候,默认会滚动,也可以设置 overflow:hidden,不让它滚动</li>
<li>需要设置背景色,因为 textarea 有默认的白色背景</li>
<li>需要设置 outline: none,否则会有蓝色轮廓框</li>
</ol>
<div class="sample-box">
<div class="sample-label">1. 一个默认的输入框</div>
<textarea v-model="myName" placeholder="请输入名称" />
<div class="sample-label">2. 设置了样式的输入框</div>
<textarea v-model="myName" class="input-ele" placeholder="请输入名称" />
<div class="sample-label">3. 设置了宽度的输入框,高度默认展示两行,内容多的时候高度不会自动调整,会滚动展示</div>
<textarea v-model="myName2" class="input-ele input-2" placeholder="请输入名称" />
<div class="sample-label">4. 设置了宽度的输入框,不设置高度,展示指定的行数:不管内容如何变化,始终展示3行</div>
<textarea v-model="myName2" rows="3" class="input-ele input-3" placeholder="请输入名称" />
<div class="sample-label">5. 让高度随着内容动态变化</div>
<div>实现步骤:</div>
<ol>
<li>给 textarea 元素设置一个 height 默认高度</li>
<li>给 textarea 元素设置一个 min-height、max-height 最大、最小高度</li>
<li>监听 input 事件</li>
<li>input 事件触发,获取 event.srcElement.scrollHeight</li>
<li>根据上述的 scrollHeight 更新 textarea 元素的高度</li>
</ol>
<div>问题:</div>
<ol>
<li>这样写有一个问题,event.srcElement.scrollHeight 并不是删除文字之后的高度,导致在选中大量的文字之后删除,textarea 元素的高度并不能更新为文字删除后的高度</li>
<li>除了使用 input 事件,还有一个办法是使用 vue 中的 watch 监听输入框值的变化,但是同样无法准确根据文字内容更新高度</li>
<li>因为我们高度的获取和设置都是针对同一个 textarea 的 dom</li>
<li>解决办法:克隆一个一摸一样的 textarea(下面说)</li>
</ol>
<textarea ref="inputEle5" v-model="myName" class="input-ele input-4" placeholder="请输入名称" @input="input" />
<div class="sample-label">6. 让高度随着内容动态变化,最少展示3行,最多5行</div>
<div>实现步骤:</div>
<ol>
<li>可以参考 5 中办法</li>
<li>需要设置 min-height 和 max-height</li>
<li>最小高度 = 单行的高度 * 3 (最少行数)</li>
<li>最大高度 = 单行的高度 * 5(最多行数)</li>
</ol>
<div>问题:</div>
<ol>
<li>除了 5 中的问题,还有个问题就是如何知道单行的高度?</li>
<li>单行的高度 = textarea 的 line-height</li>
<li>如果我们写死最小高度和最大高度,我们在改变 line-height 都需要重新改一下最小高度和最大高度,这很麻烦</li>
<li>解决办法:克隆一个一摸一样的 textarea(下面说)</li>
</ol>
<textarea ref="inputEle6" v-model="myName" rows="3" class="input-ele input-6" placeholder="请输入名称" @input="input2" />
<div class="sample-label">7. 让高度随着内容动态变化使用组件库</div>
<div>组件库如 elementPlus 或 antd 都提供了 autosize 的输入框,还能设置最大行数和最小行数</div>
<ol>
<li>一半情况使用组件库就能满足我们的要求了,但是我们需要自己改样式</li>
<li>上面提到的解决方案【克隆一个一摸一样的 textarea】实际上就是组件库的实现原理</li>
</ol>
<a-textarea style="width: 200px" v-model:value="myName" placeholder="请输入名称 antd autosize" :auto-size="{ minRows: 2, maxRows: 5 }" />
<div class="sample-label" style="color: red">8. 使用组件库的实现原理自己实现,textarea 让高度随着内容动态变化,支持最小行数、最大行数,</div>
<div>
具体实现在这里
<a href="/textInput3">textarea 标签,实现高度自适应的输入框</a>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { watch, onMounted, ref } from 'vue'
import { Textarea as ATextarea } from 'ant-design-vue'
const myName = ref('你好')
const myName2 = ref(
'在 JavaScript 中,迭代器模式通过内建的 Iterator 接口和 Iterable 协议实现。它允许你遍历对象,而不必了解对象的内部结构。JavaScript 中的迭代器:通过内建的迭代器协议和 Symbol.iterator,实现了类似的功能,但更加简洁和灵活。你可以通过自定义迭代器来控制对象的遍历行为,甚至可以使用 for...of 循环来简化代码',
)
const inputEle5 = ref()
watch(
() => myName.value,
() => {
console.log('wathc zhi')
},
)
const input = (e: any) => {
if (!inputEle5.value) {
return
}
const scrollHeight = e.srcElement.scrollHeight
inputEle5.value.style.height = `${scrollHeight}px`
}
const inputEle6 = ref()
const input2 = (e: any) => {
console.log(e)
if (!inputEle5.value) {
return
}
const scrollHeight = e.srcElement.scrollHeight
inputEle6.value.style.height = `${scrollHeight}px`
}
</script>
<style lang="scss" scoped>
.page-container {
padding: 24px;
ol {
list-style: decimal;
padding-left: 14px;
}
.title {
margin: 10px 0;
}
.sample-box {
margin: 10px 0 100px;
padding: 10px 20px;
width: 100%;
border-radius: 6px;
background: #eee;
.sample-label {
font-size: 16px;
font-weight: bold;
margin: 10px 0 5px;
}
.input-ele {
font-size: 14px;
line-height: 24px;
border: 1px solid;
outline: none; // 输入框的轮廓特别明显
background: transparent; // 一般都需要设置背景色,要不然默认为白色
&.input-2 {
width: 200px;
}
&.input-3 {
width: 200px;
}
&.input-4 {
width: 200px;
height: 24px;
min-height: 24px;
}
&.input-6 {
width: 200px;
height: 72px;
min-height: 72px; // line-height: 24 ,3行 72px
max-height: 120px; // line-height: 24 ,5行 120px
}
}
}
}
</style>
三、textarea 的复杂实践
3.1 实现高度自适应的输入框
上一节我们提到了使用 textarea 标签实现输入框的高度自适应的基本思路,但是使用 scrollHeight 是会存在问题的。
除非我们使用组件库,但是我们还是要学习一下组件库是如何实现完美的这个功能的,因为其他功能也会用到,这是一种实现思路,学习是一劳永逸的。
我这篇文章也是看了 elementPlus 的源码之后才知道是怎么弄的,所以代码部分和elementPlus 有很大的雷同,不过知识学会了就行。
要实现这个功能的基本思路是,我们监听输入框文本的变化然后计算文本内容的高度,再更新输入框的高度。
3.1.1 基本思路
- 页面上写一个 textarea 标签
- 监听 textarea 的 value 值的变化
- 值变化的时候调用 resizeTextarea 方法
- resizeTextarea 方法,支持传入可选参数最小行数 minRows 和最大行数 maxRows
- 初始化的时候调用一次 resizeTextarea 方法
3.1.2 resizeTextarea 方法
- 原理是根据页面上的输入框,克隆一个一摸一样的 textarea 元素
- 一摸一样是指:所有的 css 属性都一样,包括 font-size、line-height、padding 等所有能影响 height 的css 属性,如下:
- box-sizing: content-box 和 border-box 需要有不同的计算方法
- padding-top
- padding-bottom
- padding-left
- padding-right
- line-height
- letter-spacing
- font-size
- font-family
- text-indent: 缩近
- text-rendering:定义浏览器渲染引擎如何渲染字体,不同属性针对不同字体又不同的效果
- text-transform: 指定文本的大小写
- width
- border-width
- 接受参数 “textarea”即页面上原始的输入框的 Dom 元素,用于克隆它的样式,使用 window.getComputedStyle(textareaDom) 获取原始 textarea 的各种属性,原始的 textarea 记为 originTextarea
- 创建一个隐藏的 textarea ,记为 hiddenTextarea
- 将 3 中获取的 css 样式应用到 hiddenTextarea
- 将原始的 originTextarea 的内容的值 value,复制给 hiddenTextarea 的 value,保证两个 textarea 内容完全一致
- 设置 hiddenTextarea 的隐藏样式,避免用户在页面上看到
- 根据各种属性计算出一个最终的高度 height,并赋值给 originTextarea,注意:
- 如果参数中 minRows 和 maxRows 有值,minRows 的默认值要为 1,至少展示一行文本
- 需要限制 height 在 minRows 和 maxRows 限制的高度区间内
- 这里有个问题:如何得到单行文本的高度,从而根据 minRows/maxRows 计算出 min-height / max-height
- 单行文本的高度 = hiddenTextarea 的 value 为空的时候的内容高度(scrollHeight) - 上下 padding 之和,因为 textarea 标签默认有一个 placeholder 会撑开内容的高度
- 设置 hiddenTextarea 的 height: 0,避免 textarea 标签的默认高度(clientHeight)影响 scollHeight,从而影响单行文本高度的计算
- 注意区分 textarea 标签的 height(元素高度) 和 scrollHeight(内容高度) 的关系和区别
其实单行文本的高度 = line-height,貌似直接获取 line-height 也行,但是我看 elementPlus 组件库中没这么用,难道是 line-height 会有问题?可以后面再研究一下,记个待办。
3.1.3 用到的方法
获取一个 dom 元素的所有的 css 属性使用一下两个方法 window.getComputedStyle + getPropertyValue
(1)getComputedStyle
window.getComputedStyle 方法返回一个对象,该对象在应用活动样式表并解析这些值可能包含的任何基本计算后报告元素的所有 CSS 属性的值。私有的 CSS 属性值可以通过对象提供的 API 或通过简单地使用 CSS 属性名称进行索引来访问。
返回的
style是一个实时的 CSSStyleDeclaration 对象,当元素的样式更改时,它会自动更新本身。
(2)getPropertyValue
CSSStyleDeclaration.getPropertyValue() 接口返回一个 DOMString ,其中包含请求的 CSS 属性的值。
其中 CSSStyleDeclaration 是window.getComputedStyle 方法的返回值
(3) Number.parseFloat
这里用到了一个字符串转数字的方法,很有用,以前总是知道这个方法,但是没有用过,现在发现背再多的八股文都不如应用一次,
Number.parseFloat()静态方法解析参数并返回浮点数。如果无法从参数中解析出一个数字,则返回 NaN。
输入字符串遇到第一个不能转成数字的就返回前面能转成数字的结果。
在这个例子中我们使用 getPropertyValue 获取某个属性值一般都是有单位的比如 10px,我们可以使用 Number。paerseFlost 方法把 10px 这种像素单位的字符串转成数字 10,很好用。
// 获取的值是带有 px 单位的字符串
const styles = window.getComputedStyle(originTextarea)
const paddingTop = styles.getPropertyValue('padding-top')
const paddingBottom = styles.getPropertyValue('padding-bottom')
// Number.parseFloat 解析并返回浮点数,当遇到不可转成数字的字符时,返回已经解析好的浮点数
const paddingSize = Number.parseFloat(paddingTop) + Number.parseFloat(paddingBottom)
(4)影响高度的 css 属性
我们要知道哪些 css 的属性能够影响文本的高度,一般来说分为布局属性(边距、边框等)、文本属性(字体、字号、行高)等。
但是有一些不常见的属性也需要考虑比如,text-rendering ,我也是看了 elementPlus 源码才知道的。
// 会影响 textarea 高度的 css 属性,为了克隆出“高度一样” 的隐藏的 textarea
const CssPropertyAffectHeight = [
'box-sizing',
'padding',
'padding-top',
'padding-bottom',
'padding-left',
'padding-right',
'line-height',
'letter-spacing',
'font-size',
'font-family',
'font-weight',
'text-indent', // 缩近
'text-rendering', // 定义浏览器渲染引擎如何渲染字体,不同属性针对不同字体又不同的效果
'text-transform', // 指定文本的大小写
'width',
'border-width',
]
3.1.4 源代码
下面代码中有详细的注释,可以克隆代码,运行项目,自己手动调试。
<template>
<div class="page-container">
<h1 class="title">textarea 标签,实现高度自适应的输入框</h1>
<div>基本思路:</div>
<ol>
<li>页面上写一个 textarea 标签</li>
<li>监听 textarea 的 value 值的变化</li>
<li>值变化的时候调用 resizeTextarea 方法</li>
<li>resizeTextarea 方法,支持传入可选参数最小行数 minRows 和最大行数 maxRows</li>
<li>初始化的时候调用一次 resizeTextarea 方法</li>
</ol>
<div>resizeTextarea 方法的基本步骤:</div>
<ol>
<li>原理是根据页面上的输入框,克隆一个一摸一样的 textarea 元素</li>
<li>一摸一样是指:所有的 css 属性都一样,包括 font-size、line-height、padding 等所有能影响 height 的css 属性,如下:</li>
<ol>
<li>box-sizing: content-box 和 border-box 需要有不同的计算方法</li>
<li>padding-top</li>
<li>padding-bottom</li>
<li>padding-left</li>
<li>padding-right</li>
<li>line-height</li>
<li>letter-spacing</li>
<li>font-size</li>
<li>font-family</li>
<li>text-indent: 缩近</li>
<li>text-rendering:定义浏览器渲染引擎如何渲染字体,不同属性针对不同字体又不同的效果</li>
<li>text-transform: 指定文本的大小写</li>
<li>width</li>
<li>border-width</li>
</ol>
<li>接受参数 “textarea”即页面上原始的输入框的 Dom 元素,用于克隆它的样式,使用 window.getComputedStyle(textareaDom) 获取原始 textarea 的各种属性,原始的 textarea 记为 originTextarea</li>
<li>创建一个隐藏的 textarea ,记为 hiddenTextarea</li>
<li>将 3 中获取的 css 样式应用到 hiddenTextarea</li>
<li>将原始的 originTextarea 的内容的值 value,复制给 hiddenTextarea 的 value,保证两个 textarea 内容完全一致</li>
<li>设置 hiddenTextarea 的隐藏样式,避免用户在页面上看到</li>
<li>根据各种属性计算出一个最终的高度 height,并赋值给 originTextarea,注意:</li>
<ol>
<li>如果参数中 minRows 和 maxRows 有值,minRows 的默认值要为 1,至少展示一行文本</li>
<li>需要限制 height 在 minRows 和 maxRows 限制的高度区间内</li>
<li>这里有个问题:如何得到单行文本的高度,从而根据 minRows/maxRows 计算出 min-height / max-height</li>
<li>单行文本的高度 = hiddenTextarea 的 value 为空的时候的内容高度(scrollHeight) - 上下 padding 之和,因为 textarea 标签默认有一个 placeholder 会撑开内容的高度</li>
<li>设置 hiddenTextarea 的 height: 0,避免 textarea 标签的默认高度(clientHeight)影响 scollHeight,从而影响单行文本高度的计算</li>
<li>注意区分 textarea 标签的 height(元素高度) 和 scrollHeight(内容高度) 的关系和区别</li>
<li class="highlight">其实单行文本的高度 = line-height,貌似直接获取 line-height 也行,但是我看 elementPlus 组件库中没这么用,合理怀疑用line-height会有问题??</li>
</ol>
</ol>
<div class="sample-box">
<div class="sample-label">1. 手动实现高度自适应的输入框,最大3行,最小5行</div>
<textarea ref="originTextareaDom" v-model="myName" class="input-ele" placeholder="高度自适应的输入框" />
</div>
</div>
</template>
<script lang="ts" setup>
import { watch, onMounted, ref } from 'vue'
// 输入框的绑定值
const myName = ref(`第一行第一行第一行第一
第二行`)
// 可选参数,输入框的最小行数 => 决定了输入框的最小高度
const minRows = ref(3)
// 可选参数,输入框的最大行数 => 决定了输入框的最大高度
const maxRows = ref(5)
// 原始输入框的 dom
const originTextareaDom = ref()
// 监控输入框的内容变化,从而改变高度,实现自适应
watch(
() => myName.value,
() => {
resizeTextarea({
originTextarea: originTextareaDom.value,
minRows: minRows.value,
maxRows: maxRows.value,
})
},
)
// 隐藏的 textarea:不需要是 vue 响应式的,因为不会在页面中展示
let hiddenTextarea!: any
// 计算输入框的高度
const resizeTextarea = ({ originTextarea, minRows = 1, maxRows }: { originTextarea: HTMLTextAreaElement; minRows?: number; maxRows?: number }) => {
if (!originTextarea) {
return
}
// 如果没有隐藏的输入框,则创建,并添加到页面 dom 树中
if (!hiddenTextarea) {
hiddenTextarea = document.createElement('textarea')
// 新创建的元素,如果不挂载到页面上,有些布局属性(scrollHeight)无法正确获取
document.body.append(hiddenTextarea)
}
// 获取原始输入框的各种属性
const { borderSize, lineHeight, paddingSize, boxSizing, contentStyle } = getOriginTextareaStyle(originTextarea)
// 隐藏的输入框的样式,不要让用户看到, 要设置 height:0,否则在没有滚动的时候 scrollHeight === height
const hiddenTextareaStyle = `
position: fixed;
height: 0 !important;
top: -9999px !important;
left: -9999px !important;
min-height: 0;
`
// 将原始输入框的样式,复制到隐藏的输入框
hiddenTextarea.style = `${hiddenTextareaStyle}${contentStyle}`
// 设置文字内容,注意这里的 .value 是原生 textarea 的属性,要和 vue 中响应式变量的取值区分一下
hiddenTextarea.value = originTextarea.value || originTextarea.placeholder // 如果没有内容,展示提示文案
// 最终需要更新的输入框的高度由文本内容决定
let height = hiddenTextarea.scrollHeight
// 处理不同盒子模型的高度
if (boxSizing === 'border-box') {
// 原始的输入框在 border-box 的时候,要设置的 height 包含 border
height = height + borderSize
} else if (boxSizing === 'content-box') {
// 原始的输入框在 content-box 的时候,要设置的 height 不包含 padding,所以要减去 paddingSize
height = height - paddingSize
}
// 获取单行文本的高度, 将输入框内容设置为空,默认展示一行
hiddenTextarea.value = ''
// 元素的 scollheight 包含 padding,所以减去 paddingSize 才等于 line-height
const singleLineHeight = hiddenTextarea.scrollHeight - paddingSize
// 按理说直接获取 line-height 属性也行吧,但是我看 elementPlus 组件库中没这么用
// const singleLineHeight = lineHeight
// 根据最少、最多展示的行数处理最大最小高度
if (minRows) {
let minHeight = singleLineHeight * minRows
if (boxSizing === 'border-box') {
minHeight = minHeight + paddingSize + borderSize
}
height = Math.max(minHeight, height)
}
if (maxRows) {
let maxHeight = singleLineHeight * maxRows
if (boxSizing === 'border-box') {
maxHeight = maxHeight + paddingSize + borderSize
}
height = Math.min(maxHeight, height)
}
// 把最新的 height 应用到原始的输入框
originTextareaDom.value.style.height = `${height}px`
// 在页面的 dom 中移除隐藏的输入框
hiddenTextarea.remove()
hiddenTextarea = null
}
// 会影响 textarea 高度的 css 属性,为了克隆出“高度一样” 的隐藏的 textarea
const CssPropertyAffectHeight = [
'box-sizing',
'padding',
'padding-top',
'padding-bottom',
'padding-left',
'padding-right',
'line-height',
'letter-spacing',
'font-size',
'font-family',
'font-weight',
'text-indent', // 缩近
'text-rendering', // 定义浏览器渲染引擎如何渲染字体,不同属性针对不同字体又不同的效果
'text-transform', // 指定文本的大小写
'width',
'border-width',
]
// 获取原始输入框的各种属性
const getOriginTextareaStyle = (originTextarea: HTMLElement) => {
const styles = window.getComputedStyle(originTextarea)
// 计算上下边距的和
const paddingTop = styles.getPropertyValue('padding-top') // 获取的值是带有 px 单位的字符串
const paddingBottom = styles.getPropertyValue('padding-bottom')
// Number.parseFloat 解析并返回浮点数,当遇到不可转成数字的字符时,返回已经解析好的浮点数
const paddingSize = Number.parseFloat(paddingTop) + Number.parseFloat(paddingBottom)
// 计算上下边框的和
const borderTop = styles.getPropertyValue('border-top') // 获取的值是带有 px 单位的字符串
const borderBottom = styles.getPropertyValue('border-bottom')
const borderSize = Number.parseFloat(borderTop) + Number.parseFloat(borderBottom)
const boxSizing = styles.getPropertyValue('box-sizing')
const lineHeight = Number.parseFloat(styles.getPropertyValue('line-height'))
// 根据会影响高度的属性,拼接成 style 字符串
const contentStyle = CssPropertyAffectHeight.map(item => {
return `${item}:${styles.getPropertyValue(item)};`
}).join('')
return {
lineHeight,
borderSize,
boxSizing,
paddingSize,
contentStyle,
}
}
onMounted(() => {
resizeTextarea({
originTextarea: originTextareaDom.value,
minRows: minRows.value,
maxRows: maxRows.value,
})
})
</script>
<style lang="scss" scoped>
.page-container {
padding: 24px;
ol {
list-style: decimal;
padding-left: 14px;
}
.title {
margin: 10px 0;
}
.highlight {
color: red;
}
.sample-box {
margin: 10px 0 100px;
padding: 10px 20px;
width: 100%;
border-radius: 6px;
background: #eee;
.sample-label {
font-size: 16px;
font-weight: bold;
margin: 10px 0 5px;
}
.input-ele {
font-size: 14px;
line-height: 24px;
padding: 10px;
border: 1px solid blue;
border-radius: 4px;
outline: none; // 输入框的轮廓特别明显
background: #fff; // 一般都需要设置背景色,要不然默认为白色
}
}
}
</style>
3.2 在文字后面增加一个按钮
基本需求内容:
- 在输入框中的文字后面增加一个按钮
- 文字增加(包括输入空格),按钮位置跟向后移动
- 文字减少,按钮位置向前移动
- 总之,保证换行、文字变化按钮位置随之变化
- 在输入中文还没输入完的时候,可以不展示这个按钮,因为中文正在输入中不好获取光标的位置
基本效果如下:

不瞒你说,这个功能是我前一阵子在文心一言的网站上看到的,在输入框后面有一个润色按钮,点击按钮将文本输入框中的内容进行润色。
3.2.1 基本思路
参考上一节使用 textarea 实现高度自适应的输入框的思路,概括来说就是克隆一个隐藏的 dom:
- 创建一个隐藏的不可编辑的 dom,记为 hiddenEle,注意这里创建的是div 或者是 p标签等,而不是 textarea,因为 textarea 里面不能增加额外的按钮,只能是纯文本
- 复制页面上的输入框的样式和文本内容到隐藏的输入框
- 将隐藏的 dom 使用绝对定位,放在页面的输入框的底层
- 这样视觉上看起来按钮就是在前面的输入框后面了,注意调整按钮的层级,使按钮能够点击
- 我们需要监听输入框的滚动事件,当文本内容很长的时候原始的输入框和隐藏的输入框同步滚动才能保证位置相同
3.2.2 影响位置的 css 属性
跟上一节的属性列表相比,增加了很多属性,因为我们不只要保证高度相等,我们还要保证宽度相等,所以相对而言增加了 overflow 、word-wrap、overflow-wrap 等
// 会影响 textarea 宽高的 css 属性,为了克隆出“宽高一样” 的隐藏的 dom
const CssPropertyAffectHeight = [
'height',
'min-height',
'max-height',
'width',
'min-width',
'max-width',
'overflow',
'box-sizing',
'padding',
'padding-top',
'padding-bottom',
'padding-left',
'padding-right',
'line-height',
'letter-spacing',
'font-size',
'font-family',
'white-space', // 空白字符和换行的展示
'overflow-wrap', //应用于行内元素,用来设置浏览器是否应该在一个本来不能断开的字符串中插入换行符,以防止文本溢出
'word-wrap',
'font-weight',
'text-indent', // 缩近
'text-rendering', // 定义浏览器渲染引擎如何渲染字体,不同属性针对不同字体又不同的效果
'text-transform', // 指定文本的大小写
'border-width',
]
不瞒你说,我第一次知道 overflow-wrap 这个属性,假如我们在输入框中输入了很长很长的单词,或者数字的时候就需要用到这个属性了
CSS 属性
overflow-wrap应用于行级元素,用来设置浏览器是否应该在一个本来不能断开的字符串中插入换行符,以防止文本溢出其行向盒。备注: 与 word-break 相比,
overflow-wrap仅在无法将整个单词放在自己的行而不会溢出的情况下才会产生换行。这个属性原本属于微软扩展的一个非标准、无前缀的属性,叫做
word-wrap,后来在大多数浏览器中以相同的名称实现。目前它已被更名为 overflow-wrap,word-wrap相当于其别称。
3.2.3 源代码
<template>
<div class="page-container">
<h1 class="title">textarea 标签,在文字后面增加一个按钮</h1>
<div>要求:</div>
<ol>
<li>在输入框中的文字后面增加一个按钮</li>
<li>文字增加(包括输入空格),按钮位置跟向后移动</li>
<li>文字减少,按钮位置向前移动</li>
<li>总之,保证换行、文字变化按钮位置随之变化</li>
<li>在输入中文还没输入完的时候,可以不展示这个按钮,因为中文正在输入中不好获取光标的位置</li>
</ol>
<div>
参考上一节
<a href="/textInput3">标签,实现高度自适应的输入框</a>
的思路,概括来说就是克隆一个隐藏的 dom:
</div>
<ol>
<li>创建一个隐藏的不可编辑的 dom,记为 hiddenEle,注意这里创建的是div 或者是 p标签等,而不是 textarea,因为 textarea 里面不能增加额外的按钮,只能是纯文本</li>
<li>复制页面上的输入框的样式和文本内容到隐藏的输入框</li>
<li>将隐藏的 dom 使用绝对定位,放在页面的输入框的底层</li>
<li>这样视觉上看起来按钮就是在前面的输入框后面了,注意调整按钮的层级,使按钮能够点击</li>
</ol>
<div class="sample-box">
<div class="outer-box">
<!-- 隐藏的dom -->
<p ref="hiddenDom" class="hidden-box"></p>
<!-- 正常输入的输入框 -->
<textarea
ref="originTextareaDom"
v-model="myName"
class="input-ele"
placeholder="输入框中文字后面增加一个按钮"
@scroll="onScroll"
@compositionstart="onCompositionStart"
@compositionend="onCompositionEnd"
/>
</div>
</div>
<!-- 把按钮写在模版中,然后移动到隐藏的 dom 文本后面,这样的好处是,如果按钮很复杂,使用 js 的 document.createElement 创建会很麻烦,而且这样写 css 也很好写,
需要注意的是在开始的时候要隐藏这个按钮 -->
<span ref="myBtnELe" class="my-btn hide" @click="onBtnClick">+</span>
</div>
</template>
<script lang="ts" setup>
import { watch, onMounted, ref } from 'vue'
// 输入框的绑定值
const myName = ref(``)
// 原始输入框的 dom
const originTextareaDom = ref()
// 隐藏的 dom
const hiddenDom = ref()
// 按钮的 dom
const myBtnELe = ref()
// 监控输入框的内容变化,从而改变按钮位置
watch(
() => myName.value,
() => {
resizeBtnPosition({
originTextarea: originTextareaDom.value,
})
},
)
// 监听输入框的滚动事件,保证两个元素同步滚动
const onScroll = () => {
resizeBtnPosition({
originTextarea: originTextareaDom.value,
})
}
// 按钮的点击事件
const onBtnClick = () => {
alert('按钮点击')
}
// 输入中文,隐藏按钮
const onCompositionStart = () => {
myBtnELe.value.classList.add('hide')
}
// 中文输入结束,展示按钮
const onCompositionEnd = () => {
myBtnELe.value.classList.remove('hide')
}
// 计算输入框中按钮的位置
const resizeBtnPosition = ({ originTextarea }: { originTextarea: HTMLTextAreaElement }) => {
if (!originTextarea || !hiddenDom.value) {
return
}
// 获取原始输入框的各种属性
const { contentStyle } = getOriginTextareaStyle(originTextarea)
// 将原始输入框的样式,复制到隐藏的 DOM
hiddenDom.value.style = `${contentStyle}`
// 设置文字内容,注意这里的 originTextarea.value 是原生 textarea 的属性,要和 vue 中响应式变量的取值区分一下
// hiddenDom.value 是 vue 响应式取值的办法
hiddenDom.value.innerHTML = originTextarea.value || ''
// 如果有内容,hiddenDom 后面增加一个按钮
if (originTextarea.value) {
myBtnELe.value.classList.remove('hide')
hiddenDom.value.append(myBtnELe.value)
}
// 设置向上滚动相同的距离
const scrollTop = originTextarea.scrollTop
hiddenDom.value.scrollTop = scrollTop
}
// 会影响 textarea 宽高的 css 属性,为了克隆出“宽高一样” 的隐藏的 dom
const CssPropertyAffectHeight = [
'height',
'min-height',
'max-height',
'width',
'min-width',
'max-width',
'overflow',
'box-sizing',
'padding',
'padding-top',
'padding-bottom',
'padding-left',
'padding-right',
'line-height',
'letter-spacing',
'font-size',
'font-family',
'white-space', // 空白字符和换行的展示
'overflow-wrap', //应用于行内元素,用来设置浏览器是否应该在一个本来不能断开的字符串中插入换行符,以防止文本溢出
'word-wrap',
'font-weight',
'text-indent', // 缩近
'text-rendering', // 定义浏览器渲染引擎如何渲染字体,不同属性针对不同字体又不同的效果
'text-transform', // 指定文本的大小写
'border-width',
]
// 获取原始输入框的各种属性
const getOriginTextareaStyle = (originTextarea: HTMLElement) => {
const styles = window.getComputedStyle(originTextarea)
// 根据会影响高度的属性,拼接成 style 字符串
const contentStyle = CssPropertyAffectHeight.map(item => {
return `${item}:${styles.getPropertyValue(item)};`
}).join('')
return {
contentStyle,
}
}
onMounted(() => {
resizeBtnPosition({
originTextarea: originTextareaDom.value,
})
})
</script>
<style lang="scss" scoped>
.page-container {
padding: 24px;
ol {
list-style: decimal;
padding-left: 14px;
}
.title {
margin: 10px 0;
}
.highlight {
color: red;
}
.sample-box {
margin: 10px 0 100px;
padding: 10px 20px;
width: 100%;
border-radius: 6px;
background: #eee;
.sample-label {
font-size: 16px;
font-weight: bold;
margin: 10px 0 5px;
}
.outer-box {
display: flex;
align-items: center;
justify-content: center;
width: 500px;
height: 64px;
position: relative;
.input-ele {
height: 64px;
width: 500px;
padding: 10px;
font-size: 14px;
line-height: 24px;
border: 1px solid blue;
border-radius: 4px;
outline: none; // 输入框的轮廓特别明显
background: #fff; // 一般都需要设置背景色,要不然默认为白色
z-index: 99;
}
:deep(.hidden-box) {
position: absolute; // 绝对定位,让它位于输入框的下面
top: 0;
left: 0;
height: 64px;
width: 500px;
.my-btn {
position: relative;
z-index: 100;
}
}
}
}
.my-btn {
display: inline-flex;
align-items: center;
justify-content: center;
width: 16px;
height: 16px;
color: #fff;
border-radius: 50%;
background: #000;
cursor: pointer;
&.hide {
visibility: hidden; // 用于隐藏这个按钮,用于初始化的时候,或者正在输入中文的时候
}
&:hover {
background: red;
}
}
}
</style>
四、contenteditable 属性
4.1 基础应用
4.1.1 注意事项
可以给标签加上 contenteditable=true 实现可编辑效果,但是使用 contenteditable=true 的元素作为输入框的时候,需要处理一下几点
- 需要自定义 placeholder 的内容,一般用伪元素 + 绝对定位实现
- 在文字内容变更之后,需要手动隐藏/展示 placeholder
- 纯文本输入框:需要处理粘贴事件,保证不会把格式粘贴进去(支持输入非纯文本的情况,富文本编辑器就是用它实现的,可以各种样式,字体颜色格式等)
- 纯文本输入框:要自己处理输入的值,监控 input 事件,还要避免重复赋值,避免进入死循环,注意初始化的值,不能使用 v-html
- 如果你使用 v-html,按回车换行有问题
- shift + enter 换行:不用手动实现
- 实现最小、最大行数,只需要根据行高(line-height)给这dom这是最小、最大高度即可。
4.1.2 避免粘贴格式
在纯文本输入框的时候我们需要处理粘贴事件,让输入框粘贴纯文本,但是如果是富文本我们可能就需要保留格式和样式了,那就是另一种处理方式了。
// 避免粘贴格式
const onPaste = e => {
// 阻止默认的粘贴事件
e.preventDefault()
const clipboardData = e.clipboardData || window.clipboardData
const pastedData = clipboardData.getData('Text')
document.execCommand('insertText', false, pastedData)
}
4.1.3 源代码
<template>
<div class="page-container">
<h1 class="title">contenteditable 属性</h1>
<div>可以给标签加上 contenteditable=true 实现可编辑效果,但是使用 contenteditable=true 的元素作为输入框的时候,需要处理一下几点</div>
<ol>
<li>需要自定义 placeholder 的内容,一般用伪元素 + 绝对定位实现</li>
<li>在文字内容变更之后,需要手动隐藏/展示 placeholder</li>
<li>纯文本输入框:需要处理粘贴事件,保证不会把格式粘贴进去(支持输入非纯文本的情况,富文本编辑器就是用它实现的,可以各种样式,字体颜色格式等)</li>
<li>纯文本输入框:要自己处理输入的值,监控 input 事件,还要避免重复赋值,避免进入死循环,注意初始化的值,不能使用 v-html</li>
<li>如果你使用v-html,按回车换行有问题</li>
<li>shift + enter 换行:不用手动实现</li>
<li>实现最小、最大行数,只需要根据行高(line-height)给这dom这是最小、最大高度即可。</li>
</ol>
<div class="sample-box">
<div class="sample-label highlight">有问题的版本:v-html</div>
<div>响应式变量 myName 的值:</div>
<div class="my-name-value highlight">{{ myName }}</div>
<div>下面这种写法,每次输入一个文字,光标都会移动到前面</div>
<div class="input-ele" :class="{ 'is-empty': !myName.length }" contenteditable @input="onInput" v-html="myName"></div>
<div>下面这种写法,按回车,光标会移动到前面,而不是换行</div>
<div class="input-ele" :class="{ 'is-empty': !myName.length }" contenteditable @input="onInput">{{ myName }}</div>
<div>如果我们不需要响应式的值 myName ,也可以每次想要获取输入框内容的时候使用 dom.innerText 获取,但是这样很麻烦</div>
</div>
<div class="sample-box" style="margin-bottom: 100px">
<div class="sample-label highlight">没有问题的版本</div>
<div class="sample-label">响应式变量 myName 的值:</div>
<div class="my-name-value highlight">{{ myName }}</div>
<div class="sample-label">输入框</div>
<div ref="inputEle" class="input-ele" :class="{ 'is-empty': !myName.length }" contenteditable @input="onInput" @paste="onPaste"></div>
</div>
</div>
</template>
<script lang="ts" setup>
import { onMounted, ref } from 'vue'
// 输入框的绑定值
const myName = ref(``)
// 输入框的 dom
const inputEle = ref()
// 监听输入框的输入事件,更新 myName
const onInput = (e: any) => {
const text = e.target.innerText
myName.value = text.trim()
}
// 避免粘贴格式
const onPaste = e => {
// 阻止默认的粘贴事件
e.preventDefault()
const clipboardData = e.clipboardData || window.clipboardData
const pastedData = clipboardData.getData('Text')
document.execCommand('insertText', false, pastedData)
}
onMounted(() => {
// 初始化的时候,需要根据默认值更新 dom 上面的值
if (inputEle.value) {
inputEle.value.innerHTML = myName.value
}
})
</script>
<style lang="scss" scoped>
.page-container {
padding: 24px;
ol {
list-style: decimal;
padding-left: 14px;
}
.title {
margin: 10px 0;
}
.highlight {
color: red !important;
}
.sample-box {
margin: 10px 0;
padding: 10px 20px;
width: 100%;
line-height: 24px;
border-radius: 6px;
background: #eee;
.sample-label {
font-size: 12px;
color: #333;
margin: 10px 0 5px;
}
.my-name-value {
white-space: pre-wrap;
}
.input-ele {
// height: 64px;
min-height: 64px; // 根据行高计算,最少展示 2 行
max-height: 88px; // 根据行高计算,最多展示 3 行
width: 500px;
padding: 10px;
overflow: auto;
font-size: 14px;
line-height: 24px;
border: 1px solid blue;
border-radius: 4px;
background: #fff;
position: relative;
outline: none; // 还是要处理 outline
&::before {
display: none;
content: '请输入内容';
font-size: 14px;
line-height: 24px;
color: #666;
position: absolute;
top: 10px;
left: 10px;
}
&.is-empty {
&::before {
display: inline-block;
}
}
}
}
}
</style>
4.2 使用富文本编辑器
富文本编辑器的实现原理其实就是 4.1 小节说的 contenteditable 属性
4.2.1 常见的富文本编辑器
有很多现成的可以用的富文本编辑器,可以供我们使用,常见的好用的富文本编辑器如下(按照受欢迎的程度排名):
- Quill + vue-quill-editor 轻量级、模块化设计
- TinyMCE + @tinymce/tinymce-vue 本片文章使用这个 功能丰富,插件众多,类似于在线版的Word
- CKEditor 5 + @ckeditor/ckeditor5-vue 开源免费,支持商业用途,现代UI设计
- Tiptap + @tiptap/ 系列 基于ProseMirror,现代化,支持多人在线实时编辑
- Froala Editor 插件丰富,UI友好,功能强大
- Summernote + vue-summernote 轻量级,易于上手,支持快捷键操作
- wangEditor 轻量、简洁、界面美观、易用
- Jodit 使用纯TypeScript编写,开源且支持中文
- Simditor 界面简约,功能实用
- UEditor 由百度出品,轻量、可定制
(没错这是 ai 给的排名)
我以前用过 Quill 和 Tiptap ,但是我看到又说 TinyMCE 支持视频?看起来很神奇的样子,打算研究一下。
如果你使用 vue 最好是使用每个编辑器对应的 vue 版本,用起来更方便
TinyMCE 这个编辑需要一个 apikey 才能用,否则是只读模式,可以自己去官网登录后免费获取自己的key
4.2.2 源代码
<template>
<div class="page-container">
<h1 class="title">使用富文本编辑器</h1>
<div>有很多现成的可以用的富文本编辑器,可以供我们使用,常见的好用的富文本编辑器如下(按照受欢迎的程度排名):</div>
<ol>
<li>Quill + vue-quill-editor 轻量级、模块化设计</li>
<li class="highlight">TinyMCE + @tinymce/tinymce-vue 本片文章使用这个 功能丰富,插件众多,类似于在线版的Word</li>
<li>CKEditor 5 + @ckeditor/ckeditor5-vue 开源免费,支持商业用途,现代UI设计</li>
<li>Tiptap + @tiptap/ 系列 基于ProseMirror,现代化,支持多人在线实时编辑</li>
<li>Froala Editor 插件丰富,UI友好,功能强大</li>
<li>Summernote + vue-summernote 轻量级,易于上手,支持快捷键操作</li>
<li>wangEditor 轻量、简洁、界面美观、易用</li>
<li>Jodit 使用纯TypeScript编写,开源且支持中文</li>
<li>Simditor 界面简约,功能实用</li>
<li>UEditor 由百度出品,轻量、可定制</li>
</ol>
<div>如果你使用 vue 最好是使用每个编辑器对应的 vue 版本,用起来更方便</div>
<div>TinyMCE 这个编辑需要一个 apikey 才能用,否则是只读模式,可以自己去官网登录后免费获取自己的key,替换下面的 no-api-key</div>
<Editor
api-key="no-api-key"
:init="{
plugins: 'lists link image table code help wordcount',
}"
/>
</div>
</template>
<script lang="ts" setup>
import { onMounted, ref } from 'vue'
import Editor from '@tinymce/tinymce-vue'
onMounted(() => {})
</script>
<style lang="scss" scoped>
.page-container {
padding: 24px;
ol {
list-style: decimal;
padding-left: 14px;
}
.title {
margin: 10px 0;
}
.highlight {
color: red !important;
}
.my-textarea-box {
border: 1px solid;
height: 500px;
width: 100%;
}
}
</style>
4.3 更丰富的输入框内容
4.3.1 包含按钮下拉框的输入框
contenteditable 属性,实现更丰富的内容
输入框中可能会有除了文本之外的,更加丰富的内容,比如按钮和下拉框等,可以使用 contenteditable 实现
效果如下图

- contenteditable 也是是富文本编辑器的实现原理
- 当需求很小众的时候我们需要自己定义 contenteditable 的元素的内容
- 当需求是文本编辑器的时候,我们可以使用现成的富文本编辑器的库,比如quill、tinyMCE等
- 给一个 div 设置为 contenteditable= true 之后,div 中依旧可以插入任何 dom,知识内容可以编辑
4.3.2 使用 tippy.js
在本例中给输入框一段文案设置一个 tooltip,这段文案位置会变,因为可能在前面增加或者删除文字,但是 tooltip 的位置要自适应,可以使用 tippy.js 方便快捷。
tippy.js 同样可以应用在富文本中,很好用,很强大。
Tippy.js 是一个轻量级且功能强大的工具提示库,适用于创建工具提示、弹出窗口、下拉菜单和 Web 菜单。它由 Popper 提供支持,能够在目标元素旁边弹出并漂浮
// 给文案中的【让我来教教你】设置一个 tooltip,这段文案位置会变,因为可能在前面增加或者删除文字,但是 tooltip 的位置要自适应,可以使用 tippy.js 方便快捷
// 还可以自己监听【让我来教教你】这个元素的位置变化,自己计算,但是要处理各种边缘情况,很麻烦
const teachYouEle = document.getElementById('teachYouEle') as HTMLElement
tippy(teachYouEle, {
duration: 0,
getReferenceClientRect: null,
content: '就不告诉你',
interactive: true,
placement: 'top',
})
4.3.3 注意事项
经常写 vue 模版文件,都已经忘记了原生的 html + js 的代码咋写了,这里面有两个需要注意的点。
(1)innerHTML 应用后的事件传参
在函数的括号里面直接写 event 即可获取事件的整个 event 参数
// 注意 onclick 事件传值的方法,如果要获取 event, 就直接写 event 就行了,长时间不写原生h5代码都忘了..
const htmlText = `<span>如何学好前端点击</span><button class="see-btn" contenteditable="false" onclick="onBtnClick('第一个参数', event, '我是按钮')">按钮</button>查看,一点都不难,<span class="teach-you" id="teachYouEle" contenteditable="false">让我来教教你</span>,选择你的方法`
(2)innerHTML 应用后方法挂载
在 vue 文件中 innerHTML 的字符串中的方法,如果不挂载到全局是无法访问到的。
/ 不要用箭头函数,箭头函数没有 arguments,但是可以使用剩余参数 ...args
const onBtnClick = function () {
console.log(arguments)
alert('点击按钮')
}
onMounted(() => {
// 初始化输入框的内容
initInputEle()
// 注意这个,我们使用 innerHTML 在页面上增加标签,对应的 onclick 等方法应该挂载到全局,要不然会获取不到
window.onBtnClick = onBtnClick
})
4.3.4 源代码
<template>
<div class="page-container">
<h1 class="title">contenteditable 属性,实现更丰富的内容</h1>
<div>输入框中可能会有除了文本之外的,更加丰富的内容,比如按钮和下拉框等,可以使用 contenteditable 实现</div>
<ol>
<li>contenteditable 也是是富文本编辑器的实现原理</li>
<li>当需求很小众的时候我们需要自己定义 contenteditable 的元素的内容</li>
<li>当需求是文本编辑器的时候,我们可以使用现成的富文本编辑器的库,比如quill、tinyMCE等</li>
<li>给一个 div 设置为 contenteditable= true 之后,div 中依旧可以插入任何 dom,知识内容可以编辑</li>
</ol>
<div class="sample-box" style="margin-bottom: 100px">
<div class="sample-label highlight">输入框中除了可编辑的文本之外,还包含有按钮,针对某个文字的tooltip 提示,针对一个按钮的下拉选项</div>
<div class="sample-label">响应式变量 myName 的值:</div>
<div class="my-name-value highlight">{{ myName }}</div>
<div ref="inputEle" class="input-ele" :class="{ 'is-empty': !myName.length }" contenteditable @input="onInput" @paste="onPaste"></div>
</div>
<a-popover v-model:visible="popoverVisible" title="选择你喜欢的学习方法" trigger="click">
<template #content>
<div class="select-method-content-box">
<div class="content-item" @click="changeSelectVal('马上就去看书')">马上就去看书</div>
<div class="content-item" @click="changeSelectVal('明天在学习')">明天在学习</div>
</div>
</template>
<a-button ref="popoverEle" class="select-btn" contenteditable="false" type="primary" size="small">{{ selectVal }}</a-button>
</a-popover>
</div>
</template>
<script lang="ts" setup>
import { nextTick, onMounted, ref } from 'vue'
import { Popover as aPopover, Button as AButton } from 'ant-design-vue'
import tippy from 'tippy.js'
import 'tippy.js/dist/tippy.css'
import 'tippy.js/themes/light.css'
// 输入框的绑定值
const myName = ref(``)
// 输入框的 dom
const inputEle = ref()
// 弹出框的 reference 的 dom
const popoverEle = ref()
// 选择的内容
const selectVal = ref('选择学习方式')
// 是否展示选择的 popover
const popoverVisible = ref(false)
// 选择框
const changeSelectVal = (val: string) => {
selectVal.value = val
popoverVisible.value = false
// 需要在nexttick的时候更新myName
nextTick(() => {
myName.value = inputEle.value.innerText.trim()
})
}
// 监听输入框的输入事件,更新 myName
const onInput = (e: any) => {
const text = e.target.innerText
myName.value = text.trim()
}
// 避免粘贴格式
const onPaste = e => {
// 阻止默认的粘贴事件
e.preventDefault()
const clipboardData = e.clipboardData || window.clipboardData
const pastedData = clipboardData.getData('Text')
document.execCommand('insertText', false, pastedData)
}
// 初始化输入框的内容,使其包含按钮和下拉选项等内容
const initInputEle = () => {
// 对于简单的 html 可以直接拼接,
// 注意 onclick 事件传值的方法,如果要获取 event, 就直接写 event 就行了,长时间不写原生h5代码都忘了..
const htmlText = `<span>如何学好前端点击</span><button class="see-btn" contenteditable="false" onclick="onBtnClick('第一个参数', event, '我是按钮')">按钮</button>查看,一点都不难,<span class="teach-you" id="teachYouEle" contenteditable="false">让我来教教你</span>,选择你的方法`
inputEle.value.innerHTML = htmlText
// 对于复杂的组件,可以使用 append 等方法,但是 append 只能放在最后面
inputEle.value.append(popoverEle.value.$el)
// 给文案中的【让我来教教你】设置一个 tooltip,这段文案位置会变,因为可能在前面增加或者删除文字,但是 tooltip 的位置要自适应,可以使用 tippy.js 方便快捷
// 还可以自己监听【让我来教教你】这个元素的位置变化,自己计算,但是要处理各种边缘情况,很麻烦
const teachYouEle = document.getElementById('teachYouEle') as HTMLElement
tippy(teachYouEle, {
duration: 0,
getReferenceClientRect: null,
content: '就不告诉你',
interactive: true,
placement: 'top',
})
// 把输入框的内容更新到响应式变量
myName.value = inputEle.value.innerText
}
// 不要用箭头函数,箭头函数没有 arguments,但是可以使用剩余参数 ...args
const onBtnClick = function () {
console.log(arguments)
alert('点击按钮')
}
onMounted(() => {
// 初始化输入框的内容
initInputEle()
// 注意这个,我们使用 innerHTML 在页面上增加标签,对应的 onclick 等方法应该挂载到全局,要不然会获取不到
window.onBtnClick = onBtnClick
})
</script>
<style lang="scss" scoped>
.page-container {
padding: 24px;
ol {
list-style: decimal;
padding-left: 14px;
}
.title {
margin: 10px 0;
}
.highlight {
color: red !important;
}
.sample-box {
margin: 10px 0;
padding: 10px 20px;
width: 100%;
line-height: 24px;
border-radius: 6px;
background: #eee;
.sample-label {
font-size: 12px;
color: #333;
margin: 10px 0 5px;
}
.my-name-value {
white-space: pre-wrap;
}
:deep(.input-ele) {
// height: 64px;
min-height: 88px; // 根据行高计算,最少展示 2 行
max-height: 200; // 根据行高计算,最多展示 3 行
width: 500px;
padding: 10px;
overflow: auto;
font-size: 14px;
line-height: 24px;
border: 1px solid blue;
border-radius: 4px;
background: #fff;
position: relative;
outline: none; // 还是要处理 outline
&::before {
display: none;
content: '请输入内容';
font-size: 14px;
line-height: 24px;
color: #666;
position: absolute;
top: 10px;
left: 10px;
}
&.is-empty {
&::before {
display: inline-block;
}
}
.see-btn {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0 4px;
margin: 0 4px;
background: #666;
border-radius: 4px;
cursor: pointer;
color: #fff;
}
.teach-you {
color: blue;
cursor: pointer;
}
.select-btn {
margin: 0 4px;
border-radius: 4px;
}
}
}
}
</style>
<style lang="scss">
.select-method-content-box {
display: flex;
flex-direction: column;
.content-item {
display: flex;
align-items: center;
justify-content: flex-start;
height: 32px;
padding: 0 4px;
border-radius: 4px;
cursor: pointer;
&:hover {
background: #eee;
}
}
}
</style>
五、总结
本篇文章总结了前端开发过程中所有的文本输入的场景,由简单到复杂,文章内容很长,感谢您的耐心,关于代码的部分,我建议大家去仓库克隆源代码,几乎每一行都有注释。文章源代码在 git 上可直接下载
learn-vite: 搭建简单的vite+ts+vue框架
https://gitee.com/yangjihong2113/learn-vite感谢大家阅读,欢迎关注,我们一起学习进步,我会持续更新前端开发相关的系统化的教程,新手建议关注我的系统化专栏《前端工程化系统教程》所有教程都包含源码,内容持续更新中希望对你有帮助。

