背景
最近一个朋友有一个需求需要在wangEditor
中做一个提及的效果交互,她自己找了个Demo,但是在Vue中实现的,因为她那边一直用的是React,想把Vue那一版转为React的,但对Vue不太了解,找我帮忙,我自然是欢快的答应了,经过三个小时的文档查阅及编码最终实现
这个呢有需要的朋友也可以直接 copy index.js 和 ModalList 组件
注意: ModalList 跟随光标出现而出现 位置可以自己微调下, 这个位置我这边用自己电脑和公司电脑展示的不一样,大家使用时候微调下即可
代码更改这里即可:
js
const topPosition = `${selectionRect.top - containerRect.top + window.scrollY + 60}px`;
const leftPosition = `${selectionRect.left - containerRect.left + window.scrollX + 5}px`;
这边查阅的文档大概是 王福朋老师 做的开源wangEditor
及开源插件
因为是要用React做,这边直接贴一个对应React的文档
- 文档 React-wangEditor
- 插件集
(一)、面试中遇到的笔试题
(二)、面试中遇到的读代码题
(三)、Typescript基础篇
(四)、TS核心语法+各种实战应用(上)
(五)、TS核心语法+各种实战应用(下)
1. @提及实现
1.1 效果
1.2 实现
index.js
js
import "@wangeditor/editor/dist/css/style.css"; // 引入 css
import React, { useState, useEffect } from "react";
import { Editor, Toolbar } from "@wangeditor/editor-for-react";
import { Boot, DomEditor, i18nChangeLanguage } from "@wangeditor/editor";
import mentionModule from "@wangeditor/plugin-mention";
import markdownModule from '@wangeditor/plugin-md';
import attachmentModule from '@wangeditor/plugin-upload-attachment';
import ctrlEnterModule from '@wangeditor/plugin-ctrl-enter'
import ModalList from "./ModalList";
// 国际化 默认中文 使用引文请将 zh-CN 更改为 en
i18nChangeLanguage('zh-CN');
// 注册。要在创建编辑器之前注册,且只能注册一次,不可重复注册。
Boot.registerModule(mentionModule); // 提及
Boot.registerModule(markdownModule); // markdown
Boot.registerModule(attachmentModule); // 附件
Boot.registerModule(ctrlEnterModule); // ctrl+enter 换行
function MyEditor() {
const [editor, setEditor] = useState(null); // editor 实例
const [html, setHtml] = useState(""); // 编辑器内容
const [isModalVisible, setIsModalVisible] = useState(false); // Modal 显示隐藏
const [topPosition, setTopPosition] = useState('');
const [leftPosition, setLeftPosition] = useState('');
// 当前菜单排序和分组
const toolbar = DomEditor.getToolbar(editor);
const curToolbarConfig = toolbar?.getConfig();
console.log(curToolbarConfig?.toolbarKeys);
// 关于工具栏配置
const toolbarConfig = {
// 插入哪些菜单
insertKeys: {
index: 9, // 自定义插入的位置
keys: ['uploadAttachment'], // "上传附件"菜单
},
};
// 编辑器配置
const editorConfig = {
placeholder: "请输入内容...",
EXTEND_CONF: {
mentionConfig: {
showModal, // 必须有
hideModal, // 必须有
},
},
// 在编辑器中,点击选中"附件"节点时,要弹出的菜单
hoverbarKeys: {
attachment: {
menuKeys: ['downloadAttachment'], // "下载附件"菜单
},
},
MENU_CONF: {
// "上传附件"菜单的配置
uploadAttachment: {
server: '/api/upload', // 服务端地址
timeout: 5 * 1000, // 5s
fieldName: 'custom-fileName',
meta: { token: 'xxx', a: 100 }, // 请求时附加的数据
metaWithUrl: true, // meta 拼接到 url 上
headers: { Accept: 'text/x-json' },
maxFileSize: 10 * 1024 * 1024, // 10M
// onBeforeUpload(file) {
// console.log('onBeforeUpload', file)
// return file // 上传 file 文件
// // return false // 会阻止上传
// },
// // 上传进度
// onProgress(progress) {
// console.log('onProgress', progress)
// },
// // 成功之后
// onSuccess(file, res) {
// console.log('onSuccess', file, res)
// },
// onFailed(file, res) {
// alert(res.message)
// console.log('onFailed', file, res)
// },
// // 错误信息
// onError(file, err, res) {
// alert(err.message)
// console.error('onError', file, err, res)
// },
// // 上传成功后,用户自定义插入文件
// customInsert(res, file, insertFn) {
// console.log('customInsert', res)
// const { url } = res.data || {}
// if (!url) throw new Error(`url is empty`)
// // 插入附件到编辑器
// insertFn(`customInsert-${file.name}`, url)
// },
// // 用户自定义上传
// customUpload(file, insertFn) {
// console.log('customUpload', file)
// return new Promise(resolve => {
// // 插入一个文件,模拟异步
// setTimeout(() => {
// const src = `https://www.w3school.com.cn/i/movie.ogg`
// insertFn(`customUpload-${file.name}`, src)
// resolve('ok')
// }, 500)
// })
// },
// // 自定义选择
// customBrowseAndUpload(insertFn) {
// alert('自定义选择文件,如弹出图床')
// // 自己上传文件
// // 上传之后用 insertFn(fileName, link) 插入到编辑器
// },
// // 插入到编辑器后的回调
// onInsertedAttachment(elem) {
// console.log('inserted attachment', elem)
// },
}
}
};
// 隐藏弹窗
function hideModal() {
setIsModalVisible(false); // 隐藏 modal
}
// 显示弹框
function showModal(editor) {
// 获取光标位置,定位 modal
const domSelection = document.getSelection();
const domRange = domSelection.getRangeAt(0);
if (domRange == null) return;
const selectionRect = domRange.getBoundingClientRect();
// 获取编辑区域 DOM 节点的位置,以辅助定位
const containerRect = editor.getEditableContainer().getBoundingClientRect();
const topPosition = `${selectionRect.top - containerRect.top + window.scrollY + 60}px`;
const leftPosition = `${selectionRect.left - containerRect.left + window.scrollX + 5}px`;
// 显示 modal 弹框,并定位
// PS:modal 需要自定义,如 <div> 或 Vue React 组件
setIsModalVisible(true);
// 当触发某事件(如点击一个按钮)时,插入 mention 节点
console.log(selectionRect, containerRect, "展示");
// 更新弹框位置
setTopPosition(topPosition);
setLeftPosition(leftPosition);
}
// @功能 as 提及 当触发某事件(如点击一个按钮)时,插入 mention 节点
function insertMention(id, name) {
const mentionNode = {
type: 'mention', // 必须是 'mention'
value: name, // 文本
info: { id }, // 其他信息,自定义
children: [{ text: '' }], // 必须有一个空 text 作为 children
}
editor.restoreSelection() // 恢复选区
editor.deleteBackward('character') // 删除 '@'
editor.insertNode(mentionNode) // 插入 mention
editor.move(1) // 移动光标
}
// 编辑器中输入内容
const onChangeEditor = (editor) => {
setHtml(editor.getHtml())
}
// 及时销毁 editor ,重要!
useEffect(() => {
return () => {
if (editor == null) return
editor.destroy()
setEditor(null)
}
}, [editor])
return (
<div style={{ border: '1px solid #ccc', zIndex: 100}}>
<Toolbar
editor={editor}
defaultConfig={toolbarConfig}
mode="default"
style={{ borderBottom: '1px solid #ccc' }}
/>
<Editor
defaultConfig={editorConfig}
value={html}
onCreated={setEditor}
onChange={onChangeEditor}
mode="default"
style={{ height: '500px', overflowY: 'hidden' }}
/>
{
isModalVisible && (
<ModalList
insertMention={insertMention}
hideModal={hideModal}
topPosition={topPosition}
leftPosition={leftPosition} />
)
}
</div>
)
}
export default MyEditor
1. ModalList.js
js
import React, { useEffect, useRef, useState } from 'react';
import { Select } from 'antd';
const { Option } = Select;
export default function ModalList(props) {
const { topPosition, leftPosition } = props;
const selectRef = useRef();
const [personList, setPersonList] = useState([]);
useEffect(() => {
// TODO: 这里可以发起请求 一个List
selectRef.current.focus();
setPersonList([
{ id: 1, name: 'John' },
{ id: 2, name: 'Tom' },
{ id: 3, name: 'Jack' },
{ id: 4, name: 'Marry' },
])
}, []);
const onChangeSelect = (e) => {
let name = personList.find(item => item.id === e);
props.insertMention(e, name.name);
props.hideModal();
};
return (
<Select
ref={selectRef}
showSearch
allowClear
placeholder="要@的人"
style={{ width: '150px', position: 'absolute', top: topPosition, left: leftPosition }}
optionFilterProp="children"
filterOption={(input, option) => option?.children?.toLowerCase().indexOf(input?.toLowerCase()) >= 0}
filterSort={(optionA, optionB) => {
return optionA?.children?.toLowerCase().localeCompare(optionB?.children?.toLowerCase());
}}
onChange={onChangeSelect}
>
{
personList.map(item => {
return (
<Option key={item.id} value={item.id}>
{item.name}
</Option>
);
})
}
</Select>
);
}
2. markdown语法
2.1. 效果
2.2. 实现
typescript
安装
tnpm i @wangeditor/plugin-md
导入
import markdownModule from '@wangeditor/plugin-md'
注册
Boot.registerModule(markdownModule); // markdown
2.3. md使用语法
1.标题
js
# 一级标题 输入# 按一个空格 输入标题
##
###
####
#####
2.列表
js
- + *
3.引用
js
>
4.分割线
js
---
5.代码块
js
``` 按下回车
3. 附件
3.1. 效果
3.2. 实现
js
安装 tnpm i @wangeditor/plugin-md
导入 import markdownModule from '@wangeditor/plugin-upload-attachment'
注册 Boot.registerModule(attachmentModule); // 附件
// 在编辑器中配置
const editorConfig = {
// 在编辑器中,点击选中"附件"节点时,要弹出的菜单
hoverbarKeys: {
attachment: {
menuKeys: ['downloadAttachment'], // "下载附件"菜单
},
},
MENU_CONF: {
// "上传附件"菜单的配置
uploadAttachment: {
server: '/api/upload', // 服务端地址
timeout: 5 * 1000, // 5s
fieldName: 'custom-fileName',
meta: { token: 'xxx', a: 100 }, // 请求时附加的数据
metaWithUrl: true, // meta 拼接到 url 上
headers: { Accept: 'text/x-json' },
maxFileSize: 10 * 1024 * 1024, // 10M
onBeforeUpload(file) {
console.log('onBeforeUpload', file)
return file // 上传 file 文件
// return false // 会阻止上传
},
// 上传进度
onProgress(progress) {
console.log('onProgress', progress)
},
// 成功之后
onSuccess(file, res) {
console.log('onSuccess', file, res)
},
onFailed(file, res) {
alert(res.message)
console.log('onFailed', file, res)
},
// 错误信息
onError(file, err, res) {
alert(err.message)
console.error('onError', file, err, res)
},
// 上传成功后,用户自定义插入文件
customInsert(res, file, insertFn) {
console.log('customInsert', res)
const { url } = res.data || {}
if (!url) throw new Error(`url is empty`)
// 插入附件到编辑器
insertFn(`customInsert-${file.name}`, url)
},
// 用户自定义上传
customUpload(file, insertFn) {
console.log('customUpload', file)
return new Promise(resolve => {
// 插入一个文件,模拟异步
setTimeout(() => {
const src = `https://www.w3school.com.cn/i/movie.ogg`
insertFn(`customUpload-${file.name}`, src)
resolve('ok')
}, 500)
})
},
// 自定义选择
customBrowseAndUpload(insertFn) {
alert('自定义选择文件,如弹出图床')
// 自己上传文件
// 上传之后用 insertFn(fileName, link) 插入到编辑器
},
// 插入到编辑器后的回调
onInsertedAttachment(elem) {
console.log('inserted attachment', elem)
},
}
}
};
4. 国际化
4.1. 实现
js
安装
tnpm i @wangeditor/plugin-md
导入
import { i18nChangeLanguage } from "@wangeditor/editor";
使用
i18nChangeLanguage('zh-CN');
5. ctrl/command + enter换行
5.1. 实现
js
1.安装
tnpm i @wangeditor/plugin-ctrl-enter
2.导入
import ctrlEnterModule from '@wangeditor/plugin-ctrl-enter'
3.注册
Boot.registerModule(ctrlEnterModule); // ctrl+enter 换行