kendo-vue-editor使用及踩坑记录
背景
公司买了kendoUI的license,项目中刚好需要使用富文本编辑器,大佬们决定使用kendo-vue-editor,(之前我接触比较多的是Quill)
使用方式
首先需要引入比较多的依赖:
"@progress/kendo-drawing": "^1.17.2"
"@progress/kendo-licensing": "^1.2.2",
"@progress/kendo-theme-default": "^5.12.0",
"@progress/kendo-vue-buttons": "^3.7.3",
"@progress/kendo-vue-dialogs": "^3.7.3",
"@progress/kendo-vue-dropdowns": "^3.7.3",
"@progress/kendo-vue-editor": "^3.7.3",
"@progress/kendo-vue-inputs": "^3.7.3",
"@progress/kendo-vue-intl": "^3.7.3",
"@progress/kendo-vue-layout": "^3.7.3",
"@progress/kendo-vue-pdf": "^3.7.3",
"@progress/kendo-vue-popup": "^3.7.3",
"@progress/kendo-vue-progressbars": "^3.7.3"
组件的基本引入
<template>
<Editor
:tools="tools"
default-edit-mode="div"
resizable
:default-content="content"
:content-style="{ height: '320px' }"
@change="onChange"
/>
</template>
<script setup lang="ts">
import { Editor } from '@progress/kendo-vue-editor'
import '@progress/kendo-theme-default/dist/all.css'
interface IEditorProps {
modelValue: string
}
const tools = [
['Undo', 'Redo'],
['FormatBlock', 'AlignLeft', 'AlignCenter', 'AlignRight'],
['Bold', 'Italic', 'Underline', 'Strikethrough', 'BackColor', 'ForeColor'],
['FontSize', 'FontName'],
['Subscript', 'Superscript'],
['Link', 'Unlink', 'InsertImage'],
['OrderedList', 'UnorderedList', 'Indent', 'Outdent'],
[
'InsertTable',
'MergeCells',
'SplitCell',
'AddRowBefore',
'AddRowAfter',
'AddColumnBefore',
'AddColumnAfter',
'DeleteRow',
'DeleteColumn',
'DeleteTable'
],
['ViewHtml']
]
const $props = defineProps<IEditorProps>()
const $emit = defineEmits(['update:modelValue'])
const content = ref('')
</script>
这样就可以得到1个基本的可以使用的富文本组件了
存在的问题
但是自测过程发现组件本身存在一些小bug,项目进度又不能耽误,自行解决这些bug,后面再提pr
- view html的弹窗右上角的×不触发弹窗关闭
- 解决方案:手动监听点击事件关闭
const onBtnClick = (e: any) => {
const classList = [...(e.target?.classList || [])]
if (classList.includes('k-i-x')) {
const cancelBtn = document.querySelector('.k-dialog-wrapper .k-actions-stretched .k-button-solid-base') as any
cancelBtn?.click()
}
}
onMounted(() => {
document.addEventListener('click', onBtnClick, true)
})
onUnmounted(() => {
document.removeEventListener('click', onBtnClick)
})
- 字体颜色和背景颜色tool在光标位置变化时跟着变化,始终是上次选择的颜色,并且无法支持随意选择颜色,只能选择预设的颜色
- 解决方案:用kendo自带的colorPicker实现自定义的colorPicker组件
const BgColorButton = {
render: () =>
h(ColorPicker, {
...EditorToolsSettings.backColor.colorPickerProps,
icon: 'background',
view: 'combo',
showPreview: false,
showClearButton: false,
value: bgcolorVal.value,
onChange: ({ value }: any) => {
setColor('background-color', 'BackColor', value)
}
})
}
const ForeColorButton = {
render: () =>
h(ColorPicker, {
...EditorToolsSettings.foreColor.colorPickerProps,
icon: 'foreground-color',
view: 'combo',
showPreview: false,
showClearButton: false,
value: colorVal.value,
onChange: ({ value }: any) => {
setColor('color', 'ForeColor', value)
}
})
}
const tools = [
...
['Bold', 'Italic', 'Underline', 'Strikethrough', BgColorButton, ForeColorButton],
...
]
const colorVal = ref('')
const bgcolorVal = ref('')
同时需要在每次光标发生变化做一些处理,这里监听execute
事件,再对color
做下处理
<template>
<Editor ref="editorRef" :tools="tools" default-edit-mode="div" resizable :default-content="content"
:content-style="{ height: '320px' }" :extend-view="extendView" @change="onChange" @execute="onExecute" />
</template>
<script setup lang="ts">
...
const onExecute = (event: any) => {
checkColor()
}
const checkColor = () => {
// 内部state是异步的,为了获取到最新的state,这里也异步获取下state
window.setTimeout(() => {
const editorView = editorRef.value?.view
const colorList = getInlineStyles(editorView?.state, {
name: 'color',
value: /^.+$/
})
const bgcolorList = getInlineStyles(editorView?.state, {
name: 'background-color',
value: /^.+$/
})
// 仅在被选中的文字都使用同1个颜色时才设置colorPicker的回显,否则重置颜色
colorVal.value = colorList.length === 1 ? colorList[0] : ''
bgcolorVal.value = bgcolorList.length === 1 ? bgcolorList[0] : ''
}, 0)
}
...
</script>
顺便一说,editor自带的color tool只能选择预置的颜色
要使用这种自定义颜色的话需要自定义color tools,如上文,即colorPicker组件
但这个组件仍有它自带的bug,点击cancel按钮不会关闭这个弹窗,仍是暂时手动监听事件来解决
const onBtnClick = (e: any) => {
if (e.target?.closest('.k-coloreditor-cancel')) {
const popupContainer = e.target?.closest('.k-animation-container')
popupContainer && (popupContainer.style.display = 'none')
}
}
onMounted(() => {
document.addEventListener('click', onBtnClick, true)
})
onUnmounted(() => {
document.removeEventListener('click', onBtnClick)
})
扩展
自定义图片上传
关于自定义图片上传可能需要回显一些值,这里可以使用extendView
<template>
<Editor ref="editorRef" :tools="tools" default-edit-mode="div" resizable :default-content="content"
:content-style="{ height: '320px' }" :extend-view="extendView" @change="onChange" @execute="onExecute" />
</template>
<script setup lang="ts">
...
const extendView = (event: any) => {
const { viewProps } = event;
const { plugins, schema } = viewProps.state;
const image = { ...schema.spec.nodes.get("image") };
image.attrs['data-id'] = { default: '' }
image.attrs['data-style'] = { default: '' }
image.attrs['data-value'] = { default: '' }
image.attrs['data-filename'] = { default: '' }
image.attrs['data-url'] = { default: '' }
const nodes = schema.spec.nodes.update("image", image);
const marks = schema.spec.marks
const mySchema = new Schema({
nodes,
marks
})
const doc = EditorUtils.createDocument(mySchema, content.value);
return new EditorView(
{
mount: event.dom,
},
{
...event.viewProps,
state: EditorState.create({
doc: doc,
plugins,
}),
}
)
}
// 插入图片
const onSaveImage = (setting: IImageSetting) => {
if (!setting.url && !setting.media) {
return
}
const editorView = editorRef.value?.view
if (editorView) {
const node = editorView?.state?.schema?.nodes.image?.createAndFill({
src: setting.url || setting.media?.value,
style: getStyleAttribute(setting.style),
alt: setting.alt || '',
'data-id': setting.media?.id || '',
'data-style': setting.style || '',
'data-value': setting.media?.value || '',
'data-filename': setting.media?.fileName || '',
'data-url': setting.url || '',
})
const tr = editorView?.state.tr.replaceSelectionWith(node)
editorView?.dispatch(tr.setMeta('commandName', 'InsertImage'))
}
}
const onExecute = (event: any) => {
imageSetting.value = '{}'
const { doc, selection } = event.transaction;
// 图片的话selection.empty一定为false
if (!selection.empty) {
const node = doc.cut(selection.from, selection.to);
const selectionHtml = EditorUtils.getHtml({
doc: node,
schema: node.type.schema,
});
checkImg(selectionHtml)
}
}
// 判断当前光标所在位置是图片
const checkImg = (htmlString: string) => {
if (!htmlString) {
return
}
const isImg = /<img.*?src=[\"|\']?(.*?)[\"|\']?\s.*?>/i.test(htmlString)
if (!isImg) {
return
}
try {
const domParser = new DOMParser();
const dom = domParser.parseFromString(htmlString, 'text/html')
const imgNode = dom.querySelector('img')
if (imgNode) {
const style = imgNode.getAttribute('data-style')
const id = imgNode.getAttribute('data-id')
const value = imgNode.getAttribute('data-value')
const filename = imgNode.getAttribute('data-filename')
const url = imgNode.getAttribute('data-url')
const alt = imgNode.getAttribute('alt')
imageSetting.value = JSON.stringify({
style,
url,
alt,
media: {
id,
value,
filename
}
})
}
} catch (error) {
console.log('domParser error', error)
}
}
...
</script>
建议
至此一些奇怪bug算是解决掉了~ 如果只想使用富文本的话,我觉得其实有其他更好的选择,比如tiptap,tinymce(12.5k stars),以及近几年国产开源的wangEditor)。
转载自:https://juejin.cn/post/7194676713811345468