第二次写文章,如有错误请指正,多多担待,菜鸟一枚,话不多说,直接开始;
摘要:
本文主要解决wangeditor富文本编辑器不支持直接粘贴图文混合图片不显示的情况,本文章主要支持word,wps,网页,其它来源暂无考虑进去;使用本代码可能存在安全漏洞,望各位悉知,自行斟酌;
安装使用:
安装:个人推荐使用yarn
yarn add @wangeditor/editor
# 或者 npm install @wangeditor/editor --save
yarn add @wangeditor/editor-for-vue
# 或者 npm install @wangeditor/editor-for-vue --save
使用:局部导入注册
<script>
import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
export default {
components: { Editor, Toolbar },
}
</script>
使用:样式引入
<style src="@wangeditor/editor/dist/css/style.css"></style>
vue2模板部分:
<template>
<div style="border: 1px solid #ccc">
<Toolbar
style="border-bottom: 1px solid #ccc"
:editor="editor"
:defaultConfig="toolbarConfig"
:mode="mode"
/>
<Editor
style="height: 500px; overflow-y: hidden"
v-model="editorHtml"
:defaultConfig="editorConfig"
:mode="mode"
@onCreated="onCreated"
@customPaste="customPaste"
/>
</div>
</template>
接下来需要写methods和data:
export default {
components: { Editor, Toolbar },
data(){
return:{
editor: null,
timeoutId: [], // 存储setTimeout的ID
editorHtml: '',
toolbarConfig: {},
editorConfig: { placeholder: '请输入内容...' },
mode: 'default', // or 'simple'
value: '',
imgParent: ['span', 'font','a','picture'] // 图片可能存在的直接祖先行元素
}
}
}
备注:data函数返回值中目前包含了下文可能用到的属性;
export default {
components: { Editor, Toolbar },
data(){
return:{
editor: null,
timeoutId: [], // 存储setTimeout的ID
editorHtml: '',
toolbarConfig: {},
editorConfig: { placeholder: '请输入内容...' },
mode: 'default', // or 'simple'
value: '',
imgParent: ['span', 'font','a','picture'] // 图片可能存在的直接祖先行元素
}
},
methods: {
onCreated(editor) {
this.editor = Object.seal(editor) // 一定要用 Object.seal() ,否则会报错
},
// 粘贴方法,此事件可以覆盖编辑器原本的Paste
// 粘贴方法
customPaste(editor, event, callback) {
event.preventDefault()
let image = event.clipboardData.items[0].getAsFile() // 获取粘贴的图片文件对象
let html = event.clipboardData.getData('text/html') // 获取粘贴的 html
let rtf = event.clipboardData.getData('text/rtf') // 获取 rtf 数据(如从 word wsp 复制粘贴)
let timeoutId
if (rtf) {
let imgs = this.findAllImageElementsWithLocalSource(html)
let imgs1 = this.replaceImagesFileSourceWithInlineRepresentation(
imgs,
this.extractImageDataFromRtf(rtf)
)
this.value.querySelectorAll('img').forEach((img, index) => {
if (imgs1.length > 0) {
img.setAttribute('src', imgs1[index].src)
} else {
// 将base64转换为文件对象
// let boldFile = this.convertBase64ToBlob(img.src)
// img.setAttribute('src', URL.createObjectURL(boldFile))
img.setAttribute('src', img.src)
}
img.style.width = this.imgWidth(img)
img.style.height = this.imgHeight(img)
if (
parseFloat(img.style.height) <= 20 &&
parseFloat(img.style.width) > parseFloat(img.style.height)
) {
img.style.width = 'auto'
} else if (
parseFloat(img.style.width) <= 20 &&
parseFloat(img.style.height) > parseFloat(img.style.width)
) {
img.style.height = 'auto'
}
this.findParent(img)
return img
})
timeoutId = setTimeout(() => {
editor.dangerouslyInsertHtml(this.value.outerHTML)
}, 100)
} else if (html && !rtf) {
const div = document.createElement('div')
div.innerHTML = html
const imgs = div.querySelectorAll('img')
imgs.forEach((img) => {
let newSrc = img.src
img.onerror = () => {
// if (img.src.startsWith('data:image/')) {
// let boldFile = this.convertBase64ToBlob(img.src)
// newSrc = URL.createObjectURL(boldFile)
// } else
if (img.src.startsWith('http')) {
const cleanSrc = img.src.split('?')[0]
// 构造Base64地址; **这里要换成自己的服务器转换地址**
newSrc = `https://aaaa.bbbb.com/download/img/${btoa(
cleanSrc
)}`
}
if (this.editorHtml?.includes(img.src)) {
this.editorHtml = this.editorHtml.replace(img.src, newSrc)
}
img.setAttribute('src', newSrc)
img.onerror = null
}
img.style.width = this.imgWidth(img)
img.style.height = this.imgHeight(img)
if (
parseFloat(img.style.height) <= 20 &&
parseFloat(img.style.width) > parseFloat(img.style.height)
) {
img.style.width = 'auto'
} else if (
parseFloat(img.style.width) <= 20 &&
parseFloat(img.style.height) > parseFloat(img.style.width)
) {
img.style.height = 'auto'
}
this.findParent(img)
return img
})
timeoutId = setTimeout(() => {
editor.dangerouslyInsertHtml(div.innerHTML)
}, 100)
} else if (image && image.type.startsWith('image/')) {
// let img = new Image()
// 转换为base64地址
let reader = new FileReader()
reader.readAsDataURL(image)
reader.onload = (e) => {
let base64 = e.target.result
let img = new Image()
img.src = base64
editor.dangerouslyInsertHtml(img.outerHTML)
}
// img.src = URL.createObjectURL(image)
// editor.dangerouslyInsertHtml(img.outerHTML)
}
this.timeoutId.push(timeoutId)
callback(true) // 返回值(注意,vue 事件的返回值,不能用 return)
},
}
}
备注:editor.dangerouslyInsertHtml();此方法具有安全隐患;
完整代码:
<template>
<div style="border: 1px solid #ccc">
<Toolbar
style="border-bottom: 1px solid #ccc"
:editor="editor"
:defaultConfig="toolbarConfig"
:mode="mode"
/>
<Editor
style="height: 500px; overflow-y: hidden"
v-model="editorHtml"
:defaultConfig="editorConfig"
:mode="mode"
@onCreated="onCreated"
@customPaste="customPaste"
/>
</div>
</template>
<script>
import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
export default {
components: { Editor, Toolbar },
data() {
return {
editor: null,
timeoutId: [], // 存储setTimeout的ID
editorHtml: '',
toolbarConfig: {},
editorConfig: { placeholder: '请输入内容...' },
mode: 'default', // or 'simple'
value: '',
imgParent: ['span', 'font', 'a', 'picture'] // 图片可能存在的直接祖先行元素
}
},
methods: {
onCreated(editor) {
this.editor = Object.seal(editor) // 一定要用 Object.seal() ,否则会报错
},
// 粘贴方法
customPaste(editor, event, callback) {
event.preventDefault()
let image = event.clipboardData.items[0].getAsFile() // 获取粘贴的图片文件对象
let html = event.clipboardData.getData('text/html') // 获取粘贴的 html
let rtf = event.clipboardData.getData('text/rtf') // 获取 rtf 数据(如从 word wsp 复制粘贴)
let timeoutId
if (rtf) {
let imgs = this.findAllImageElementsWithLocalSource(html)
let imgs1 = this.replaceImagesFileSourceWithInlineRepresentation(
imgs,
this.extractImageDataFromRtf(rtf)
)
this.value.querySelectorAll('img').forEach((img, index) => {
if (imgs1.length > 0) {
img.setAttribute('src', imgs1[index].src)
} else {
// 将base64转换为文件对象
// let boldFile = this.convertBase64ToBlob(img.src)
// img.setAttribute('src', URL.createObjectURL(boldFile))
img.setAttribute('src', img.src)
}
img.style.width = this.imgWidth(img)
img.style.height = this.imgHeight(img)
if (
parseFloat(img.style.height) <= 20 &&
parseFloat(img.style.width) > parseFloat(img.style.height)
) {
img.style.width = 'auto'
} else if (
parseFloat(img.style.width) <= 20 &&
parseFloat(img.style.height) > parseFloat(img.style.width)
) {
img.style.height = 'auto'
}
this.findParent(img)
return img
})
timeoutId = setTimeout(() => {
editor.dangerouslyInsertHtml(this.value.outerHTML)
}, 100)
} else if (html && !rtf) {
const div = document.createElement('div')
div.innerHTML = html
const imgs = div.querySelectorAll('img')
imgs.forEach((img) => {
let newSrc = img.src
img.onerror = () => {
// if (img.src.startsWith('data:image/')) {
// let boldFile = this.convertBase64ToBlob(img.src)
// newSrc = URL.createObjectURL(boldFile)
// } else
if (img.src.startsWith('http')) {
const cleanSrc = img.src.split('?')[0]
// 构造Base64地址
newSrc = `https://aaa.bbb.com/download/img/${btoa(
cleanSrc
)}`
}
if (this.editorHtml?.includes(img.src)) {
this.editorHtml = this.editorHtml.replace(img.src, newSrc)
}
img.setAttribute('src', newSrc)
img.onerror = null
}
img.style.width = this.imgWidth(img)
img.style.height = this.imgHeight(img)
if (
parseFloat(img.style.height) <= 20 &&
parseFloat(img.style.width) > parseFloat(img.style.height)
) {
img.style.width = 'auto'
} else if (
parseFloat(img.style.width) <= 20 &&
parseFloat(img.style.height) > parseFloat(img.style.width)
) {
img.style.height = 'auto'
}
this.findParent(img)
return img
})
timeoutId = setTimeout(() => {
editor.dangerouslyInsertHtml(div.innerHTML)
}, 100)
} else if (image && image.type.startsWith('image/')) {
// let img = new Image()
// 转换为base64地址
let reader = new FileReader()
reader.readAsDataURL(image)
reader.onload = (e) => {
let base64 = e.target.result
let img = new Image()
img.src = base64
editor.dangerouslyInsertHtml(img.outerHTML)
}
// img.src = URL.createObjectURL(image)
// editor.dangerouslyInsertHtml(img.outerHTML)
}
this.timeoutId.push(timeoutId)
callback(true) // 返回值(注意,vue 事件的返回值,不能用 return)
},
// 查找图片的父元素
findParent(element) {
let parent = element.parentElement
// 向上遍历,检查祖先元素
while (parent && parent.tagName.toLowerCase() !== 'body') {
if (this.imgParent.includes(parent.tagName.toLowerCase())) {
const newDiv = document.createElement('div')
// 将原有内容迁移到新的 div 中
while (parent.firstChild) {
newDiv.appendChild(parent.firstChild)
}
// 用新的 div 替换掉原来的 span 或 font
parent.parentElement.replaceChild(newDiv, parent)
parent = newDiv
} else {
parent = parent.parentElement
}
}
},
imgWidth(img) {
const width = img.width || 0 // 获取 img 实际宽度
const styleWidth = img.style.width ? parseFloat(img.style.width) || 0 : 0 // 获取样式中的宽度并转换为数字
const maxWidth = img.style.maxWidth
? parseFloat(img.style.maxWidth) || 0
: 0 // 获取样式中的最大宽度并转换为数字
// 获取根元素的字体大小以计算rem的像素值
const rootFontSize =
parseFloat(getComputedStyle(document.documentElement).fontSize) || 1 // 默认值为1以避免NaN
// 转换为像素,如果是百分比则乘以父元素的宽度
const convertedStyleWidth =
img.style.width && img.style.width.endsWith('%')
? (parseFloat(img.style.width) / 100) *
(img.parentElement.clientWidth || 0)
: img.style.width && img.style.width.endsWith('rem')
? parseFloat(img.style.width) * rootFontSize
: styleWidth
const convertedMaxWidth =
img.style.maxWidth && img.style.maxWidth.endsWith('%')
? (parseFloat(img.style.maxWidth) / 100) *
(img.parentElement.clientWidth || 0)
: img.style.maxWidth && img.style.maxWidth.endsWith('rem')
? parseFloat(img.style.maxWidth) * rootFontSize
: maxWidth
// 找到最小不为零的值
const values = [width, convertedStyleWidth, convertedMaxWidth].filter(
(v) => !isNaN(v) && v > 0
)
console.log(width, convertedStyleWidth, convertedMaxWidth)
return values.length > 0 ? Math.min(...values) + 'px' : 'auto'
},
imgHeight(img) {
const height = img.height || 0 // 获取 img 实际高度
const styleHeight = img.style.height
? parseFloat(img.style.height) || 0
: 0 // 获取样式中的高度并转换为数字
const maxHeight = img.style.maxHeight
? parseFloat(img.style.maxHeight) || 0
: 0 // 获取样式中的最大高度并转换为数字
// 获取根元素的字体大小以计算rem的像素值
const rootFontSize =
parseFloat(getComputedStyle(document.documentElement).fontSize) || 1 // 默认值为1以避免NaN
// 转换为像素,如果是百分比则乘以父元素的高度
const convertedStyleHeight =
img.style.height && img.style.height.endsWith('%')
? (parseFloat(img.style.height) / 100) *
(img.parentElement.clientHeight || 0)
: img.style.height && img.style.height.endsWith('rem')
? parseFloat(img.style.height) * rootFontSize
: styleHeight
const convertedMaxHeight =
img.style.maxHeight && img.style.maxHeight.endsWith('%')
? (parseFloat(img.style.maxHeight) / 100) *
(img.parentElement.clientHeight || 0)
: img.style.maxHeight && img.style.maxHeight.endsWith('rem')
? parseFloat(img.style.maxHeight) * rootFontSize
: maxHeight
// 找到最小不为零的值
const values = [height, convertedStyleHeight, convertedMaxHeight].filter(
(v) => !isNaN(v) && v > 0
)
console.log(height, convertedStyleHeight, convertedMaxHeight)
return values.length > 0 ? Math.min(...values) + 'px' : 'auto'
},
_convertHexToBase64(hexString) {
return btoa(
hexString
.match(/\w{2}/g)
.map((char) => {
return String.fromCharCode(parseInt(char, 16))
})
.join('')
)
}, // 将base64转换为文件对象
convertBase64ToBlob(base64) {
const base64Arr = base64.split(',')
let imgtype = ''
let base64String = ''
if (base64Arr.length > 1) {
// 如果是图片base64,去掉头信息
base64String = base64Arr[1]
imgtype = base64Arr[0].substring(
base64Arr[0].indexOf(':') + 1,
base64Arr[0].indexOf(';')
)
} // 将base64解码
var bytes = atob(base64String)
var bytesCode = new ArrayBuffer(bytes.length) // 转换为类型化数组
var byteArray = new Uint8Array(bytesCode) // 将base64转换为ascii码
for (var i = 0; i < bytes.length; i++) {
byteArray[i] = bytes.charCodeAt(i)
} // 生成Blob对象(文件对象)
return new Blob([byteArray], { type: imgtype })
},
findAllImageElementsWithLocalSource(documentFragment) {
const div = document.createElement('div')
div.innerHTML = documentFragment
this.value = div
const imgs = []
div.querySelectorAll('img').forEach((img) => {
if (img.src.startsWith('file://')) {
imgs.push(img)
}
})
return imgs
},
extractImageDataFromRtf(rtfData) {
if (!rtfData) {
return []
} // 旧的写法 // const regexPictureHeader = /{\\pict[\s\S]+?\\bliptag-?\d+(\\blipupi-?\d+)?({\\\*\\blipuid\s?[\da-fA-F]+)?[\s}]*?/ // 新删减后的写法
const regexPictureHeader = /{\\pict[\s\S]+?({\\\*\\blipuid\s?[\da-fA-F]+)[\s}]*/
const regexPicture = new RegExp(
'(?:(' + regexPictureHeader.source + '))([\\da-fA-F\\s]+)\\}',
'g'
)
const images = rtfData.match(regexPicture)
const result = []
if (images) {
for (const image of images) {
let imageType = false
if (image.includes('\\pngblip')) {
imageType = 'image/png'
} else if (image.includes('\\jpegblip')) {
imageType = 'image/jpeg'
}
if (imageType) {
result.push({
hex: image
.replace(regexPictureHeader, '')
.replace(/[^\da-fA-F]/g, ''),
type: imageType
})
}
}
}
return result
},
replaceImagesFileSourceWithInlineRepresentation(
imageElements,
imagesHexSources
) {
// Assume there is an equal amount of image elements and images HEX sources so they can be matched accordingly based on existing order.
if (imageElements.length === imagesHexSources.length) {
for (let i = 0; i < imageElements.length; i++) {
const newSrc = `data:${
imagesHexSources[i].type
};base64,${this._convertHexToBase64(imagesHexSources[i].hex)}`
// let boldFile = this.convertBase64ToBlob(newSrc)
// imageElements[i].setAttribute('src', URL.createObjectURL(boldFile))
imageElements[i].setAttribute('src', newSrc)
}
}
return imageElements
}
},
beforeDestroy() {
const editor = this.editor
if (editor == null) return
editor.destroy() // 组件销毁时,及时销毁编辑器
// 清除定时器
if (this.timeoutId.length > 0) {
this.timeoutId.forEach((timeoutId) => {
clearTimeout(timeoutId)
})
}
}
}
</script>
<style src="@wangeditor/editor/dist/css/style.css"></style>
总结:
部分方法是借鉴网友的,感谢被我借鉴的网友,记不清啦!jym如有问题请留言