Editor 极简编辑器
可得到html
标签内容,且能插入自定义元素,含有提及和字数统计功能的极简编辑器。
该组件基于 Tiptap 及 ElTooltip 封装而来。
基础用法
使用v-model
双向绑定html
值。使用enter
事件来处理什么(比如发送消息之类的)
按住ctrl+enter
或shift+enter
回车换行。
<template>
<el-editor
v-model="value"
style="width: 320px"
placeholder="Please input"
:editor-options="editorOptions"
@enter="handleEnter"
/>
<pre>{{ value }}</pre>
</template>
<script setup>
import { ref } from 'vue'
import { ElMessage } from 'element-plus'
const value = ref()
const handleEnter = (values) => {
console.log(values)
ElMessage('回车做点什么')
}
const editorOptions = {
onFocus: () => {
console.log('onFocus')
},
onBlur: () => {
console.log('onBlur')
},
}
</script>
禁用
使用disabled
属性控制编辑器是否可以编辑
<template>
<el-switch
v-model="disabled"
size="large"
active-text="禁用"
inactive-text="启用"
/>
<el-editor
v-model="value"
style="width: 320px"
:disabled="disabled"
placeholder="Please input"
disable-enter-emit
/>
</template>
<script setup>
import { ref } from 'vue'
const value = ref('hi 徐尹啊 <br /> 今天又是苦逼的一天')
const disabled = ref(true)
</script>
resize 缩放
使用resize
属性来缩放高度,如需缩放宽度,重写 css resize: both
即可
<template>
<el-editor
v-model="value"
class="editor"
resize
placeholder="Please input"
@enter="handleEnter"
/>
<pre>{{ value }}</pre>
</template>
<script setup>
import { ref } from 'vue'
import { ElMessage } from 'element-plus'
const value = ref()
const handleEnter = (values) => {
console.log(values)
ElMessage('回车做点什么')
}
</script>
<style lang="scss" scoped>
.editor {
max-width: 450px;
min-height: 80px;
max-height: 250px;
}
</style>
宽高
使用style
样式来直接控制编辑器的宽高
<template>
<el-editor
v-model="value"
style="width: 450px; height: 150px; overflow: auto"
placeholder="Please input"
@enter="handleEnter"
/>
<pre>{{ value }}</pre>
</template>
<script setup>
import { ref } from 'vue'
import { ElMessage } from 'element-plus'
const value = ref()
const handleEnter = (values) => {
console.log(values)
ElMessage('回车做点什么')
}
</script>
提及
使用prefix
属性来修改唤醒词。使用options
属性来注入提及列表的值
options
也可以是返回Promise实例
的函数。
<template>
<el-editor
v-model="value"
style="width: 320px; margin-bottom: 24px"
placeholder="Please input"
:options="options"
/>
<el-editor
v-model="value2"
style="width: 320px"
prefix="/"
placeholder="输入 / 试试"
:options="getList"
/>
</template>
<script setup>
import { ref } from 'vue'
const value = ref(`@`)
const options = ref(
Array.from({ length: 40 }).map((_, index) => {
return {
label: `Summer_${index}`,
id: `xzw_${index}`,
url: 'https://fuss10.elemecdn.com/e/5d/4a731a90594a4af544c0c25941171jpeg.jpeg',
}
})
)
const value2 = ref(``)
const getList = ({ query }) => {
// 这里应该使用防抖处理远程搜索的结果,这里只做简单处理
return Promise.resolve(
Array.from({ length: 40 }).map((_, index) => {
return {
label: `Summer_${index}`,
id: `xzw_${index}`,
}
})
)
}
</script>
自定义提及列表
使用header
label
footer
插槽,分别对列表的头部,内容,尾部进行自定义 ui。
如何你想完全自定义提及列表,建议使用content
插槽
<template>
<el-editor v-model="value" :options="options" style="width: 320px">
<!--自定义头-->
<!-- <template #header>
<div>header</div>
</template> -->
<!--自定义label-->
<!-- <template #label="{ item, index }">
<div>{{ item }}-{{ index }}</div>
</template> -->
<!--自定义底部-->
<!-- <template #footer>
<div>header</div>
</template> -->
<!--完全自定义-->
<template #content="{ items, command }">
<ul style="list-style: none; margin: 0px; padding: 0">
<li
v-for="(item, index) in items"
:key="index"
@click="() => command(item)"
>
{{ item.label }}
</li>
</ul>
</template>
</el-editor>
</template>
<script setup>
import { ref } from 'vue'
const value = ref(`@`)
const options = ref(
Array.from({ length: 40 }).map((_, index) => {
return {
label: `Summer_${index}`,
id: `xzw_${index}`,
url: 'https://fuss10.elemecdn.com/e/5d/4a731a90594a4af544c0c25941171jpeg.jpeg',
}
})
)
</script>
插入
使用 组件实例的insertHtml
方法来插入文本
html
自定义元素
<template>
<div style="margin-bottom: 12px">
<el-button size="small" @click="insertText">插入文本</el-button>
<el-button size="small" @click="insertHtml">插入html</el-button>
<el-button size="small" @click="insertTag">插入自定义标签</el-button>
<el-button size="small" @click="insertComp">插入Vue组件</el-button>
<el-button size="small" @click="insertComp1">插入原生任意标签</el-button>
<el-button size="small" @click="getHtml">获取html内容</el-button>
<el-button size="small" @click="getText">获取text内容</el-button>
</div>
<el-editor
ref="editor"
v-model="html"
style="width: 500px; height: 180px; overflow: auto"
/>
<div
style="margin-top: 20px; width: 500px; max-height: 200px; overflow: auto"
>
{{ content }}
</div>
</template>
<script setup>
import { ref } from 'vue'
const content = ref()
const html = ref()
const editor = ref(null)
const getRandom = () => Math.floor(Math.random() * 100)
function insert(text) {
editor.value.insertHtml(text)
}
function getHtml() {
content.value = editor.value.getHtml()
}
function getText() {
content.value = editor.value.getText()
}
const insertTag = () =>
insert(
`<tag other-attr="hi" disable-transitions id="${getRandom()}" class="tag" text="Summer${getRandom()}"></tag>`
)
const insertComp = () =>
insert(
`<component is="el-button" type="warning" size="small" style="margin: 0 6px 6px 0;" wrap-class="comp__wrap-class">点我</component>`
)
const insertComp1 = () =>
insert(
`<component is="div"><div style="color: red"><i>hi summer</i></div></component>`
)
const insertHtml = () =>
insert(`
<h1><a href="https://tiptap.dev/">Tiptap</a></h1>
<p><strong>Hello World</strong></p>
<p>This is a paragraph<br />with a break.</p>
<p>And this is some additional string content.</p>
`)
const insertText = () => insert(`hello tiptap `)
</script>
<style lang="scss">
.comp__wrap-class {
display: inline-block;
}
</style>
插入任意内容(component 插件)
Tiptap 是不支持插入任意原生标签的,且支持的元素(比如 h1 和 p)也不支持编写内联样式。于是 editor 内置了component 插件
用来解决此问题,使用姿势和 vue 中的component
类似
component 插件
相比 vue 中的component
有一些限制。1:子内容只能最为组件的默认插槽,其他插槽不支持 2:属性只能传递字符串,引用类型不支持 3:上述问题可以通过把复杂的业务 UI 封装到一个 Vue 组件中(ui 的交互全在该组件中编写),最后用component
渲染即可
<template>
<div style="margin-bottom: 12px">
<el-button size="small" @click="insert1">插入任意原生标签</el-button>
<el-button size="small" @click="insert2">插入Vue组件</el-button>
<el-button size="small" @click="getHtml">获取html内容</el-button>
</div>
<el-editor
ref="editor"
v-model="html"
disable-enter-emit
style="width: 500px; height: 180px; overflow: auto"
/>
<div
style="margin-top: 20px; width: 500px; max-height: 200px; overflow: auto"
>
{{ content }}
</div>
</template>
<script setup>
import { ref } from 'vue'
const content = ref()
const html = ref()
const editor = ref(null)
function insert(text) {
editor.value.insertHtml(text)
}
function getHtml() {
content.value = editor.value.getHtml()
}
const insert1 = () =>
insert(
`<component is="div" style="border: 1px solid #ccc; width: 80%; margin-bottom: 12px; padding: 12px;">
<p>1.component最终渲染为div</p>
<span>2.这里的 p和span元素都会作为div的子内容</span>
</component>`
)
const insert2 = () =>
insert(
`<component is="el-card" wrap-class="comp__wrap-class1" style="width: 80%; margin-bottom: 12px;">
<el-flex vertical="vertical">
<el-text type="primary">
1. component最终渲染为ElCard组件
</el-text>
<el-text type="warning">
2. 注意: component标签上的属性最终会被解析为字符串,也就意味着不能传入引用类型的值,属性的大小写也敏感
</el-text>
<el-text type="danger">
3. component的子元素最终会被解析最为is组件的默认插槽内容
</el-text>
</el-flex>
</component>`
)
</script>
<style lang="scss">
.comp__wrap-class {
display: inline-block;
}
</style>
字数统计及限制
使用maxlength
属性来控制最大输入字数
<template>
<el-editor
v-model="value"
style="width: 400px; min-height: 60px"
placeholder="Please input"
maxlength="50"
@enter="handleEnter"
/>
</template>
<script setup>
import { ref } from 'vue'
import { ElMessage } from 'element-plus'
const value = ref()
setTimeout(() => {
value.value = '你好啊,Summer'
}, 1000)
const handleEnter = (values) => {
console.log(values)
ElMessage('回车做点什么')
}
</script>
表情包插入
配合ElEmoji
表情包来插入图片
可以使用setImage
方法
<template>
<div style="margin-bottom: 12px">
<el-emoji @change="insertImage">
<template #trigger>
<el-button size="small">插入表情包</el-button>
</template>
</el-emoji>
</div>
<el-editor
ref="editor"
v-model="html"
class="edit-wrapper"
style="width: 500px; min-height: 180px"
/>
</template>
<script setup>
import { ref } from 'vue'
const html = ref()
const editor = ref(null)
const insertImage = (item, type) => {
if (type === 'wx') {
editor.value.setImage({
src: item.image,
alt: item.text,
})
} else {
editor.value.insertHtml(
`<component is="span" wrap-class="wrap-class">${item.text}</component>`
)
}
}
</script>
<style lang="scss">
.edit-wrapper {
p {
vertical-align: middle;
}
img {
display: inline-block;
vertical-align: text-bottom;
}
.wrap-class {
display: inline-flex;
align-items: center;
justify-content: center;
span {
width: 24px;
height: 24px;
font-size: 22px;
}
}
}
</style>
视频插入
使用setVideo
方法来插入视频,参数参考 ISetVideOptions
<template>
<div style="margin-bottom: 12px">
<el-button size="small" @click="insertVideo">插入视频</el-button>
</div>
<el-editor
ref="editor"
v-model="html"
disable-enter-emit
style="width: 500px; min-height: 220px; max-height: 500px; overflow: auto"
/>
</template>
<script setup>
import { ref } from 'vue'
const html = ref()
const editor = ref(null)
const insertVideo = () =>
editor.value.setVideo({
src: 'https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4',
width: 320,
})
</script>
工具栏配置
使用toolbar-list
来配置需要展示的工具
上传图片时,可以根据配置onChange
回调拿到相关参数,调用后端接口获取真实 url,然后进行回显
<template>
<div class="editor">
<el-editor-toolbar
v-if="editorRef"
:editor="editorRef.editor"
:configure="configure"
:toolbar-list="[
'bold',
'strike',
'underline',
'link',
'ordered',
'bullet',
'image',
'video',
]"
class="editor-bar"
/>
<el-editor
ref="editorRef"
v-model="html"
disable-enter-emit
placeholder="输入点什么"
class="editor-wrapper"
/>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import type { UploadFile } from 'element-plus'
import type { ISetImageOptions, ISetVideOptions } from 'element-plus-x'
const html = ref()
const editorRef = ref()
const configure = {
image: {
onChange(
uploadFile: UploadFile,
callback: (options: ISetImageOptions) => void
) {
// 后台接口得到图片的真实地址,调用callback回显
setTimeout(() => {
callback({
src: 'https://fuss10.elemecdn.com/3/63/4e7f3a15429bfda99bce42a18cdd1jpeg.jpeg?imageMogr2/thumbnail/360x360/format/webp/quality/100',
alt: '图片',
title: '图片',
})
}, 300)
},
},
video: {
onChange(
uploadFile: UploadFile,
callback: (options: ISetVideOptions) => void
) {
console.log('视频:', uploadFile)
// 后台接口得到视频的真实地址,调用callback回显
setTimeout(() => {
callback({
src: 'https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4',
width: 320,
})
}, 300)
},
},
}
</script>
<style lang="scss">
.editor {
max-width: 500px;
&-wrapper {
min-height: 200px;
img {
max-width: 400px;
}
}
}
</style>
转换为纯文本(html2text)
Tiptap 支持自定义标签来渲染任意 ui,但是调用实例 getText 方法并不会返回文本,下面这个案例是把自定义标签解析成对应的值传给后端
html 内容可以用来回显编辑器的内容,有时需要解析 html 变成纯文本给后端,可以使用getTextContent
方法即可,回车换行符 br 默认替换成了\n
<template>
<div style="margin-bottom: 12px">
<el-emoji @change="insertImage">
<template #trigger>
<el-button size="small" style="margin-right: 12px">
插入表情包
</el-button>
</template>
</el-emoji>
<el-button size="small" @click="insertTag">插入Tag</el-button>
<el-button size="small" @click="getHtml">获取html内容</el-button>
<el-button size="small" type="success" @click="getText">
html2text
</el-button>
</div>
<el-editor
ref="editor"
v-model="html"
class="edit-wrapper"
style="max-width: 500px; min-height: 180px; margin-bottom: 12px"
/>
<el-input
v-model="content"
placeholder="赋值回显"
show-word-limit
type="textarea"
style="max-width: 500px"
:autosize="{ minRows: 4, maxRows: 8 }"
/>
<div
style="margin-top: 20px; width: 500px; max-height: 200px; overflow: auto"
>
{{ content }}
</div>
</template>
<script setup>
import { ref } from 'vue'
import { getTextContent } from 'element-plus-x'
const html = ref()
const editor = ref(null)
const content = ref()
function getHtml() {
content.value = editor.value.getHtml()
}
function getText() {
content.value = getTextContent(editor.value.getHtml(), [
{
tag: 'component',
attr: 'value',
},
{
tag: 'img',
attr: 'alt',
},
{
tag: 'tag',
attr: 'id',
},
])
}
const insertImage = (item, type) => {
if (type === 'wx') {
editor.value.setImage({
src: item.image,
alt: item.text,
})
} else {
editor.value.insertHtml(
`<component is="span" wrap-class="wrap-class" value="${item.text}">${item.text}</component>`
)
}
}
const getRandom = () => Math.floor(Math.random() * 100)
const insertTag = () => {
const id = getRandom()
editor.value.insertHtml(
`<tag id="Summer${id}" class="tag" text="Summer${id}"></tag> `
)
}
</script>
<style lang="scss">
.edit-wrapper {
p {
vertical-align: middle;
}
img {
display: inline-block;
vertical-align: text-bottom;
}
.wrap-class {
display: inline-flex;
align-items: center;
justify-content: center;
span {
width: 24px;
height: 24px;
font-size: 22px;
}
}
}
</style>
极简编辑器
使用ElEditorToolbar
组件来渲染工具栏,使用character-count
插槽来渲染文字统计。
编辑器内部的样式可以按照不同业务需求自定义样式
如果当做富文本编辑器时,记得使用disable-enter-emit
属性来关闭其内部回车发送事件
<template>
<div class="custom-editor">
<el-editor-toolbar
v-if="editorRef"
:editor="editorRef.editor"
:configure="configure"
class="custom-editor-bar"
/>
<el-editor
ref="editorRef"
v-model="html"
:border="false"
placeholder="输入点什么"
disable-enter-emit
class="custom-editor-wrapper"
>
<template #character-count="{ count }">
<div class="custom-editor-count">
<span>字符统计: {{ count }}</span>
</div>
</template>
</el-editor>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import type { UploadFile } from 'element-plus'
import type { ISetImageOptions, ISetVideOptions } from 'element-plus-x'
const html = ref()
const editorRef = ref()
const configure = {
image: {
onChange(
uploadFile: UploadFile,
callback: (options: ISetImageOptions) => void
) {
// 后台接口得到图片的真实地址,调用callback回显
setTimeout(() => {
callback({
src: 'https://fuss10.elemecdn.com/3/63/4e7f3a15429bfda99bce42a18cdd1jpeg.jpeg?imageMogr2/thumbnail/360x360/format/webp/quality/100',
alt: '图片',
title: '图片',
})
}, 300)
},
},
video: {
onChange(
uploadFile: UploadFile,
callback: (options: ISetVideOptions) => void
) {
console.log('视频:', uploadFile)
// 后台接口得到视频的真实地址,调用callback回显
setTimeout(() => {
callback({
src: 'https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4',
width: 320,
})
}, 300)
},
},
}
</script>
<style lang="scss">
.custom-editor {
border-radius: 8px;
border: 1px solid #ebeef5;
max-width: 530px;
&-wrapper {
margin: 6px 6px 0 6px;
border-top: 1px solid #ebeef5;
.el-editor-wrapper {
min-height: 400px;
max-height: 600px;
overflow: auto;
}
// 编辑器html内部样式-diy
img {
display: inline-block;
vertical-align: text-bottom;
max-width: 400px;
}
pre {
background: #2e2b29;
border-radius: 0.5rem;
color: #fff;
font-family: 'JetBrainsMono', monospace;
margin: 1.5rem 0;
padding: 0.75rem 1rem;
}
em {
font-style: italic;
}
blockquote {
border-left: 3px solid rgba(61, 37, 20, 0.12);
margin: 1.5rem 0;
padding-left: 1rem;
}
ul,
ol {
padding: 0 1rem;
margin: 1.25rem 1rem 1.25rem 0.4rem;
}
ul li p,
ol li p {
margin-top: 0.25em;
margin-bottom: 0.25em;
}
}
&-count {
border-top: 1px solid #ebeef5;
padding: 10px;
display: flex;
justify-content: flex-end;
align-items: center;
font-size: 14px;
span {
color: #939599;
}
}
}
</style>
API
属性
属性名 | 说明 | 类型 | 默认值 |
---|---|---|---|
v-model | 绑定的 html | string | - |
maxlength | 最大输入长度 | string | - |
options | 提及选项列表 | array|function | - |
prefix | 触发字段的前缀。 字符串长度必须且只能为 1 | string | @ |
extensions | tiptap 扩展包 | array | - |
disabled | 是否禁用 | boolean | false |
disable-enter-emit | 是否禁用回车 enter 事件 | boolean | false |
border | 编辑器是否有边框 | boolean | true |
Methods
方法名 | 说明 | 参数 |
---|---|---|
insertHtml | 插入文本或 html | Function |
resetHtml | 重置文本或 html | Function |
getHtml | 获取 html 值 | - |
getText | 获取 纯文本 | - |
setImage | 插入图片 | Function |
Slots
插槽名 | 说明 | 参数 |
---|---|---|
prepend | 编辑器前置插槽 | - |
append | 编辑器后置插槽 | - |
header | 提及列表头插槽 | - |
label | 提及列表 label 插槽 | Object |
footer | 提及列表尾部插槽 | - |
content | 完全自定义提及列表插槽 | Object |
ElEditorToolbar 属性
属性名 | 说明 | 类型 | 默认值 |
---|---|---|---|
editor | 编辑器实例 | Object | - |
toolbar-list | 工具栏列表 | Array | - |
configure | 工具栏配置 | Object | - |