Skip to content

Commit

Permalink
✨ feat(富文本组件naive-ui-weditor): 封装
Browse files Browse the repository at this point in the history
  • Loading branch information
zyuting committed Dec 5, 2023
1 parent ea65ace commit 0b35e38
Show file tree
Hide file tree
Showing 11 changed files with 2,754 additions and 256 deletions.
9 changes: 8 additions & 1 deletion app/src/views/Editor.vue
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
<template>
<NaiveUiEditor></NaiveUiEditor>
<NaiveUiEditor v-model:value="content"></NaiveUiEditor>
</template>

<script setup lang="ts">
import { ref, watch } from 'vue'
import { NaiveUiEditor } from 'naive-ui-editor'
const content = ref('')
watch(content, () => {
console.log(content.value)
})
</script>

<style scoped></style>
13 changes: 13 additions & 0 deletions components/naive-ui-editor/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,16 @@
import NaiveUiEditor from './src/NaiveUiEditor.vue'

import type { App } from 'vue'
import type { Props, RequestFun } from './src/types/index'

export type { Props, RequestFun }
export { NaiveUiEditor }

export default {
install(app: App, option?: Props) {
app.component('NaiveUiEditor', NaiveUiEditor)
if (option?.requestFunc) {
app.provide('requestFunc', option.requestFunc)
}
}
}
25 changes: 23 additions & 2 deletions components/naive-ui-editor/package.json
Original file line number Diff line number Diff line change
@@ -1,12 +1,20 @@
{
"name": "naive-ui-editor",
"version": "0.0.0",
"version": "1.0.0",
"private": true,
"main": "index.ts",
"type": "module",
"description": "基于naive-ui和wangEditor 5封装的富文本组件",
"keywords": [
"naive-ui",
"vue3",
"wangEditor 5"
],
"scripts": {
"dev": "vite",
"build": "run-p type-check \"build-only {@}\" --",
"preview": "vite preview",
"test:unit": "vitest",
"build-only": "vite build",
"type-check": "vue-tsc --noEmit -p tsconfig.app.json --composite false"
},
Expand All @@ -15,13 +23,26 @@
},
"devDependencies": {
"@tsconfig/node18": "^18.2.2",
"@types/jsdom": "^21.1.3",
"@types/node": "^18.18.5",
"@vitejs/plugin-vue": "^4.4.0",
"@vitejs/plugin-vue-jsx": "^3.0.2",
"@vue/test-utils": "^2.4.1",
"@vue/tsconfig": "^0.4.0",
"jsdom": "^22.1.0",
"npm-run-all2": "^6.1.1",
"typescript": "~5.2.0",
"vite": "^4.4.11",
"vitest": "^0.34.6",
"vue-tsc": "^1.8.19"
}
},
"peerDependencies": {
"@wangeditor/editor": ">=5.1.23",
"@wangeditor/editor-for-vue": ">=5.1.12",
"naive-ui": ">=2.34.0",
"vue": ">=3.2.0"
},
"files": [
"dist"
]
}
54 changes: 49 additions & 5 deletions components/naive-ui-editor/src/NaiveUiEditor.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,51 @@
<template>
<div>editor</div>
</template>
<script lang="ts" setup>
import { NSpin } from 'naive-ui'
import '@wangeditor/editor/dist/css/style.css'
import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
import { useEditor } from './hooks'
import type { Props, Emits } from './types'
<script setup lang="ts"></script>
const props = withDefaults(defineProps<Props>(), {
mode: 'default',
height: 500,
editorConfig: {
placeholder: '请输入内容...',
MENU_CONF: {},
}
})
<style scoped></style>
const emits = defineEmits<Emits>()
const {
loading,
editorRef,
style,
customConfig,
customPaste,
handleCreated,
handleChange
} = useEditor({ props, emits })
</script>

<template>
<n-spin :show="loading">
<Toolbar
:editor="editorRef"
:mode="mode"
:defaultConfig="toolbarConfig"
style="border-bottom: 1px solid #ccc"
/>
<Editor
class="editor-content-view"
:defaultConfig="customConfig"
:mode="mode"
:modelValue="value || ''"
:style="{ height: height + 'px', overflowY: 'hidden' }"
@customPaste="customPaste"
@onCreated="handleCreated"
@onChange="handleChange"
/>
</n-spin>
</template>
58 changes: 58 additions & 0 deletions components/naive-ui-editor/src/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { ref, shallowRef, inject, computed, onBeforeUnmount } from 'vue'
import { useFile } from './useFile'

import type { Props, Emits, RequestFun } from '../types'

export const useEditor = ({ props, emits }: {
props: Props,
emits: Emits
}) => {
const loading = ref(false)
const editorRef = shallowRef()

const injectRequestFunc = inject<RequestFun | undefined>('requestFunc', undefined)
const requestFunc = props.requestFunc ?? injectRequestFunc

if (!requestFunc) {
throw new Error('requestFunc is required')
}

const { getElementLen, formatConfig, customPaste } = useFile({ loading, requestFunc })

const style = computed(() => ({ height: props.height + 'px', overflowY: 'hidden' }))
const customConfig = computed(() => formatConfig(props.editorConfig))

// 编辑器回调函数
const handleCreated = (editor) => {
editorRef.value = editor; // 记录 editor 实例
};

// 编辑器change
const handleChange = (editor) => {
const isEmpty =
editor.isEmpty() ||
(!editor
.getText()
.replace(/[\r\n]/g, '')
.replace(/&nbsp;/gi, '')
.trim() &&
!getElementLen(editor));
emits('update:value', isEmpty ? null : editor.getHtml());
};

// 组件销毁时,及时销毁编辑器
onBeforeUnmount(() => {
editorRef.value?.destroy();
});

return {
loading,
editorRef,
style,
customConfig,
customPaste,
handleCreated,
handleChange,
}
}

123 changes: 123 additions & 0 deletions components/naive-ui-editor/src/hooks/useFile.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import { findImgFromHtml, extractImgFromRtf, transAsImgToFile, replaceAsUpdSrc } from '../utils/file'
import to from 'await-to-js'

import type { Ref } from 'vue'
import type { InsertFnType, IEditorConfig } from '@wangeditor/editor';
import type { RequestFun } from '../types'

/**
* @name 富文本文件相关处理方法
* @param loading
* @param requestFunc
* @returns
*/
export const useFile = ({
loading,
requestFunc,
}: {
loading: Ref<boolean>,
requestFunc: RequestFun,
}) => {
/**
* 获取图片、视频、链接标签数
* @param editor
* @returns
*/
const getElementLen = (editor) => {
return ['image', 'link', 'video']
.map((tag) => editor.getElemsByType(tag).length)
.reduce((a, b) => a + b, 0);
};

/**
* 自定义上传
*/
const customUpload = async (file: File, insertFn?: InsertFnType) => {
const [err, url] = await to(requestFunc(file));
if (err) return '';
insertFn && insertFn(url, file.name);
return url;
};

/**
* 自定义配置
*/
const formatConfig = (editorConfig: Partial<IEditorConfig>) => {
const config = { ...(editorConfig || {}) }
config.MENU_CONF = {
uploadImage: {
customUpload,
},
...(config.MENU_CONF || {})
}
return config
}

/**
* 自定义复制粘贴
* @description 获取word里的图片上传到服务器并替换图片地址
* @param editor
* @param event
* @returns
*/
const customPaste = async (editor, event) => {
// 获取粘贴的html部分,该部分包含了图片img标签
let html = event.clipboardData.getData('text/html');

// 获取rtf数据(从word、wps复制粘贴时有),复制粘贴过程中图片的数据就保存在rtf中
const rtf = event.clipboardData.getData('text/rtf');

if (html && rtf) {
// 该条件分支即表示要自定义word粘贴

// 列表缩进会超出边框,直接过滤掉
html = html.replace(/text\-indent:\-(.*?)pt/gi, '');

// 从html内容中查找粘贴内容中是否有图片元素,并返回img标签的属性src值的集合
const imgSrcs = findImgFromHtml(html);

// 如果有
if (imgSrcs.length) {
// 从rtf内容中查找图片数据
const rtfImageData = extractImgFromRtf(rtf);
if (rtfImageData.length) {
// 阻止默认的粘贴行为
event.preventDefault();
try {
loading.value = true;
// 将图片转为file上传
const imgs = transAsImgToFile(imgSrcs, rtfImageData);
const urls = await Promise.all(imgs.map((file) => requestFunc(file)));
// 替换为上传后的url
html = replaceAsUpdSrc(html, imgSrcs, rtfImageData, urls);
editor.dangerouslyInsertHtml(html);
return Promise.resolve();
} catch (e) {
return Promise.reject(e);
} finally {
loading.value = false;
}
}
}
return false;
} else {
// 从html内容中查找粘贴内容中是否有图片元素,并返回img标签的属性src值的集合
const imgSrcs = findImgFromHtml(html);
// 清除html当中的img标签(单独复制某一张图片没问题)
if (imgSrcs.length && !html.includes('StartFragment--><img')) {
// 阻止默认的粘贴行为
event.preventDefault();
html = html.replace(/<img [^>]*src=['"]([^'"]+)[^>]*>/g, '');
editor.dangerouslyInsertHtml(html);
return false;
}
return true;
}
};

return {
getElementLen,
formatConfig,
customPaste,
}
}
21 changes: 21 additions & 0 deletions components/naive-ui-editor/src/types/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import type { PropType, ExtractPropTypes } from 'vue';
import type { IEditorConfig, IToolbarConfig } from '@wangeditor/editor';

export interface Props {
value?: string
mode?: 'default' | 'simple'
height?: number
editorConfig?: Partial<IEditorConfig>
toolbarConfig?: Partial<IToolbarConfig>
requestFunc?: RequestFun
}
export type Emits = {
(e: 'update:value', value: string): void
}

export type Recordable<T = any> = Record<string, T>

export type RequestFun = (
file: File,
onProgerss?: (e: { percent: number }) => void
) => Promise<string>
Loading

0 comments on commit 0b35e38

Please sign in to comment.