背景
最近公司项目有个新需求,就是富文本编辑器要支持文件上传功能,然后上传之后以超链接的形式显示,像这样:
公司项目用的react,之前一直用的是react-draft-wysiwyg作为富文本编辑器。如果重新再找编辑器那投入和产出比就失衡了。so,最好的方式就是改造编辑器了。
开始
先说说react-draft-wysiwyg,它算是目前最火的react的富文本编辑器,它基于draft-js来开发。长这样。
draft-js是react 富文本编辑器框架,注意,它是一个框架,感兴趣的朋友可以研究下,然后开发属于自己的编辑器,它提供丰富的api供你调用。
fork react-draft-wysiwyg项目&clone 到本地
项目拉下来之后,我们首先看看package.json
,看它怎么运行:
"scripts": {
"clean": "rimraf dist",
"build:webpack": "cross-env NODE_ENV=production webpack --config config/webpack.config.js",
"build": "npm run clean && npm run build:webpack",
"test": "cross-env BABEL_ENV=test mocha --compilers js:config/test-compiler.js config/test-setup.js src/**/*Test.js",
"lint": "eslint src",
"lintdocs": "eslint docs/src",
"flow": "flow; test $? -eq 0 -o $? -eq 2",
"check": "npm run lint && npm run flow",
"storybook": "start-storybook -p 6006",
"build-storybook": "build-storybook"
},
复制代码
可以看到它是通过storybook
来运行的,so npm install && npm run storybook
。运行之后访问http://localhost:6006
。我们可以看到它是编辑器的demo集合,找了一通确实没有文件上传功能。
// src 文档结构
├── components
| ├── Dropdown
| ├── Option
| └── Spinner
├── config
| └── defaultToolbar.js // 功能按钮配置
├── controls // 放置组件的目录
| ├── BlockType
| ├── Clear
| ├── ColorPicker
| ├── Embedded
| ├── Emoji
| ├── FileUpload
| ├── FontFamily
| ├── FontSize
| ├── History
| ├── Image
| ├── index.js
| ├── Inline
| ├── Link
| ├── List
| ├── Remove
| └── TextAlign
├── Editor
| ├── index.js // 编辑器入口
| ├── styles.css
| └── __test__
├── i18n // 国际化
| ├── da.js
| ├── de.js
| ├── en.js
| ├── es.js
| ├── fr.js
| ├── index.js
| ├── it.js
| ├── ja.js
| ├── ko.js
| ├── nl.js
| ├── pl.js
| ├── pt.js
| ├── ru.js
| ├── zh.js
| └── zh_tw.js
├── index.js
├── renderer
| ├── Embedded
| ├── Image
| └── index.js
└── utils
├── BlockStyle.js
├── common.js
├── handlePaste.js
└── toolbar.js
复制代码
第一步 toolbar加个附件上传按钮:
查看代码可以看到配置toolbar的地方在src/config/defaultToolbar.js
export default {
options: [// 配置可见的按钮
...
"image",
"fileUpload",//我们在这里加一个fileUpload,代表文件上传
],
// 下面的属性都是每个工具的具体的具体配置,如image:
image: {
icon: image, // 按钮图标
className: undefined,
component: undefined,
popupClassName: undefined,
urlEnabled: false,// 是否支持url形式引入
uploadEnabled: true,// 是否支持上传
previewImage: false,// 是否支持预览图片
alignmentEnabled: true,// 是否显示排列按钮(相当于配置text-align)
uploadCallback: undefined,// 上传的回调,这个是重点
inputAccept: undefined,// 文件格式(这里只适合写图片格式)
alt: { present: false, mandatory: false },
defaultSize: {
height: "auto",
width: "auto"
},
title: undefined // html title属性
}
...
// 相当于图片上传,我们的文件上传有诸多类似。所以我们可以参照着配置一下
fileUpload: {
icon: fileUpload,
className: undefined,
component: undefined,
popupClassName: undefined,
urlEnabled: false,
uploadEnabled: true,
previewFileUpload: true,
alignmentEnabled: true,
uploadCallback: function() {},
inputAccept: undefined,
alt: { present: false, mandatory: false },
title: undefined
},
}
复制代码
这里我们需要找一个icon,可以直接在阿里的iconfont里面找个代表上传的图标,存为svg格式,在上面引用即可。
第二步 新增fileUpload上传组件
新增文件夹src/controls/FileUpload
,参照其他组件的格式,新增三个文件FileUpload/index.js
, FileUpload/Component/index.js
,FileUpload/Component/style.css
。三个文件皆可参照image组件。 咱们来说说不同点: FileUpload/index.js
...
addLink: Function = (linkTitle, linkTarget, linkTargetOption): void => {
const { editorState, onChange } = this.props;// editorState就是当前编辑器的实例
const { currentEntity } = this.state;
let selection = editorState.getSelection();
if (currentEntity) {
const entityRange = getEntityRange(editorState, currentEntity);
selection = selection.merge({
anchorOffset: entityRange.start,
focusOffset: entityRange.end
});
}
const entityKey = editorState
.getCurrentContent()
.createEntity("LINK", "MUTABLE", {
url: linkTarget,
targetOption: linkTargetOption
})
.getLastCreatedEntityKey();
let contentState = Modifier.replaceText(
editorState.getCurrentContent(),
selection,
`${linkTitle}`,
editorState.getCurrentInlineStyle(),
entityKey
);
let newEditorState = EditorState.push(
editorState,
contentState,
"insert-characters"
);
selection = newEditorState.getSelection().merge({
anchorOffset: selection.get("anchorOffset") + linkTitle.length,
focusOffset: selection.get("anchorOffset") + linkTitle.length
});
newEditorState = EditorState.acceptSelection(newEditorState, selection);
contentState = Modifier.insertText(
newEditorState.getCurrentContent(),
selection,
" ",// 插入一个空格,避免后面编辑的内容落在a标签内
newEditorState.getCurrentInlineStyle(),
undefined
);
onChange(
EditorState.push(newEditorState, contentState, "insert-characters")
);
this.doCollapse();
};
...
复制代码
上传之前的步骤我们都可以和image组件保持一致,但是上传之后的操作就不一样了。我们知道image组件是生成了一个img标签,直接在编辑器内显示图片。我们现在的需要的是插入一个超文本链接也就是a标签。所以我们需要改造addImage为addLink方法。
然后我们改造下图片预览,使它上传之后显示一个可点击的链接:FileUpload/Component/index.js
...
uploadFileUpload: Function = (file: Object): void => {
this.toggleShowFileUploadLoading();
const { uploadCallback } = this.props.config;
uploadCallback(file)
.then(({ data }) => {
this.setState({
showFileUploadLoading: false,
dragEnter: false,
fileSrc: data.link || data.url,
fileName: data.name // 获取文件名
});
this.fileUpload = false;
})
.catch(() => {
this.setState({
showFileUploadLoading: false,
dragEnter: false
});
});
};
...
render () {
...
...
}
复制代码
当引用图片上传组件或者文件上传组件时,我们可以配置uploadCallback
方法,用自己的上传接口自定义上传操作,只需保证此函数返回promise对象且返回结果包含{data: {link:'',name: ''}}
即可。
最后一步
发布我们的代码,在组建中引用,改下package.json
{
"name": "react-mysoft-wysiwyg",
"version": "1.12.16",
...
}
复制代码
改下名字,保证npm平台没有重名,改下版本号。
运行 npm publish
。发布到npm平台,然后到自己的npm主页,看是否发布成功。 发布成功后。直接在自己的项目安装组件即可。
npm install react-mysoft-wysiwyg --save
以上。完整项目地址 。
使用编辑器的时候发现,有一个小bug,就是不管怎么删,最终总会遗留一个p标签,而且如果这个p标签带了样式属性,它是清除不了的。这就产生了如何判断编辑器为空的麻烦。索性咱们就加一个‘清空’按钮。增加步骤和上面类似。大致思路就是当我们点击清空的时候,利用draft-js提供的api,初始化editorState:
...
removeInlineStyles: Function = (): void => {
const { onChange } = this.props;
const newState = EditorState.createEmpty();// 初始化
onChange(newState);
};
...
复制代码
最后
上面我也是为了方便所以直接随便整了一下自己用,还有国际化文件的修改、加上单测、以及文档的完善工作没做。最好还是建议大家提pr,这样可以紧跟版本,而且可以锻炼下单测技能。