关于Vue内使用tinymce图片上传粘贴相关问题
最近因为工作需要,用到了富文本编辑器让用户填写反馈,上传图片等功能,经过一些对比选择了tinymce,记录下图片相关问题。
完整版封装的组件代码,放到最后。
环境
- vue2.x
- tinymce 5.10.3
- tinymce-vue 2.1.0
这里由于开发环境是vue2,所以目前这个时间点,需要选择版本的去安装,引用官方文档的一句话
Version 4 of the
tinymce-vue
package supports Vue.js 3.x, but does not support Vue.js 2.x. For Vue.js 2.x applications, usetinymce-vue
version 3.
图片上传
这个比较简单,在init的配置中,配置images_upload_handler
...
data () {
init: {
images_upload_handler:this.handleImageUpload
}
},
methods: {
handleImageUpload (blobInfo, success, failure) {
// 将图片上传到服务器.
let formdata = new FormData()
formdata.append('file', blobInfo.blob(), blobInfo.filename())
this.uploadImage(formdata)
.then(success)
.catch(failure)
},
uploadImage (formdata) {
return new Promise((resolve, reject) => {
Axios({
url: 'https://xxxx.xx.com/xxx/xxx',
method: 'post',
data: formdata,
headers: {
'Content-Type': 'multipart/form-data'
}
}).then(result => {
console.log(result)
if (result.status !== 200 || result.data.code !== '200') {
const msg = '上传失败'
reject(msg)
} else {
resolve(result.data.data)
}
})
})
}
}
图片粘贴
刚开始是想到的直接监听document
的paste
事件,去获取剪切板的图片,代码如下:
// 代码引用至张鑫旭的个人网站文章
// https://www.zhangxinxu.com/wordpress/2018/09/ajax-upload-image-from-clipboard/
document.addEventListener('paste', function (event) {
var items = event.clipboardData && event.clipboardData.items;
var file = null;
if (items && items.length) {
// 检索剪切板items
for (var i = 0; i < items.length; i++) {
if (items[i].type.indexOf('image') !== -1) {
file = items[i].getAsFile();
break;
}
}
}
// 此时file就是剪切板中的图片文件
});
后来发现不生效,检查一下元素得知,tinymce是通过iframe
使用的,
所以在tinymce输入框内,paste事件无法触发
后来找到了init_instance_callback
配置项,返回的实例可以监听粘贴事件
init: {
init_instance_callback: editor => {
editor.on('paste', e => {})
}
}
粘贴监听算是解决了,接下来要处理的是,粘贴后如何如何替换图片。
因为默认粘贴进去的图片,是以base64存在的,这样直接存进数据库不好,所以需要将base64替换。
刚开始想到的是,将base64删除,然后替换新的img标签进去,这样存在一个问题,就是如果上传图片的网络慢,用户多次粘贴,或者输入文字,图片的位置就会错位. 最后这里选择了在用户一粘贴,拿到base64,上传成功后替换即可。代码如下
methods: {
listenImgPaste (event) {
return new Promise(resolve => {
const items = event.clipboardData && event.clipboardData.items
let file = null
if (items && items.length) {
for (let i = 0; i < items.length; i++) {
if (items[i].type.indexOf('image') !== -1) {
file = items[i].getAsFile()
break
}
}
}
if (file) {
setTimeout(() => {
// 获取当前图片的base64
const base64 = this.myValue.match(/src="data:image.*?"/g)
let formdata = new FormData()
formdata.append('file', file)
this.uploadImage(formdata).then(url => {
// 成功后将base64替换
this.myValue = this.myValue.replace(base64, `src="${url}"`)
resolve()
})
})
}
})
},
}
成功实现。
最后一个问题,替换修改值以后,tinymce会默认将光标定位到最前面,体验不是很好,最后的解决方法也是加配置
init: {
init_instance_callback: editor => {
// 初始化后移动光标到最后
this.moveCursorToLast(editor)
console.log(editor)
editor.on('paste', async event => {
await this.listenImgPaste(event)
this.moveCursorToLast(editor)
})
}
}
methods: {
// 移动光标到最后
moveCursorToLast (editor) {
editor.selection.select(editor.getBody(), true)
editor.selection.collapse(false)
}
}
最后的完整组件代码
// tinymce.vue
<template>
<div>
<editor
ref="tinymce"
v-model="myValue"
:init="init"
></editor>
</div>
</template>
<script>
// import xx form 'xxx'
import Editor from '@tinymce/tinymce-vue'
import Axios from 'axios'
export default {
name: 'tinymce',
props: {
value: String
},
components: {
Editor
},
watch: {
myValue (val) {
this.$emit('input', val)
},
value: {
handler (v) {
this.myValue = v
},
immediate: true
}
},
data () {
return {
myValue: '',
init: {
// language_url: '/static/plugins/tinymce/zh_CN.js', // 如果语言包不存在,指定一个语言包路径
language: 'zh_CN', // 语言
// skin_url: '/static/plugins/tinymce/skins/ui/oxide', // 如果主题不存在,指定一个主题路径
// content_css: '/static/plugins/tinymce/mycontent.css',
height: '700px',
width: '1200px',
plugins: 'link image', // 插件
toolbar: 'bold italic underline strikethrough link image', // 工具栏
branding: false, // 技术支持(Powered by Tiny || 由Tiny驱动)
menubar: false, // 菜单栏
theme: 'silver', // 主题
zIndex: 1101,
paste_data_images: true,
images_upload_handler: this.handleImageUpload,
init_instance_callback: editor => {
// 初始化后移动光标到最后
this.moveCursorToLast(editor)
editor.on('paste', async event => {
await this.listenImgPaste(event)
this.moveCursorToLast(editor)
})
}
}
}
},
methods: {
listenImgPaste (event) {
return new Promise(resolve => {
const items = event.clipboardData && event.clipboardData.items
let file = null
if (items && items.length) {
for (let i = 0; i < items.length; i++) {
if (items[i].type.indexOf('image') !== -1) {
file = items[i].getAsFile()
break
}
}
}
if (file) {
setTimeout(() => {
// 获取当前图片的base64
const base64 = this.myValue.match(/src="data:image.*?"/g)
let formdata = new FormData()
formdata.append('file', file)
this.uploadImage(formdata).then(url => {
// 成功后将base64替换
this.myValue = this.myValue.replace(base64, `src="${url}"`)
resolve()
})
})
}
})
},
handleImageUpload (blobInfo, success, failure) {
console.log(blobInfo, blobInfo.filename())
// 将图片上传到服务器.
let formdata = new FormData()
formdata.append('file', blobInfo.blob(), blobInfo.filename())
this.uploadImage(formdata)
.then(success)
.catch(failure)
},
// 上传图片
uploadImage (formdata) {
return new Promise((resolve, reject) => {
Axios({
url: 'https://xxxx.xxx.xxx/xxx/xxx/xx',
method: 'post',
data: formdata,
headers: {
'Content-Type': 'multipart/form-data'
}
}).then(result => {
console.log(result)
if (result.status !== 200 || result.data.code !== '200') {
const msg = '上传失败'
reject(msg)
} else {
resolve(result.data.data)
}
})
})
},
// 移动光标到最后
moveCursorToLast (editor) {
editor.selection.select(editor.getBody(), true)
editor.selection.collapse(false)
}
}
}
</script>
<style scoped>
</style>
引用
<div>
<tinymce v-model="form.des"/>
</div>
<script>
import tinymce from '../../../common/tinymce'
export default {
name: 'createFeedback',
components: {
tinymce
},
data () {
return {
form: {
des: ''
}
}
}
</script>
转载自:https://juejin.cn/post/7148385803981291528