canvas-editor是一个基于canvas/svg
的富文本编辑器,可以实现文档的前端在线编辑功能,同时配合作者提供的组件,可以方便的导入导出word
文档。但是遗憾的是作者给出是一个单独的项目,虽然vue
/react
的组件版作者表示已经加入待开发计划中,但是目前该项目仍不能在前端框架中开箱即用。在这里依据自己的经验提供一种组件化的可行方案。
将canvas-editor嵌入react项目
拉取官方的项目demo,该实例在提供了编辑器核心层功能外,作者还给出了菜单栏、状态栏等的一个官方实现。
这里以我新建的react-app
为例,将红框部分目录及文件复制到src/components/canvas-editor
目录下,在根目录复制tsconfig.json
及vite.config.ts
配置文件(如果项目中已存在配置文件则参考官方demo修改配置文件)。并在json.package
文件内"dependencies"
下补全以下依赖。
"@hufe921/canvas-editor": "^0.9.92",
"@types/prismjs": "^1.26.0",
"prismjs": "^1.27.0",
"typescript": "^5.6.2",
"vite": "^5.4.6",
其中官方demo中的editor
目录下为编辑器的核心功能,该部分内容作者也提供了npm
包。我在项目中未复制该目录,而是通过安装@hufe921/canvas-editor
调用核心功能。用这种方式注意需要将代码中通过相对路径引用到import xxx form '(../..)/editor'
或import xxx form ''(../..)/canvas-editor'
的部分全部替换为import xxx from '@hufe921/canvas-editor'
。
完成上述操作后,打开react
项目中的main.ts
文件,将
window.onload = function () {
const isApple =
typeof navigator !== 'undefined' && /Mac OS X/.test(navigator.userAgent)
......
}
修改为
export function init () {
const isApple =
typeof navigator !== 'undefined' && /Mac OS X/.test(navigator.userAgent)
......
}
然后在src/components/canvas-editor
目录下新建index.tsx
,写入以下代码:
import "./style.css";
import { init } from "./main";
import { useEffect } from "react";
import React from "react";
const CanvasEditor = () => {
useEffect(() => {
init()
}, [])
return (
<>
</>
)
};
export default CanvasEditor;
找到官方demo中的index.html
文件,将其中<body>
标签内的内容利用工具转换成jsx
代码,作为上文index.tsx
文件中的return
值。在这一步中可能会出现 无法使用JSX,除非提供了 "--jsx"
的报错,直接把文件改成index.jsx即可 需要在tsconfig.json
的compilerOptions
中加上"jsx": "react",
。
然后在需要的地方引入CanvasEditor,安装依赖运行即可。
(按理通过这种方法在vue中也是可行的,useEffect
换成mounted/onMounted
即可,但是我没试过)
组件的二次修改
修改前先大致了解一下canvas-editor
项目内各文件目录的功能,
assets/images
目录保存了菜单栏、状态栏的按钮图标,新增保存图标时建议保存为16*16,#3D4757
的svg
文件。
assets/snapshots
目录下为官方demo的简介图片,可以直接删除该目录。
components
目录下的dialog
是菜单栏超链接的弹窗,如果编辑器不需要插入超链接功能,可以同时删除该目录。signature
为官方demo示例中的插入签名,不需要也可以删除。
pluins
目录下分别为复制内容时带入版权信息、markdown
转文档正文的部分插件功能,github.com/Hufe921/can… ,官方也提供了二维码、代码块、word
、excel
等其他插件,均可在此按需增减。
utils
目录下index.ts
内有菜单栏字数统计的防抖,批注、目录的点击跳转,批注、目录的更新需要用到的函数。prism.ts
为菜单栏代码块功能的附加内容。
main.ts
文件内包括了编辑器实例的初始化、为菜单栏、状态栏各按钮绑定事件、事件监听、注册右键菜单、注册快捷键等功能。对编辑器功能的二次修改基本均在此页面内完成。
mock.ts
为官方demo模拟数据,可以自行替换成需要的数据或从接口读取数据。
以删除功能为例
在上一章节的组件化后,得到的是通过mock数据生成拥有完整功能的示例文档,但假设项目中并不需要用到编辑器内修改字体的功能,则需要在index.tsc
中找到该元素,即类名为menu-item__font
的div
标签内包裹的内容。删除该部分内容后,通过该类名一并删除style.css
中的相关样式。如果该功能有图标,还需在assets/images
中一并删除对应图标。
最后在并在main.ts
中找到该类名,删除相关的绑定代码。
const fontDom = document.querySelector<HTMLDivElement>('.menu-item__font')!
const fontSelectDom = fontDom.querySelector<HTMLDivElement>('.select')!
const fontOptionDom = fontDom.querySelector<HTMLDivElement>('.options')!
fontDom.onclick = function () {
console.log('font')
fontOptionDom.classList.toggle('visible')
}
fontOptionDom.onclick = function (evt) {
const li = evt.target as HTMLLIElement
instance.command.executeFont(li.dataset.family!)
}
此外记得在文件内搜索删除功能调用的函数executeFont
,因为可能该功能除了绑定按钮点击事件,还注册了右键菜单或者快捷键,如果有也一并删除。
以新增功能为例
例如在项目中想要增加一个将文档以base64
形式通过接口保存的数据库的功能,需要实现该部分内容,我们需要额外的docx
插件,去上文提到的插件仓库找到我们需要的这四个文件(其实保存功能并不需要importDocx
,但是一般保存和读取功能都是绑定增删的)
我们首先在index.tsc
文件中需要的位置加上标签<div className="menu-item__save"><i></i></div>
,然后下载相应的保存图标save.svg
到assets/images
,在style.css
中加上:
.menu-item__save i { background-image: url('./assets/images/save.svg'); }
(当然如果要求没那么高也可以只在index.tsc
添加标签<div className="menu-item__save">保存</div>
)。
最后在main.ts
中为按钮绑定功能,
const saveDom = document.querySelector<HTMLDivElement>('.menu-item__save')!
saveDom.title = `保存(${isApple ? '⌘' : 'Ctrl'}+S)`
saveDom.onclick = function () {
console.log('save')
instance.command.executeExportDocx()
}
如果更追求完美一些,可以在该文件末尾的instance.register.shortcutList
中加入快捷键:
{
key: KeyMap.S,
ctrl: true,
isGlobal: true,
callback: (command: Command) => {
command.executeExportDocx()
}
}
这样按下ctrl+S
也可以保存了。但是即使完成了以上操作你会发现该保存功能实现的是将在线文档保存为本地docx
文件,和预期还有点不一样。按源码去找executeExportDocx()
,会发现最后它调用了plugins/docx/utils.ts
中的saveAs
,这就是一个将blob
流保存为本地文件的函数,
export function saveAs(blob: Blob, name: string) {
const a = document.createElement('a')
a.href = window.URL.createObjectURL(blob)
a.download = name
a.click()
window.URL.revokeObjectURL(a.href)
}
将其改造一下
import { saveDocument } from "../../api" // 这里是项目调用的保存接口
export function saveAs(blob: Blob) {
const fileReader = new FileReader()
fileReader.readAsDataURL(blob)
fileReader.onload = (e) => {
saveDocument(e!.target!.result as string)
}
fileReader.onerror = () => {
new Error('save error')
}
}
OK这下可以了(截图与代码并不完全对应,截图代码的保存功能还额外传了id字段)。
其他配置的修改
其他配置一般都可以在配置项中进行配置,在main.ts
中找到初始化编辑器的代码
const container = document.querySelector<HTMLDivElement>('.editor')!
const instance = new Editor(
container,
{
header: [],
main: <IElement[]>data,
footer: []
},
options // 从mock.ts中引入的options即编辑器配置项
)
以将编辑器修改为一个博客/评论为例,我们可以做以下配置
const container = document.querySelector<HTMLDivElement>('.editor')!
const instance = new Editor(
container,
{
header: [],
main: <IElement[]>data,
footer: []
},
{
pageMode: <PageMode>'continuity', // 默认为连页模式,长文章也不会显示多张纸张
marginIndicatorSize: 0, // 不显示四角的页边距标识
contextMenuDisableKeys: ["globalPrint"], // 禁用右键打印菜单
}
)
完整配置项可以参见文档 hufe.club/canvas-edit…
另外,原项目的菜单栏及状态栏以position: fixed
固定在视窗顶部与底部,若需要将编辑器组件卡片化嵌入项目中,只需将position
修改为absolute
并将父级position
修改为relative
即可。