自定义自定义Vue-Quill-Editor富文本框可参照:
https://blog.csdn.net/ccsundhine/article/details/125867053?spm=1001.2014.3001.5502
注意:
- 秀米官网声明只支持ueditor内核的编辑器内核(本文使用quill富文本框,自定义了一个blot文件,防止quill自动过滤掉秀米和135编辑器里面的section之类的样式)
- 秀米的第三方对接文档地址:https://ent.xiumi.us/doc2.html
- 您的网站务必使用https访问,否则会造成用户无法登录秀米账户
- 秀米官方更新后,在本地开发环境时,无法正常插入数据到编辑器;且在ip环境下无法登录秀米
- 秀米插入编辑器前需要做图片本地化处理才能正常显示(如果不处理图片就会裂开,上传的图片是存放在秀米的服务器上面的,这样会消耗秀米的服务器资源,所以秀米会禁止外站的图片请求)可以考虑两种方式:quill自定义处理粘贴的文本内容;在index.html通过通过referrer去处理;
- 秀米以及135编辑器回显的时候,如果有section之类的元素也会被过滤,所以在回显数据的时候也需要处理
- 把秀米编辑器和135编辑器的html文件放入public文件下(秀米这里有两个文件一个新版一个旧版本,自愿引入哪一个)
135EditorDialogPage.html文件:
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>135编辑器</title>
<style>
html,
body {
padding: 0;
margin: 0;
}
#editor135 {
position: absolute;
width: 100%;
height: 100%;
border: none;
box-sizing: border-box;
}
</style>
</head>
<body>
<iframe id="editor135" src="//www.135editor.com/simple_editor.html?callback=true&appkey="></iframe>
<!-- <script type="text/javascript" src="internal.js"></script> -->
<script>
var editor135 = document.getElementById('editor135');
var parent = window.parent;
window.onload = function () {
setTimeout(function () {
editor135.contentWindow.postMessage(parent.getHtml(), '*');
// parent.getHtml 其实是quill里暴露的 window.getHtml
}, 3000);
};
document.addEventListener("mousewheel", function (event) {
event.preventDefault();
event.stopPropagation();
});
window.addEventListener('message', function (event) {
if (typeof event.data !== 'string') return;
parent.setRichText_135(event.data)
// editor.setContent(event.data);
// editor.fireEvent("catchRemoteImage");
// dialog.close();
}, false);
</script>
</body>
</html>
xiumi-ue-dialog-v5_new.html文件:
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>XIUMI connect</title>
<style>
html,
body {
padding: 0;
margin: 0;
}
#xiumi {
position: absolute;
width: 100%;
height: 100%;
border: none;
box-sizing: border-box;
}
</style>
</head>
<body>
<iframe id="xiumi" src="//xiumi.us/studio/v5#/paper">
</iframe>
<!-- <script type="text/javascript" src="dialogs/internal.js"></script> -->
<script>
var parent = window.parent;
console.log('parent: ', parent);
var xiumi = document.getElementById('xiumi');
var xiumi_url = window.location.protocol + "//xiumi.us";
console.log("xiumi_url is %o", xiumi_url);
xiumi.onload = function () {
console.log("postMessage to %o", xiumi_url);
// "XIUMI:3rdEditor:Connect" 是特定标识符,不能修改,大小写敏感
xiumi.contentWindow.postMessage('XIUMI:3rdEditor:Connect', xiumi_url);
};
document.addEventListener("mousewheel", function (event) {
event.preventDefault();
event.stopPropagation();
});
window.addEventListener('message', function (event) {
console.log("Received message from xiumi, origin: %o %o", event.origin, xiumi_url);
console.log('event.data: ', event.data);
if (event.origin == xiumi_url) {
console.log("Inserting html");
parent.setRichText_xm(event.data)
console.log("Xiumi dialog is closing");
// dialog.close();
}
}, false);
</script>
</body>
</html>
xiumi-ue-dialog-v5.html文件:
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>XIUMI connect</title>
<style>
html,
body {
padding: 0;
margin: 0;
}
#xiumi {
position: absolute;
width: 100%;
height: 100%;
border: none;
box-sizing: border-box;
}
</style>
</head>
<body>
<iframe id="xiumi" src="//xiumi.us/studio/v5#/paper">
</iframe>
<!-- <script type="text/javascript" src="internal.js"></script> -->
<script>
var parent = window.parent;
var xiumi = document.getElementById('xiumi');
var xiumi_url = window.location.protocol + "//xiumi.us";
xiumi.onload = function () {
console.log("postMessage");
xiumi.contentWindow.postMessage('ready', xiumi_url);
};
document.addEventListener("mousewheel", function (event) {
event.preventDefault();
event.stopPropagation();
});
window.addEventListener('message', function (event) {
if (event.origin == xiumi_url) {
parent.setRichText_xm(event.data)
// editor.execCommand('insertHtml', event.data);
// dialog.close();
}
}, false);
</script>
</body>
</html>
- 用iframe引入(这里踩了一个坑!!!!src引入文件的时候一定要看清楚自己项目的publicPath )
<div :style="{ height: fullheight + 'px' }">
<!-- quill -->
<quill-editor
ref="myQuillEditor"
v-model="articleForm.artContent"
v-screen
class="quilleditor"
:options="editorOption"
style="height: 265px"
/>
<el-dialog
:append-to-body="true"
:close-on-click-modal="false"
:modal-append-to-body="false"
title="秀米"
top="50px"
:visible.sync="visible"
width="90%"
z-index="99999999"
>
<!-- 秀米插件弹框 -->
<div v-if="visible">
<!-- :src="`${baseUrl}static/xiumi-ue-dialog-v5_new.html?time=1267765432`" -->
<iframe
id="xiumiIframe"
frameborder="0"
:height="fullheight - 150 + 'px'"
src="./static/xiumi-ue-dialog-v5_new.html"
width="100%"
></iframe>
</div>
</el-dialog>
<!-- 135编辑器弹框 -->
<el-dialog
:append-to-body="true"
:close-on-click-modal="false"
:modal-append-to-body="false"
title="135编辑器"
top="50px"
:visible.sync="visible2"
width="90%"
z-index="99999999"
>
<div v-if="visible2">
<iframe
id="xiumiIframe"
frameborder="0"
:height="fullheight - 150 + 'px'"
src="./static/135EditorDialogPage.html"
width="100%"
></iframe>
</div>
</el-dialog>
</div>
- 自定义blot.js文件(防止quill过滤)
export default function (Quill) {
// 引入源码中的BlockEmbed
const BlockEmbed = Quill.import('blots/block/embed');
// 定义新的blot类型
class AppPanelEmbed extends BlockEmbed {
static create(value) {
const node = super.create(value);
// node.setAttribute('contenteditable', 'false');
// node.setAttribute('width', '100%');
// 设置自定义html
node.innerHTML = this.transformValue(value)
// 返回firstChild,避免被包一层<div class='rich-innerHtml'></div>的无意义标签
return node.firstChild;
}
static transformValue(value) {
let handleArr = value.split('\n')
handleArr = handleArr.map(e => e.replace(/^[\s]+/, '')
.replace(/[\s]+$/, ''))
return handleArr.join('')
}
// 返回节点自身的value值 用于撤销操作
static value(node) {
return node.innerHTML
}
}
// blotName
AppPanelEmbed.blotName = 'AppPanelEmbed';
// class名将用于匹配blot名称
AppPanelEmbed.className = 'rich-innerHtml';
// 标签类型自定义,这玩意还必须加,去掉会报错
AppPanelEmbed.tagName = 'div';
Quill.register(AppPanelEmbed, true);
}
- 在js中使用 (引入blot文件,配置quill等)
<script>
// 秀米引入
import blotSelect from './components/blot.js'
blotSelect(Quill)
// 工具栏配置(可根据自己的需求配置quill工具栏)
const toolbarOptions = [
['insertMetric'], //秀米
['otEdit'], //135编辑器
]
export default {
data() {
return {
msg: undefined,
imgFile: undefined,
//富文本内容
articleForm:{
artContent:""
},
visible: false,//秀米
visible2: false,135编辑器
selection: {}, // 光标位置
fullheight: document.documentElement.clientHeight, // 给quill容器设置了个高度
quill: null, // 待初始化的编辑器
//quill配置
editorOption: {
modules: {
toolbar: {
container: toolbarOptions, //自定义工具栏
handlers: {
that: this,
// 秀米
insertMetric: function () {
let self = this.handlers.that
self.visible = true
},
// 135编辑器
otEdit: function () {
let self = this.handlers.that
self.visible2 = true
},
},
},
},
//主题
theme: 'snow',
placeholder: '请输入正文',
},
}
},
watch: {
value(newVal, oldVal) {
console.log(newVal, oldVal)
if (newVal) {
this.articleForm.artContent = newVal
} else if (!newVal) {
this.articleForm.artContent = ''
}
},
},
created() {
this.articleForm.artContent = this.value
},
mounted() {
this._initEditor()//初始化编辑器
this.initButton() //自定义图标(秀米,135)
// 暴露方法绑定到window上,给public\xiumi-ue-dialog-v5.html使用
window.setRichText_xm = this.setRichText_xm
window.setRichText_135 = this.setRichText_135
// 调用135页面的时候 带入数据 getHtml()
window.getHtml = this.getHtml
},
methods:{
// 初始化编辑器
_initEditor() {
// 初始化编辑器
this.quill = this.$refs.myQuillEditor.quill
// 双向绑定代码 v-model
this.quill.on('text-change', () => {
this.emitChange()
this.selection = this.quill.getSelection()
})
// 插入内容
this.firstSetHtml()
// 粘贴板监听
this.listenPaste()
},
//秀米编辑器
setRichText_xm(e) {
const index = this.selection ? this.selection.index : 0
// console.log('光标位置',index)
this.quill.insertEmbed(index || 0, 'AppPanelEmbed', e)
this.visible = false
},
//135编辑器
setRichText_135(e) {
const index = this.selection ? this.selection.index : 0
//这个主要是用来处理在135编辑器添加导出到quill再点击135编辑器返回到quill的重复内容
this.quill.setContents([
{ insert: '', attributes: { bold: true } },
{ insert: '\n' },
])
this.quill.insertEmbed(index || 0, 'AppPanelEmbed', e)
this.visible2 = false
},
emitChange() {
// 获取到quill 根dom中的html
let html = this.articleForm.artContent
const quill = this.quill
const text = this.quill.getText()
if (html === '<p><br></p>') html = ''
// v-model相关
this.$emit('input', html)
this.$emit('change', { html, text, quill })
// 返回quill中文本长度
// bug注意:这个方法无法计算秀米代码的中的文字长度!
this.$emit('getConetntLength', this.quill.getLength())
},
// 回显内容时检查秀米代码
firstSetHtml() {
// value 为回显内容
if (this.value) {
// 判断是否有秀米和或135元素
if (
this.value.indexOf('xiumi.us') > -1 ||
this.value.indexOf('135editor.com') > -1
) {
const originNode = new DOMParser().parseFromString(
this.value,
'text/html'
).body.childNodes
this.nodesInQuill(originNode)
} else {
// 正常插入
this.quill.clipboard.dangerouslyPasteHTML(this.value)
}
}
},
// 根据node类型分发处理
nodesInQuill(originNode) {
for (let i = originNode.length - 1; i >= 0; i--) {
if (originNode[i].localName === 'section') {
// 秀米类型代码,走新blot
this.setRichText_xm(originNode[i].outerHTML, 0)
this.setRichText_135(originNode[i].outerHTML, 0)
} else {
// 正常插入
this.quill.clipboard.dangerouslyPasteHTML(
0,
originNode[i].outerHTML
)
}
}
},
// 监听粘贴板(请求接口把秀米图片本地化处理)
//注意!注意!注意!如果是本地复制粘贴的话,会走此方法,如果是线上直接点击秀米编辑器上的√ 直接导入,可以考虑在chang时贴入此段代码
listenPaste() {
var that = this
var imageArr = []
this.quill.root.addEventListener('paste', (e) => {
that.msg = (e.clipboardData || window.clipboardData).getData(
'text/html'
)
// //匹配图片
var imgReg = /<img.*?(?:>|\/>)/gi // eslint-disable-line
// //匹配src属性
var srcReg = /src=[\'\"]?([^\'\"]*)[\'\"]?/i // eslint-disable-line
if (that.msg) {
if (
that.msg.indexOf('xiumi.us') > -1 ||
that.msg.indexOf('_135editor') > -1
) {
that.msg.replace(imgReg, function (txt) {
return txt.replace(srcReg, function (src) {
var img_src = src.match(srcReg)[1]
//正则把?x-oss-process后面的都去掉
img_src = img_src.replace(/\?.*/i, '')
imageArr.push(img_src)
})
})
const parmas = {
urlList: imageArr,
}
if (imageArr.length != 0) {
// 如果有图片则 请求接口上传图片
uploadUrlImgs(parmas)
.then((res) => {
if (res.data && res.data.length) {
var index = 0
while (index < res.data.length) {
//接口返回图片根据index替换
that.msg = that.msg.replace(
imageArr[index],
res.data[index]
)
index++
that.$emit('change', that.msg)
// 富文本
const value = new DOMParser().parseFromString(
that.msg,
'text/html'
).body.childNodes // 获取nodes
e.preventDefault() // 阻止复制动作
e.stopPropagation() // 阻止冒泡
that.nodesInQuill(value) // 根据不同标签,使用不同的插入方法
}
} else {
// 富文本
const value1 = new DOMParser().parseFromString(
that.msg,
'text/html'
).body.childNodes // 获取nodes
e.preventDefault() // 阻止复制动作
e.stopPropagation() // 阻止冒泡
that.nodesInQuill(value1) // 根据不同标签,使用不同的插入方法
}
})
.catch(() => {})
} else {
// 富文本
const value1 = new DOMParser().parseFromString(
that.msg,
'text/html'
).body.childNodes // 获取nodes
e.preventDefault() // 阻止复制动作
e.stopPropagation() // 阻止冒泡
that.nodesInQuill(value1) // 根据不同标签,使用不同的插入方法
}
}
}
})
//以下是不需要请求接口,直接使用秀米的图片地址
// this.quill.root.addEventListener('paste', (e) => {
// let msg = (e.clipboardData || window.clipboardData).getData(
// 'text/html'
// ) // 获取粘贴板文本
// if (msg) {
// if (
// msg.indexOf('xiumi.us') > -1 ||
// msg.indexOf('_135editor') > -1
// ) {
// let value = new DOMParser().parseFromString(msg, 'text/html').body
// .childNodes // 获取nodes
// e.preventDefault() // 阻止复制动作
// e.stopPropagation() // 阻止冒泡
// this.nodesInQuill(value) // 根据不同标签,使用不同的插入方法
// }
// }
// })
},
// 自定义图标(秀米,135)
initButton() {
const sourceEditorButton = document.querySelector('.ql-insertMetric')
sourceEditorButton.innerHTML = `<button id="custom-button-xiumi" title="秀米" ></button>`
const sourceEditorButtonotEdit = document.querySelector('.ql-otEdit')
sourceEditorButtonotEdit.innerHTML = `<button id="custom-button-135" title="135编辑器" ></button>`
},
// 获取html内容
getHtml() {
return this.articleForm.artContent
},
}
}
</script>
5.css
<style lang="scss" scoped>
::v-deep(#custom-button-xiumi) {
background-size: contain;
background-repeat: no-repeat;
height: 16px;
width: 33px;
background-image: url('../../../assets/img/xiumi-connect-icon.png');
}
::v-deep(#custom-button-135) {
background-size: contain;
background-repeat: no-repeat;
height: 16px;
width: 33px;
background-image: url('../../../assets/img/editor-135-icon.png');
}
</style>
本文参考:https://www.freesion.com/article/97151190977/
https://blog.csdn.net/qq_41621896/article/details/121975513