yarn add @wangeditor/editor
# 或者 npm install @wangeditor/editor --save
yarn add @wangeditor/editor-for-react
# 或者 npm install @wangeditor/editor-for-react --save
import '@wangeditor/editor/dist/css/style.css' // 引入 css
import { useState, useEffect } from 'react'
import { Editor, Toolbar } from '@wangeditor/editor-for-react'
import { IDomEditor, IEditorConfig, IToolbarConfig } from '@wangeditor/editor'
type InsertImgType = (url: string, alt: string, href: string) => void;
type InsertVideoType = (url: string, poster?: string) => void;
const MyEditor: FunctionComponent = () => {
// editor 实例
const [editor, setEditor] = useState<IDomEditor | null>(null);
// 编辑器内容
const [html, setHtml] = useState('hello
')
// 模拟 ajax 请求,异步设置 html
useEffect(() => {
setTimeout(() => {
setHtml('hello world
')
}, 1500)
}, [])
// 工具栏配置
const toolbarConfig: Partial<IToolbarConfig> = {
excludeKeys: ['group-video']
};
// 编辑器配置
const editorConfig: Partial<IEditorConfig> = {
placeholder: '请输入内容...',
readOnly: false,
MENU_CONF: {
uploadImage: {
// 自定义上传 -- 图片
customUpload: (file: File, insertFn: InsertImgType) => {
if(file.type.startsWith('image/')) {
// file 即选中的文件
// 自己实现上传,并得到图片 url alt href
// 最后插入图片
insertFn(url, alt, href)
} else {
// 错误提示
}
}
},
uploadVideo: {
// 自定义上传 -- 视频
customUpload: (file: File, insertFn: InsertVideoType) => {
// file 即选中的文件
// 自己实现上传,并得到视频 url poster
// 最后插入视频
insertFn(url, poster)
}
}
}
}
useEffect(() => {
// 修改弹窗位置为编译器居中
editor?.on('modalOrPanelShow', modalOrPanel => {
if (modalOrPanel.type !== 'modal') return
const { $elem } = modalOrPanel; // modal element
const width = $elem.width();
const height = $elem.height();
// set modal position z-index
$elem.css({
left: '50%',
top: '50%',
bottom: 'auto', // 需要修改底部间距,不然会受组件自身计算影响
marginLeft: `-${width / 2}px`,
marginTop: `-${height / 2}px`,
zIndex: 1000
});
});
// 及时销毁 editor ,重要!
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={editor => setHtml(editor.getHtml())}
mode="default"
style={{ height: '500px', overflowY: 'hidden' }}
/>
</div>
</>
)
}
export default MyEditor;
import { DomEditor, IDomEditor, IModalMenu, SlateNode, SlateTransforms, t } from '@wangeditor/editor';
import { DOMElement } from '@wangeditor/editor/dist/editor/src/utils/dom';
import { genModalButtonElems, genModalInputElems } from './utils';
class EditImageSize implements IModalMenu {
showModal: boolean;
modalWidth: number;
title: string;
iconSvg?: string;
hotkey?: string;
alwaysEnable?: boolean;
tag: string;
width?: number;
private $content: DOMElement | null = null;
private getSelectedImageNode(editor: IDomEditor): SlateNode | null {
return DomEditor.getSelectedNodeByType(editor, 'image')
}
constructor() {
this.title = t('videoModule.editSize');
// this.iconSvg = '';
this.tag = 'button';
this.showModal = true;
this.modalWidth = 320;
}
// 菜单是否需要激活(如选中加粗文本,“加粗”菜单会激活),用不到则返回 false
isActive(): boolean {
// 任何时候,都不用激活 menu
return false
}
// 获取菜单执行时的 value ,用不到则返回空 字符串或 false
getValue(): string | boolean {
// 插入菜单,不需要 value
return ''
}
// 菜单是否需要禁用(如选中 H1 ,“引用”菜单被禁用),用不到则返回 false
isDisabled(editor: IDomEditor): boolean {
if (editor.selection == null) return true
const videoNode = this.getSelectedImageNode(editor)
if (videoNode == null) {
// 选区未处于 image node ,则禁用
return true
}
return false
}
// 点击菜单时触发的函数
exec() {
// 点击菜单时,弹出 modal 之前,不需要执行其他代码
// 此处空着即可
}
// 弹出框 modal 的定位:1. 返回某一个 SlateNode; 2. 返回 null (根据当前选区自动定位)
getModalPositionNode(editor: IDomEditor): SlateNode | null {
return this.getSelectedImageNode(editor);
}
// 定义 modal 内部的 DOM Element
getModalContentElem(editor: IDomEditor): DOMElement {
const $content = this.$content || document.createElement('div');
const [inputWidthContainerElem, inputWidthElem] = genModalInputElems(
t('videoModule.width'),
`input-width-${Math.random().toString(36).slice(2)}`,
'auto'
);
const [inputHeightContainerElem, inputHeightElem] = genModalInputElems(
t('videoModule.height'),
`input-height-${Math.random().toString(36).slice(2)}`,
'auto'
);
const buttonContainerElem = genModalButtonElems(
`button-${Math.random().toString(36).slice(2)}`,
t('videoModule.ok')
);
$content.append(inputWidthContainerElem);
$content.append(inputHeightContainerElem);
$content.append(buttonContainerElem);
const imageNode = this.getSelectedImageNode(editor) as unknown as HTMLElement;
// 绑定事件(第一次渲染时绑定,不要重复绑定)
if (this.$content == null) {
buttonContainerElem.onclick = () => {
const width = Number(inputWidthElem.value);
const height = Number(inputHeightElem.value);
console.log(editor, isNaN(width) ? inputWidthElem.value : width ? width +'px' : 'auto', isNaN(height) ? inputHeightElem.value : height ? height +'px' : 'auto')
editor.restoreSelection();
// 修改尺寸
SlateTransforms.setNodes(
editor,
{
style: {
width: isNaN(width) ? inputWidthElem.value : width ? width +'px' : 'auto',
height: isNaN(height) ? inputHeightElem.value : height ? height +'px' : 'auto',
}
} as any,
{
match: n => DomEditor.checkNodeType(n, 'image'),
}
)
editor.hidePanelOrModal(); // 隐藏 modal
}
}
if (imageNode == null) return $content;
// 初始化 input 值
const { width = 'auto', height = 'auto' } = imageNode.style;
inputWidthElem.value = width || 'auto';
inputHeightElem.value = height || 'auto';
setTimeout(() => {
inputWidthElem.focus()
});
return $content // 返回 DOM Element 类型
// PS:也可以把 $content 缓存下来,这样不用每次重复创建、重复绑定事件,优化性能
}
}
export const EditImageSizeConf = {
key: 'editImageSize', // 定义 menu key :要保证唯一、不重复(重要)
factory() {
return new EditImageSize() // 把 `YourMenuClass` 替换为你菜单的 class
},
}
公用工具utils
// 生成输入框
export const genModalInputElems = (label: string, id: string, val: string): [HTMLLabelElement, HTMLInputElement] => {
const $label = document.createElement('label');
$label.className = 'babel-container';
const $span = document.createElement('span');
$span.textContent = label;
const $input = document.createElement('input');
$input.type = 'text';
$input.id = id;
$input.value = val;
$label.append($span);
$label.append($input);
return [$label, $input];
};
// 生成按钮
export const genModalButtonElems = (id: string, text: string) => {
const $content = document.createElement('div');
$content.className = 'button-container';
const $button = document.createElement('button');
$button.id = id;
$button.textContent = text;
$content.append($button);
return $content;
};
// 注册自定义菜单
useEffect(() => {
try {
Boot.registerMenu(EditImageSizeConf);
} catch (e) {}
}, [])
// 工具栏配置
const toolbarConfig: Partial<IToolbarConfig> = {
insertKeys: {
index: 5, // 插入的位置,基于当前的 toolbarKeys
keys: ['editImageSize']
}
}
// 编辑器配置
const editorConfig: Partial<IEditorConfig> = {
hoverbarKeys: {
image: {
menuKeys: ['editImageSize'] // 注意:要保留原有的菜单需加上之前的菜单key
}
}
}