├── .github └── workflows │ └── pushtocoding.yml ├── .gitignore ├── CHANGES.md ├── LICENSE ├── README.md ├── cdn ├── README.md └── index.html ├── docs └── zh-CN │ ├── assets │ ├── complex-block.png │ ├── custom-context-menu.png │ ├── editor.png │ ├── embed-block.png │ ├── server-architecture-1.png │ └── server-architecture-2.png │ ├── box.md │ ├── calendar.md │ ├── comment.md │ ├── complex-block.md │ ├── custom-context-menu.md │ ├── custom-style.md │ ├── custom-suggest.md │ ├── custom-template.md │ ├── custom-text-toolbar.md │ ├── date.md │ ├── editor-structure.md │ ├── embed-block.md │ ├── handle-message.md │ ├── index.md │ ├── label.md │ ├── mention.md │ ├── post-custom-message.md │ ├── server-architecture.md │ └── server.md ├── h5 ├── index.html ├── package.json ├── server.js ├── src │ ├── box_calendar.ts │ ├── box_custom_mention.ts │ ├── box_date.ts │ ├── box_label.ts │ ├── custom.ts │ ├── index.ts │ ├── mention.ts │ └── simple.ts ├── tsconfig.json ├── webpack.calendar.js ├── webpack.custom.js ├── webpack.custom.mention.js ├── webpack.date.js ├── webpack.dev.js ├── webpack.label.js ├── webpack.mention.js └── webpack.simple.js ├── local ├── README.md ├── index.html ├── package.json ├── src │ └── local.ts ├── tsconfig.json └── webpack.dev.js ├── react ├── .gitignore ├── README.md ├── package.json ├── public │ ├── favicon.ico │ ├── index.html │ ├── logo192.png │ ├── logo512.png │ ├── manifest.json │ └── robots.txt ├── server.js └── src │ ├── App.css │ ├── App.js │ ├── App.test.js │ ├── index.css │ ├── index.js │ ├── logo.svg │ ├── reportWebVitals.js │ └── setupTests.js ├── wiki-react ├── .gitignore ├── README.md ├── package.json ├── public │ ├── favicon.ico │ ├── index.html │ ├── logo192.png │ ├── logo512.png │ ├── manifest.json │ └── robots.txt ├── server.js └── src │ ├── App.css │ ├── App.js │ ├── App.test.js │ ├── index.css │ ├── index.js │ ├── logo.svg │ ├── reportWebVitals.js │ └── setupTests.js └── wiki ├── README.md ├── client ├── package.json ├── src │ └── index.ts ├── tsconfig.json ├── webpack.dev.js ├── webpack.prod.js └── wiki.html ├── package.json └── server ├── index.js ├── package.json └── pm2.config.js /.github/workflows/pushtocoding.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | 3 | name: push_to_coding 4 | 5 | # Controls when the action will run. 6 | on: 7 | # Triggers the workflow on push or pull request events but only for the main branch 8 | push: 9 | branches: [ main ] 10 | # Allows you to run this workflow manually from the Actions tab 11 | workflow_dispatch: 12 | 13 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 14 | jobs: 15 | # This workflow contains a single job called "build" 16 | push: 17 | # The type of runner that the job will run on 18 | runs-on: ubuntu-latest 19 | 20 | # Steps represent a sequence of tasks that will be executed as part of the job 21 | steps: 22 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 23 | - uses: actions/checkout@v2 24 | with: 25 | fetch-depth: 0 26 | 27 | # Runs a set of commands using the runners shell 28 | - name: push to coding 29 | run: | 30 | git remote add coding https://ptjc0djpnv6v:15558ea3b09b11427eeb70c3df940d0d1c07e787@e.coding.net/wizteam/wizeditor/demo.git 31 | git push coding HEAD:main -f 32 | echo done 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | client/dist 3 | env.sh 4 | start.sh 5 | yarn*.* 6 | .log 7 | dist/ 8 | */.eslintcache 9 | package-lock.json 10 | .DS_Store 11 | react-demo/.eslintcache 12 | .vscode -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | # WizEditor辑器更新历史 2 | ## 428 3 | 1. 在markdown正文里面转义\$,避免被错误当成公式 4 | 2. 中文输入的守候不进行快速输入识别 5 | 3. drawio可以正常导出数学公式 6 | 4. 修复bug:创建inline code会把内容修改成后面的math 7 | 5. 修复数学公式光标可能无法正常定位的问题 8 | 6. 修复数学公式undo/redo可能不正常的问题 9 | 7. 数学公式,直接输入\$,不需要空格确认,和粗体,斜体快捷输入相同的处理方式 10 | 8. 修正bug:移动端查看其它评论后,无法查看第一条评论 11 | 9. 修正bug:解决多行注释解析错误的问题 12 | 10. tooltip增加部分icon 13 | 11. 修正bug:blockMenuButton 和 contextMenu 可能同时显示的问题 14 | 12. 修正bug:mac下面可能无法出现右键菜单的问题 15 | 13. 修正bug:markdown链接语法可能无法识别的问题 16 | 14. 修正bug:plantuml可能无法渲染的问题 17 | 15. 优化部分UI 18 | 19 | ## 427 20 | 1. 修复可以将图片拖入code的问题 21 | 2. 优化拖动图片到表格操作 22 | 3. 鼠标选择文本过程中,不显示block menu button以及菜单 23 | 4. 兼容微信文章的code 24 | 5. 有右键菜单的时候,不显示文字工具栏。 25 | 6. 选中内容改编的时候,关闭右键菜单 26 | 7. 优化block快照 27 | 28 | ## 426 29 | 1. 复制列表包含前面的数字 30 | 2. 优化mathjax显示 31 | 3. 修复调整表格宽度问题 32 | 4. 优化微信聊天记录导入 33 | 5. 编辑器选项,更改enableContextMenu为disableContextMenu。默认显示右键菜单 34 | 6. 优化文字下划线样式 35 | 7. 更改wiki链接,支持添加id等 36 | 8. 复制block,取消选中状态 37 | 9. 增加animateScrollToBlock方法 38 | 10. 添加appId,docId到剪贴板数据中 39 | 11. 增加onGetBlockCommand回调参数,添加当前的menu data 40 | 41 | ## 425 42 | 1. 避免android无法显示复制菜单 43 | 2. 复制出来的markdown,将clike语言转换为cpp,兼容vscode 44 | 3. bug fix: 判断光标的位置。当光标在一个box和一段文字之间的时候,获取的rect可能是空的。兼容这种情况 45 | 4. auto suggest 阻止esc冒泡 46 | 5. \[服务端\] fix: 不在更新keepAlive的时候清理startup,避免在多个服务启动时,交替删除对方 47 | 6. 优化编辑内部分block编辑样式 48 | 7. 优化微信文章,转换html的时候,去掉隐藏的元素 49 | 8. 为兼容低版本浏览器,同时设置两个cookie,一个sameSite=None,另一个不设置sameSite 50 | 9. 优化表格右键菜单 51 | 52 | ## 424 53 | 1. 支持编辑服务在子路径下面 54 | 2. 表格增加row title, column title功能,增加stripe style样式 55 | 3. 支持plantuml插入(\`\`\`uml) 56 | 4. list block支持quote 57 | 5. code里面强制使用纯文本粘贴 58 | 6. markdown表格工具栏优化 59 | 7. markdown表格内空的checkbox识别问题修复 60 | 8. 支持数学公式block 61 | 9. 优化表格工具栏 62 | 10. 增加flowchart支持 63 | 11. 隐藏表格删除按钮 64 | 12. 优化加载速度,延迟渲染code language,将code高亮代码放在worker里面执行 65 | 13. 整合layout表格。任何表格都可以设置是否显示表格线 66 | 67 | ## 423 68 | 1. toc 内点击 超链接时,不去设置光标,避免锚点跳转异常 69 | 2. toOrderedList 指令执行后,不再选中 block 内全体文本,避免误操作 70 | 71 | ## 422 72 | 1. 移动端不显示text toolbar,右键菜单 73 | 2. 修复无法复制excel表格内容的问题 74 | 3. 修复移动光标可能出错的问题 75 | 4. 修复导出text only list导出markdown错误的问题 76 | 5. 修复ios插入ocr文字的问题 77 | 6. 修复可以给code设置样式的问题 78 | 7. 在code里面全选,优先选择整个code,再选择整个文档 79 | 8. 修复可以在code里面插入markdown的问题 80 | 81 | ## 421 82 | 1. 兼容掘金内容复制粘贴 83 | 2. 修复url里面可能包含错误的字符的问题 84 | 3. bug fix:光标移动错误的问题 85 | 4. bug fix: 插入code 保留回车之后的内容 86 | 5. 优化html2markdown 87 | 6. 优化github代码粘贴,去掉行号单元格,保持前面的空格 88 | 7. 阅读模式光标在底部时,不自动滚动 & 光标必须在编辑器内部时才进行滚动 89 | 8. 避免输入时,光标紧贴在最低端;修正 打字机模式居中位置算法;统一使用 getRangeRect 方法 90 | 9. 修正 markdown 下 table 100% 的样式 91 | 10. 粘贴html,保持行首的空格 92 | 11. 修复表格滚动问题 93 | 12. 添加保存图片回调 94 | 13. markdown笔记,增加复制为纯文本功能 95 | 14. 屏蔽表格右侧点击事件,避免光标跳动 96 | 97 | ## 420 98 | 1. 修复搜狗输入法兼容问题 99 | 2. 设置 全局 button 样式 & 调整 修改编号 对话框内 样式 100 | 3. 兼容某些版本markdown的表格语法 101 | 4. 列表:如果当前是一个heading,那么转换列表的时候,尝试找到前一个heading的list block 102 | 103 | ## 419 104 | 1. 增加播放历史操作功能 105 | 2. 优化删除表格行/列的问题 106 | 3. 允许插入远程的图片 107 | 4. 修复复制的纯文本/html包含code language select的问题 108 | 5. 调整修改序号功能 109 | 6. handleBlockInserted 应用到complex block的子block,这样可以让表格里面的图片进行本地化 110 | 7. bug fix: 粘贴表格可能丢失文字的问题 111 | 8. 调整弹出框/菜单样式 112 | 9. 优化markdown粘贴功能 113 | 10. 粘贴纯文本继承样式 114 | 11. 修复在表格内移动光标不能自动滚动的问题 115 | 12. 在最后一个单元格,按下tab的时候,自动增加新行 116 | 117 | ## 418 118 | 1. 修改block 点击消息处理;去掉drawio选中状态,统一使用embed的选中状态 119 | 2. 有序列表增加修改编号的功能 120 | 3. 允许给图片设置对齐方式 121 | 4. 增加图片loading样式 122 | 5. 识别markdown图片语法 123 | 6. \[服务端\]支持无redis启动 124 | 7. \[服务端\]支持S3存储 125 | 8. 优化code粘贴 126 | 9. 图片错误,仅显示新加入的图片 127 | 10. 复制:markdownonly的时候默认复制markdown源代码,粘贴的时候默认按照markdown粘贴 128 | 11. 粘贴前转换data url 129 | 12. 优化markdowwn复制粘贴 130 | 13. 代码高亮增加bash和R语言 131 | 132 | ## 417 133 | 1. 允许appId长度为2个字符 134 | 135 | ## 416 136 | 1. 支持阿里云redis集群 137 | 2. 修复markdown转换可能有多余空行的问题 138 | 3. 允许drawio放弃编辑 139 | 4. 粘贴html,将pre转换为code 140 | 141 | ## 415 142 | 1. 优化粘贴html功能,不添加多余的空格 143 | 2. 修正drawio无法编辑的问题 144 | 3. 优化导出markdown功能 145 | 4. 调整图片loading样式 146 | 147 | ## 414 148 | 1. 给toc增加点击回调 149 | 2. 修复图片缩放按钮可能错位的问题 150 | 3. 优化图片loading样式 151 | 4. 优化导出docx功能 152 | 153 | ## 413 154 | 1. 给drawio增加loading状态 155 | 156 | ## 412 157 | 1. 修复包含code的文档转换为纯文本bug 158 | 2. 修复执行block menu的时候页面可能跳动的问题 159 | 3. 给预览的文件增加编辑按钮 160 | 4. 给文件box增加编辑功能 161 | 5. 导出markdown,支持导出评论 162 | 6. 修复部分快捷键在windows上面无法使用的问题(对齐,引用) 163 | 7. 优化表格宽度调整 164 | 8. 新建表格,默认输入文字不强制换行 165 | 9. 增加text转换为doc的功能 166 | 10. 输入数学公式,需要使用空格键进行确认再转换 167 | 11. 输出markdown,对于特殊字符进行转义 168 | 12. 在两个\`之间粘贴文字,尝试自动识别为code 169 | 13. 优化markdown导入导出,增加忽略空行选项 170 | 14. 导入markdown兼容typora的空行策略 171 | 15. 正在上传的图片增加占位 172 | 16. 移动端给文件card设置默认宽度 173 | 17. 修复下载图片可能失败的问题 174 | 18. 修复safari下面card,视频等无法撑起单元格的问题 175 | 176 | ## 411 177 | 1. 增加分割线之后,光标放在分割线下面 178 | 2. 移动端取消hover状态和样式 179 | 3. 增加评论禁止回复,禁止编辑选项 180 | 4. drawio支持控制语言 181 | 5. 修改drawio数据保存方式,不再把数据保存到json里面 182 | 6. 增加drawio保存数据错误回调 183 | 7. 修复drawio中文乱码问题 184 | 185 | ## 410 186 | 1. 修复导出word文字大小错误问题 187 | 2. 给空的drawio增加样式 188 | 3. 修复快捷方式可能无效的问题 189 | 190 | ## 409 191 | 1. 导出word支持字体,大小,颜色,背景色 192 | 2. 导出word支持自定义默认的字体大小 193 | 3. 支持导出markdown 194 | 4. 修复超大表格复制出错的问题 195 | 196 | ## 408 197 | 1. 修复表格工具栏显示逻辑 198 | 199 | ## 407 200 | 1. 支持右键复制图片 201 | 202 | ## 406 203 | 1. 修复无法修改锁定的表格内的input的问题 204 | 2. 支持右键菜单单独复制图片 205 | 3. 完善reload部分事件清理 206 | 4. 避免有多个编辑器的时候,tooltip重复的问题 207 | 5. 可以通过esc取消公式编辑对话框 208 | 6. 修复可能无法执行cut的问题 209 | 7. 修复表格分割线可能错位的问题 210 | 8. 增加Alt+T,Alt+B,Alt+H快捷键,可以快速设置颜色 211 | 9. 支持连续四个\$输入公式 212 | 10. 支持只读模式显示查找对话框 213 | 214 | ## 405 215 | 1. 修复插入数学公式错误进行编辑的问题 216 | 2. block锁定的情况下,允许修改input 217 | 218 | ## 404 219 | 1. 修复表格更改大小bug 220 | 2. 修复某些快捷键无效的问题 221 | 3. 修复文件拖放bug 222 | 223 | ## 403 224 | 1. 增加onFileCardClick点击事件 225 | 2. 点击box的时候自动选中整个box 226 | 3. 修复自动完成的bug 227 | 4. 禁止readonly模式下更改iframe大小 228 | 229 | ## 402 230 | 1. 禁止浏览器的右键菜单 231 | 232 | ## 401 233 | 1. 修复某些情况下编辑器可能变成只读的问题 234 | 2. 修复某些情况下锁定的表格内容仍然可以修改的问题 235 | 236 | ## 400 237 | 1. 优化手机样式 238 | 239 | ## 399 240 | 1. 提供禁用表格工具栏选项 241 | 2. box下拉框增加选项 242 | 3. \[服务端\]增加删除快照功能 243 | 244 | ## 398 245 | 1. 修复无法复制图文到其他应用的问题 246 | 2. 优化markdown转换功能 247 | 248 | ## 397 249 | 1. 修复滚动条可能无响应的问题 250 | 2. 修复在表格中可以插入code的问题 251 | 3. 增加导出markdown功能 252 | 4. 增加插入toc功能 253 | 5. 记住图片高度,避免页面加载的时候高度变化 254 | 6. 修复表格中图片缩放的一些bug 255 | 7. 优化右键菜单,显示快捷键 256 | 8. 插入code,记住最后选择的语言 257 | 258 | ## 396 259 | 1. 优化code的语言,常用语言放在最前面 260 | 261 | ## 395 262 | 1. 调整 list 中的 查看脑图、插入成员、插入时间 显示规则(移动端 或 宽度小于 512 时隐藏) 263 | 2. 更改语言后重新高亮 264 | 3. 编辑器增加行号 265 | 266 | ## 394 267 | 1. 避免复制code页面跳动 268 | 269 | ## 393 270 | 1. 增加导出为docx功能 271 | 2. 增加禁止下载office文件功能 272 | 3. 修改math输入框样式 273 | 4. 优化code语言选择框样式 274 | 275 | ## 392 276 | 1. 增加集群支持 277 | 278 | ## 391 279 | 1. 修正表格更改大小错误 280 | 281 | ## 390 282 | 1. bug修复:被锁定的内容可以进行文字替换 283 | 2. bug修复:表格里面的checkbox背景颜色错误 284 | 3. bug修复:修复表格被锁定的情况下仍然可以修改里面内容的问题 285 | 4. bug修复:修复表格被锁定的情况下仍然可以修改宽度,插入行列等问题 286 | 5. bug修复:修复某些情况下无法复制表格的问题 287 | 6. bug修复:修复文字和图片可能无法同时选中的问题 288 | 7. bug修复:修复可能无法调整表格列宽的问题 289 | 8. 允许在多选单元格的情况下插入行和列 290 | 9. 优化:表格命令状态 291 | 10. 优化:图片选择 292 | 11. 优化:数学公式渲染 293 | 12. 优化:优化粘贴html样式 294 | 13. 服务端:增加集群管理,当文档已经被另一个节点服务的时候,通知所有的集群节点使缓存失效 295 | 296 | ## 389 297 | 1. 优化表格粘贴操作 298 | 2. 修复表格列宽调整bug 299 | 3. code增加Racket语言 300 | 4. 删除最后一个评论的时候,触发onUpdateLayout 301 | 5. 增加onGetPreviewInfo回调,允许返回自定义的预览信息 302 | 6. bug修复:在文档标题前面回车无法换行的问题 303 | 7. bug修复:包含图片时,无法复制完整选中部分 304 | 8. bug修复:无法正确选择图片的问题(包含多个图片的block) 305 | 9. bug修复:可能无法删除某些文字的bug 306 | 10. 优化错误信息处理 307 | 308 | ## 388 309 | 1. 支持token里面包含用户名称,用户头像,增强安全性 310 | 2. op中允许包含del和create操作 311 | 312 | ## 387 313 | 1. 自动修复可能有问题的op 314 | 2. 修复表格粘贴可能不全的问题 315 | 316 | ## 386 317 | 1. bug fix: 中文输入法状态下标题可能无法正常删除的问题 318 | 319 | ## 385 320 | 1. 修改文字中空格渲染的方式 321 | 2. 从inline-code中删除serif字体 322 | 3. markdown转换识别br 323 | 4. 调整评论交互,优化ios下面的评论体验 324 | 325 | ## 383 326 | 1. 修复表格选中状态可能无法清除的问题 327 | 328 | ### 383 329 | 1. list block作为mindmap查看,取消focused显示,仅保留hover 330 | 2. bug fix: 复制代码粘贴的时候,将代码转换为纯文本 331 | 3. office 文件支持下载打开编辑 332 | 4. 增加save office回调 333 | 5. 支持通过键盘选中图片 334 | 6. \[服务端\]增加migrate api 335 | 7. 支持更新插入的文件 336 | 8. 更改text input渲染方式,增加inline style样式设置 337 | 338 | text input 点击和更改数据方式: 339 | 340 | ```typescript 341 | function handleInputClicked(editor: Editor, box: BoxData, event: Event): void { 342 | console.log(box, event); 343 | setTimeout(() => { 344 | const color = ['red', 'green', 'blue'][Date.now() % 3]; 345 | editor.updateBoxData(box.id, { 346 | value: `clicked on ${new Date().toLocaleTimeString()}`, 347 | inlineStyle: `color: ${color}`, 348 | }); 349 | }, 300); 350 | } 351 | 352 | const options = { 353 | ... 354 | callbacks: { 355 | ... 356 | onInputClicked: handleInputClicked, 357 | }, 358 | }; 359 | ``` 360 | ## 382 361 | 1. 修复拖拽code可能出错的问题 362 | 2. 修复markdown表格支持 363 | 3. 避免 在 code 内移动鼠标时, BlockMenuButton 不断闪烁 364 | 365 | ## 381 366 | 1. 优化markdown支持,更符合markdown标准 367 | 2. 调整code样式 368 | 3. 允许上传任意文件 369 | 4. 支持通过客户端导出docx 370 | 5. 调整列表样式,避免列表折行后不对齐 371 | 6. Link 点击编辑按钮时,自动关闭 editableToolbar; 编辑器 destroy 时,自动关闭 editableToolbar 372 | 7. 修复markdown导入bug 373 | 8. 修正数学公式输入框样式 374 | 9. \[服务端\]上传文件错误包含错误代码 375 | 376 | ## 380 377 | 1. 支持markdown里面的html代码 378 | 2. 兼容firefox 379 | 380 | ## 379 381 | 1. \[服务端\]兼容旧版本node 382 | 383 | ## 378 384 | 1. 增加下载图片回调,可以让外部拦截下载图片功能(利用客户端跨域下载图片) 385 | 2. 调整数学公式夜间样式 386 | 3. 调整错误图片样式 387 | 4. \[服务端\],修复复制文档时可能不是最新版本的问题 388 | 389 | ## 377 390 | 1. 调整code的样式 391 | 2. 增加复制code功能 392 | 3. 调整emit错误顺序 393 | 4. 支持代码换行 394 | 5. 修正 img 缺省图片位置,避免 裂图显示 395 | 6. 导出docx支持字体大小和颜色 396 | 7. 修复markdown2doc的错误 397 | 8. code增加kotlin支持 398 | 9. 修复代码输入可能的错误 399 | 400 | ## 376 401 | 1. 修复code里面可能错误识别快速输入的bug 402 | 2. 支持block内软回车 403 | 3. bug修复:修复在code前后删除内容报错的问题 404 | 4. bug修复:支持在code后面继续输入纯文本 405 | 5. 增强:如果编辑器最后是一个图片等block,点击最后面空白,自动添加空白文字行 406 | 6. 在列表内支持输入软回车 407 | 7. bug修复:修复表格工具栏错位问题 408 | 8. 修复firefox兼容问题 409 | 9. 支持粘贴office里面的本地图片 410 | 10. 增加disableAudio选项 411 | 11. 修复markdown转换的时候,没有decode html标签的问题 412 | 413 | ## 375 414 | 1. 修复@可能无效的bug 415 | 416 | ## 374 417 | 1. 调整提醒下拉框UI 418 | 2. toHeading命令,支持取消当前heading 419 | 3. \[服务端\]fake token api支持指定权限 420 | 421 | ## 373 422 | 1. \[服务端\]导出docx/pdf支持指定版本 423 | 2. \[服务端\]获取text支持指定版本 424 | 3. 修复表格bug:在合并的单元格前后插入列 425 | 426 | ## 372 427 | 1. 修正mindmap按钮问题 428 | 2. 给text input增加关闭autocommplete属性 429 | 430 | ## 371 431 | 1. 修复firefox崩溃的问题 432 | 433 | ## 370 434 | 1. 修正夜间模式问题 435 | 2. 修正新建评论可能报错的问题 436 | 3. 修正code block选中的问题 437 | 4. 增加复制粘贴是否保留offcie文件字体设置的开关: 438 | 439 | ```typescript 440 | { 441 | ... 442 | officeConverter?: { 443 | convertList?: boolean; 444 | convertFont?: boolean; 445 | }, 446 | } 447 | ``` 448 | ## 369 449 | 1. 修复某些导入的文档无法显示的问题 450 | 451 | ## 368 452 | 1. 修正表格选中部分命令状态问题 453 | 454 | ## 367 455 | 1. 修改表格选中判断逻辑 456 | 2. 修改图片失败的样式 457 | 3. 修正markdown转换为doc的错误 458 | 459 | ## 366 460 | 1. 修正有删除线的时候无法正常显示光标的问题 461 | 2. 修正部分样式 462 | 463 | ## 365 464 | 1. 修正错误处理逻辑 465 | 466 | ## 364 467 | 1. editor.~~executeTextCommand~~增加inline-style-命令支持,可以支持设置字体名称和字体大小。 468 | 469 | ```typescript 470 | editor.executeTextCommand('inline-style-font-size', {'inline-style-font-size': '12px'}); 471 | editor.executeTextCommand('inline-style-font-family', {'inline-style-font-family': 'Times New Roman'}); 472 | ``` 473 | 获取当前样式:`editor.getDetailCommandStatus(editor.getSelectionDetail())` 474 | 475 | 2. 修正表格选中判断。 476 | 477 | 478 | 479 | | | | | | | | | | | 480 | | ----- | ----- | ----- | ----- | ----- | ----- | ----- | ----- | ----- | 481 | | | | | | | | | | | 482 | | | | | | | | | | | 483 | 484 | 485 | ## 363 486 | 1. mindmap增加视图自适应按钮 487 | 2. 修正导出markdown后code类型无法识别的问题。 488 | 3. 错误图片增加选中outline 489 | 4. \[服务端\]增加wmf/emf导入功能 490 | 491 | ## 362 492 | 1. 修正删除表格按钮显示规则 493 | 2. \[服务端\]版本列表增加创建时间 494 | 495 | ## 361 496 | 1. 修复中文输入可能报错的问题 497 | 2. 修正可能无法点击block menu button的问题 498 | 499 | ## 360 500 | 1. \[服务端\]复制文档时,支持指定版本 501 | 502 | ## 359 503 | 1. 修正表格阴影显示 504 | 505 | ## 358 506 | 1. 兼容低版本safari(夜间模式) 507 | 2. 修正mermaid样式 508 | 3. fixed issue : 选中 TextBlock 内的 box 不应该显示 TextToolbar 509 | 4. 修正内存占用 510 | 5. 修改打字机模式,底部增加padding。 511 | 6. TextToolbar增加updatePosition方法 512 | 7. 增加keepalive 超时功能。 513 | 514 | ## 357 515 | 1. 调整text input大小策略 516 | 2. 修正mermaid的theme(夜间模式等)事件监听方式 517 | 3. 修复完整删除多行文字,没有保留空行的bug 518 | 4. 给编辑器增加adjustTextInputSize方法 519 | 520 | ## 356 521 | 1. 增加获取纯文本功能 522 | 523 | ## 355 524 | 1. 优化自动调整文字input大小功能 525 | 526 | ## 354 527 | 2. \[服务器\],修复通过模版创建文档大小限制的问题 528 | 3. 优化内存占用 529 | 4. 修改websocket重连机制,心跳包没有回复3次后强制重连 530 | 5. 优化编辑器loader显示规则,超过300ms文档没有加载完成,再显示loader 531 | 6. 自动调整文字input大小 532 | 7. 增加checkbox可点击区域大小 533 | 8. 修复可能无法删除表格行/列的问题 534 | 535 | ## 353 536 | 1. 修复更改block类型后lock info丢失的问题 537 | 2. 修复列表继续编号可能会导致前面的list编号错误的问题 538 | 3. 无法显示的图片,显示占位图,同时增加错误回调: 539 | 540 | ```typescript 541 | onImageError?: (editor: Editor, image: HTMLImageElement) => void; 542 | ``` 543 | 4. 避免插入id相同的box,如果id相同则报错 544 | 5. \[服务端\]: 增加revoke token功能 545 | 546 | ## 352 547 | 1. 修改onRecognizeLink回调添加参数: 548 | 549 | ```typescript 550 | onRecognizeLink?: (editor: Editor, text: string, block: BlockElement, options: { offset: number, count: number }) => Promise<{ text: string, link: string, processed?: boolean} | null>; 551 | ``` 552 | 添加参数 block和options,返回参数processed,支持外部拦截插入链接消息并进行处理。如果外部已经处理了插入链接消息,则返回processed为true。 553 | 554 | ## 349 555 | 3. 修复可以剪切锁定的block的问题 556 | 4. 调整只读模式下右键菜单显示规则 557 | 558 | ## 348 559 | 1. 添加编辑器选项readonlyTitlePlaceholder,readonly模式下显示标题placeholder 560 | 2. \[服务端\] 上传大文件不再强制关闭链接 561 | 3. \[服务端\] 导入doc文件,修正表格导入bug 562 | 4. 添加source到文档create消息,可以区分revert和主动create。 563 | 564 | ## 347 565 | 1. 修复移动端checkbox右边padding大的问题 566 | 2. block被锁定的时候禁止拖动图片 567 | 568 | ## 346 569 | 1. \[服务端\]docx支持input的导出 570 | 2. 修复表格工具栏按钮重复问题 571 | 3. markdown模式下禁止出现合并单元格按钮 572 | 4. 前端内存占用优化 573 | 5. wiki link选择框宽度限制 574 | 575 | ## 345 576 | 1. 执行文字命令,排除掉被锁定的block 577 | 2. 评论部分样式,修复名字超长引起日期显示不全的问题 578 | 3. 修复跨页表格无法多选的问题 579 | 4. 修复表格插入新行/新列位置可能错误的问题 580 | 581 | ## 344 582 | 1. 修复锁定block可能无效的问题 583 | 584 | ## 343 585 | 1. 修复mindmap样式的问题 586 | 587 | ## 342 588 | 1. online user增加用户权限数据 589 | 2. 调整日历样式(不可选择的日期样式) 590 | 3. 调整插入layout的逻辑,和table保持一致 591 | 4. \[服务端\] 超过2k的op保存成文件,避免数据库里面存储超大数据 592 | 593 | ## 341 594 | 1. 插入网页的时候,不再保留协议,默认采用//开头,和当前页面协议保持一致。 595 | 596 | ## 340 597 | 1. 增加单独清除文字格式命令 598 | 2. 修改保存/恢复选中部分状态功能,不再依赖dom 599 | 3. \[服务端\] 修复word导入表格数据可能有问题的bug 600 | 601 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 WizTeam 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # wiz-editor 多人实时编辑器使用说明 2 | 3 | wiz-editor是一个支持多人实时协同编辑的网页富文本编辑。 4 | 5 | 1. 官网:[https://wiz-editor.com/](https://wiz-editor.com/) 6 | 2. 在线演示:[https://api.wiz-editor.com/demo.html](https://api.wiz-editor.com/demo.html) 7 | 8 | **注意:在线演示的内容,因为监管问题,会定期自动清空,请勿用于内容发布。** 9 | 10 | 视频演示 11 | 12 | https://user-images.githubusercontent.com/2747978/110881767-da9cd900-831b-11eb-8d18-8498a3dc3be2.mp4 13 | 14 | 15 | ## 升级日志 16 | [wiz-editor升级日志](./CHANGES.md) 17 | 18 | ## 应用 19 | 为知笔记重构版,为知笔记Lite已经全面使用该编辑器。 20 | 21 | ## 编辑器主要特性 22 | 23 | 1. 支持多人实时协同编辑,单一文档可以支持多达1000人同时编辑。 24 | 2. 支持扩展各种业务模块。可以自定义行内/行间模块。 25 | 3. 可以接入业务用户系统及权限,文档权限完全由业务控制。 26 | 4. 纯h5原生开发,可以直接嵌入各种web应用中 27 | 5. 允许脱离服务端直接使用(此时无多人实时协作能力,可作为传统网页编辑器使用) 28 | 6. 支持word文件导入,office文件预览 29 | 7. 支持markdown语法 30 | 8. 完整的二次开发支持。所有接口均有typescript定义。 31 | 32 | ## 适用场景 33 | 34 | ### 开发企业在线文档/wiki等应用。 35 | 36 | wiz-editor专门为在线文档而开发,已为多家企业提供集成服务。wiz-editor提供了强大的扩展能力,可以将企业的业务集成到文档里面。 37 | 例如,常规的提醒,任务,日历等整合,可以和企业内部的IM,任务系统进行整合。同时利用模版能力,可以快速的生成各种文档,例如合同, 38 | 周报等。 39 | 40 | 同时,wiz-editor可以无缝的和企业内部的用户和权限进行整合,无需用户多次登录。 41 | 42 | 利用wiz-editor多人实时协同编辑的特点,企业内共享的文档,无需担心版本冲突的问题,让文档永远保持最新版本。 43 | 44 | ### 作为富文本编辑器使用 45 | 46 | wiz-editor可以替换传统的网页编辑器,例如各种博客的编辑器等。即使不需要多人实时协同编辑, 47 | 也可以单独使用wiz-editor客户端(无需依赖服务端)。这样可以充分利用wiz-editor强大的编辑功能。 48 | 49 | ### 作为markdown编辑器使用 50 | 51 | wiz-editor提供了完整的markdown功能,可以利用markdown语法直接编写文档。同时支持markdown导入/导出功能。 52 | 53 | 54 | ## 编辑器详细功能 55 | 56 | ### 文字样式 57 | 58 | * 标题(1-5) 59 | * 有序/无需列表 60 | * 任务清单 (可以在任务清单中设置截止日期,@相关成员) 61 | * 文字引用 62 | * 文字颜色/背景色 63 | * 粗体,斜体,下划线,删除线等 64 | * 任意的css样式 65 | 66 | ### 行内对象 67 | 68 | * 链接 69 | * latex公式 70 | * 提醒 71 | * 评论 72 | * 其它自定义行内样式 73 | 74 | ### 块对象 75 | 76 | * 文字(普通文字,标题,列表,任务清单) 77 | * 图像,视频,音频 78 | * 表格 79 | * 网页(例如各种视频网站的视频,地图等) 80 | * mermaid图形(Flowchart, Sequence Diagram, Class Diagram等) 81 | * UML图表等(drawio) 82 | * 各种复杂布局 83 | * 图表(chart.js) 84 | * office, pdf文件(支持office, pdf文件预览) 85 | * 代码,支持语法高亮 86 | 87 | ### 块对象状态 88 | 89 | * 锁定状态。可以锁定某些块,则这些块元素无法被删除和编辑 90 | * 手写标记(可通过手写工具标注某些文字并保留记录) 91 | 92 | ### 编辑器功能 93 | 94 | * 支持markdown语法 95 | * 支持markdown only模式(仅支持markdown功能,此时可以作为一个markdown编辑器使用,并且保存数据为markdown) 96 | * 支持离线模式(无需编辑服务,仅作为一个纯前端编辑器,可直接替换业务中原有的编辑器) 97 | * 支持导入/导出markdown 98 | * 支持导出html 99 | * 支持导入docx文件 100 | * 支持focus模式,支持打字机模式 101 | * 支持将列表作为思维导图显示 102 | * 支持theme,支持夜间模式(可自动切换,可禁用) 103 | 104 | ![1](https://user-images.githubusercontent.com/2747978/110880234-42055980-8319-11eb-9083-4d7f7c801914.png) 105 | 106 | ![2](https://user-images.githubusercontent.com/2747978/110880241-4762a400-8319-11eb-9b80-29902deef93c.gif) 107 | 108 | 109 | 110 | ## 运行demo 111 | 112 | 包含编辑器服务端,可以直接在本机测试运行。 113 | 114 | 直接从git clone或者下载代码,解压缩到磁盘。 115 | 116 | 注意:**需要nodejs 13或者更高的版本** 117 | 118 | ## 运行原生js例子 119 | 120 | 1. 安装和运行 121 | 122 | ```sh 123 | cd h5 124 | npm install 125 | npm start 126 | ``` 127 | 128 | 2. 打开浏览器 129 | 130 | ``` 131 | localhost:9000 132 | ``` 133 | 134 | ## 更多demo 135 | 136 | ```bash 137 | cd h5 138 | # 完整的自定义扩展例子,包含外部工具栏,各种自定义组件等 139 | npm run custom 140 | # 极简编辑器,无外部UI 141 | npm run simple 142 | # 各种自定义box 143 | npm run calendar 144 | npm run date 145 | npm run mention 146 | npm run label 147 | ``` 148 | 149 | ## 在自己的项目中使用wiz-editor 150 | 151 | ### 通过npm安装wiz-editor 152 | ```bash 153 | npm i wiz-editor 154 | ``` 155 | 156 | ### 在项目中使用wiz-editor 157 | 158 | ```ts 159 | import { 160 | createEditor, 161 | Editor, 162 | } from 'wiz-editor/client'; 163 | 164 | // 定义AppID,AppSecret, AppDomain。在自带的测试服务器中,下面三个key不要更改 165 | const AppId = '_LC1xOdRp'; 166 | const AppSecret = '714351167e39568ba734339cc6b997b960ed537153b68c1f7d52b1e87c3be24a'; 167 | const AppDomain = 'wiz.cn'; 168 | 169 | // 初始化服务器地址 170 | const WsServerUrl = window.location.protocol !== 'https:' 171 | ? `ws://${window.location.host}` 172 | : `wss://${window.location.host}`; 173 | 174 | // 定义一个用户。该用户应该是由应用服务器自动获取当前用户身份 175 | // 编辑服务需要提供用户id以及用户的显示名。 176 | const user = { 177 | userId: `${new Date().valueOf()}`, 178 | displayName: 'test user', 179 | avatarUrl: 'xxx', 180 | }; 181 | 182 | // 设置编辑器选项 183 | const options = { 184 | serverUrl: WsServerUrl, 185 | }; 186 | 187 | // 从应用服务器获取一个AccessToken。应用服务器需要负责验证用户对文档的访问权限。 188 | // accessToken采用jwt规范,里面应该包含用户的userId,文档的docId,以及编辑应用的AppId。 189 | // 下面是一个演示例子。在正常情况下,AccessToken应该通过用户自己的应用服务器生成。 190 | // 因为在前端使用JWT加密规范的时候,必须在https协议下面的网页才可以使用。为了演示, 191 | // 我们自带的测试服务器会提供一个虚拟的token生成功能。(启动服务的时候,需要指定--enable-fake-token-api 参数) 192 | // 请勿在正式服务器上面,启用这个参数。 193 | 194 | async function fakeGetAccessTokenFromServer(userId: string, docId: string): Promise { 195 | // 196 | const data = { 197 | userId, 198 | docId, 199 | appId: AppId, 200 | }; 201 | 202 | const fromHexString = (hexString: string) => { 203 | const parts = hexString.match(/.{1,2}/g); 204 | assert(parts); 205 | const arr = parts.map((byte) => parseInt(byte, 16)); 206 | return new Uint8Array(arr); 207 | }; 208 | // 209 | const key = fromHexString(AppSecret); 210 | 211 | try { 212 | const accessToken = await new EncryptJWT(data) 213 | .setProtectedHeader({ alg: 'dir', enc: 'A256GCM' }) 214 | .setIssuedAt() 215 | .setIssuer(AppDomain) 216 | .setExpirationTime('120s') 217 | .encrypt(key); 218 | 219 | return accessToken; 220 | } catch (err) { 221 | const res = await fetch(`http://${window.location.host}/token/${AppId}/${docId}/${userId}`); 222 | const ret = await res.json(); 223 | return ret.token; 224 | } 225 | } 226 | 227 | // 文档id 228 | const docId = 'my-test-doc-id'; 229 | 230 | (async function loadDocument() { 231 | // 验证身份,获取accessToken 232 | const token = await fakeGetAccessTokenFromServer(user.userId, docId); 233 | 234 | // 生成编辑服务需要的认证信息 235 | const auth = { 236 | appId: AppId, 237 | userId: user.userId, 238 | displayName: 'name', 239 | avatarUrl: 'xxx', 240 | docId, 241 | token, 242 | permission: 'w', 243 | }; 244 | 245 | // 创建编辑器并加载文档 246 | const editor = createEditor(document.getElementById('editor') as HTMLElement, options, auth); 247 | })(); 248 | 249 | ``` 250 | 251 | 通过上面的代码,就可以在自己的应用中,创建一个多人实时协同编辑器。 252 | 上面的代码,可以通过在h5例子下面,运行npm run simple来查看效果。 253 | 254 | [查看源码](./h5/src/simple.ts) 255 | 256 | ## react 组件 257 | [wiz-editor-react](https://github.com/WizTeam/wiz-editor-react) 258 | 259 | ## 直接在浏览器里面使用(无需npm) 260 | 261 | ```html 262 | 263 | 264 | 283 | ``` 284 | 285 | ## 扩展编辑器功能 286 | 287 | [编辑器结构](./docs/zh-CN/editor-structure.md) 288 | 289 | ### 自定义@xxx提醒用户功能 290 | 291 | [增加插入提醒功能](./docs/zh-CN/mention.md) 292 | 293 | ### 响应评论消息 294 | 295 | [响应评论消息](./docs/zh-CN/comment.md) 296 | 297 | ### 自定义inline的模块(box) 298 | 299 | [自定义box说明](./docs/zh-CN/box.md) 300 | 301 | ### 自定义block按钮/菜单 302 | 303 | [自定义block按钮/菜单](./docs/zh-CN/custom-context-menu.md) 304 | 305 | ### 自定义文字样式 306 | 307 | [自定义文字样式](./docs/zh-CN/custom-style.md) 308 | 309 | ### 自定义模版 310 | 311 | [自定义文档模版](./docs/zh-CN/custom-template.md) 312 | 313 | ### 自定义block 314 | 315 | [编辑器结构](./docs/zh-CN/editor-structure.md) 316 | 317 | [自定义Embed类型block](./docs/zh-CN/embed-block.md) 318 | 319 | [自定义Complex类型block](./docs/zh-CN/complex-block.md) 320 | 321 | ## 服务端 322 | 323 | [wiz-editor 服务端适配](./docs/zh-CN/server.md) 324 | 325 | [wiz-editor 服务端架构介绍](./docs/zh-CN/server-architecture.md) 326 | 327 | 328 | ## 完全作为本地编辑器使用,无需依赖服务端 329 | [local demo](/local) 330 | 331 | ## 通过cdn使用编辑器代码 332 | [CDN demo](/cdn) 333 | 334 | 335 | ## 自定义UI 336 | [自定义浮动工具栏](./docs/zh-CN/custom-text-toolbar.md) 337 | 338 | ## 响应编辑器事件 339 | [响应编辑器事件](./docs/zh-CN/handle-message.md) 340 | 341 | ## 给在线用户发送自定义消息 342 | [发送自定义消息](./docs/zh-CN/post-custom-message.md) 343 | -------------------------------------------------------------------------------- /cdn/README.md: -------------------------------------------------------------------------------- 1 | # 通过cdn使用WizEditor 2 | 3 | 可以直接使用jsdelivr来加载编辑器代码(可以自行替换所需的版本号) 4 | 5 | ``` 6 | https://cdn.jsdelivr.net/npm/wiz-editor@0.0.44/client/src/index.js 7 | ``` 8 | 9 | ## 代码 10 | 11 | ```html 12 | 13 | 14 | 33 | ``` 34 | -------------------------------------------------------------------------------- /cdn/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Wiz Editor Demo 5 | 6 | 7 | 8 | 9 |
10 | 11 | 147 | 148 | -------------------------------------------------------------------------------- /docs/zh-CN/assets/complex-block.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WizTeam/wiz-editor/bbe0787308acf8e667404dca322492b0ef941f69/docs/zh-CN/assets/complex-block.png -------------------------------------------------------------------------------- /docs/zh-CN/assets/custom-context-menu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WizTeam/wiz-editor/bbe0787308acf8e667404dca322492b0ef941f69/docs/zh-CN/assets/custom-context-menu.png -------------------------------------------------------------------------------- /docs/zh-CN/assets/editor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WizTeam/wiz-editor/bbe0787308acf8e667404dca322492b0ef941f69/docs/zh-CN/assets/editor.png -------------------------------------------------------------------------------- /docs/zh-CN/assets/embed-block.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WizTeam/wiz-editor/bbe0787308acf8e667404dca322492b0ef941f69/docs/zh-CN/assets/embed-block.png -------------------------------------------------------------------------------- /docs/zh-CN/assets/server-architecture-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WizTeam/wiz-editor/bbe0787308acf8e667404dca322492b0ef941f69/docs/zh-CN/assets/server-architecture-1.png -------------------------------------------------------------------------------- /docs/zh-CN/assets/server-architecture-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WizTeam/wiz-editor/bbe0787308acf8e667404dca322492b0ef941f69/docs/zh-CN/assets/server-architecture-2.png -------------------------------------------------------------------------------- /docs/zh-CN/box.md: -------------------------------------------------------------------------------- 1 | # wiz-editor 自定义box方法 2 | 3 | wiz-editor里面,可以在文本中插入自定义类型数据,例如用户提醒(@xxx),日历事件,等等。 4 | box类型,在html里面,由一个span标签包含,可以在这个span里面,包含image和文字。 5 | 这个span,将会作为一个整体出现在编辑器里面,不可以对里面的内容进行编辑。 6 | 7 | 8 | ## box定义: 9 | 10 | ```ts 11 | interface Box { 12 | prefix?: string; 13 | suggestPlaceholder?: string; 14 | createNode: (data: BoxData) => BoxNode, 15 | getItems?: (editor: Editor, keywords: string) => Promise; 16 | createBoxDataFromItem?: (editor: Editor, item: AutoSuggestData) => BoxTemplateData; 17 | createBoxData?: (editor: Editor) => Promise; 18 | handleBoxInserted?: (editor: Editor, data: BoxData) => void; 19 | handleBoxClicked?: (editor: Editor, data: BoxData) => void; 20 | handleBoxItemSelected?: (editor: Editor, item: AutoSuggestData) => void; 21 | }; 22 | ``` 23 | 24 | ### prefix:(可选) 25 | 是指触发输入box的文字,例如对于提醒类型的box,prefix为@。当用户在键盘输入@之后,会自动触发插入提醒的操作。 26 | prefix可以是一个或者多个字符,也可以没有。如果没有prefix,则用户只能通过工具栏/菜单来插入box。 27 | 28 | ### suggestPlaceholder:(可选) 29 | 在通过键盘触发插入box事件之后,编辑器可能会自动显示一个auto suggest(依赖于是否实现了getItems方法)。 30 | 在用户继续输入内容的时候,auto suggest会自动过滤items,如果没有找到匹配的items,则会显示这个placeholder。 31 | 32 | ### createNode:(必要) 33 | 用来向编辑器描述如何创建一个box。该方法返回BoxNode类型。 34 | 35 | ```ts 36 | 37 | interface BoxChild { 38 | type: 'text' | 'image'; 39 | classes?: string[]; 40 | attributes?: { 41 | [index: string]: string 42 | }; 43 | }; 44 | 45 | interface BoxTextChild extends BoxChild { 46 | text: string; 47 | }; 48 | 49 | interface BoxImageChild extends BoxChild { 50 | src: string; 51 | alt?: string; 52 | }; 53 | 54 | interface BoxNode { 55 | classes?: string[]; 56 | attributes?: { 57 | [index: string]: string 58 | }; 59 | children?: BoxChild[]; 60 | }; 61 | ``` 62 | 63 | ### getItems, createBoxDataFromItem: (可选) 64 | 如果提供了getItems方法,在用户输入prefix之后,将会调用这个方法,获取一个auto suggest列表,并显示给用户进行选择。 65 | 如果用户继续输入内容,那么编辑器会不断调用这个方法(传入的keywords不同)。该方法应该根据keywords,返回相应的数据。 66 | 67 | ```ts 68 | interface AutoSuggestData { 69 | iconUrl: string; 70 | text: string; 71 | id: string; 72 | data: any, 73 | }; 74 | ``` 75 | 76 | 如果用户通过auto suggest选择了一个item,那么编辑器会调用createBoxDataFromItem方法,来获取一个box数据。 77 | 应用需要实现该方法,将用户选择的item,转换为一个box数据。 78 | 79 | ```ts 80 | interface BoxTemplateData { 81 | [index: string]: string | boolean | number | undefined, 82 | }; 83 | ``` 84 | 85 | 如果没有提供getItems方法,并且提供了prefix,那么在用户输入prefix之后,会自动调用createBoxData方法,创建一个box并且自动插入。其中没有任何交互。 86 | 87 | ### createBoxData (可选) 88 | 89 | 要求创建一个box数据,该方法为异步方法。如果无法创建,可以返回null。 90 | 91 | 92 | ### handleBoxInserted, handleBoxClicked, handleBoxItemSelected: (可选) 93 | 94 | 响应用户交互事件。 95 | 96 | 例如当某一个box被插入的时候,可能会触发某些消息的产生。可以在相应的方法里面进行处理。 97 | 98 | ## 向编辑器注册box 99 | 100 | ```ts 101 | import { 102 | boxUtils, 103 | } from 'wiz-editor/client'; 104 | 105 | const someBox = { 106 | ... 107 | }; 108 | 109 | boxUtils.registerBoxType('some_box_type' as BOX_TYPE, someBox); 110 | ``` 111 | 112 | 其中box的类型需要唯一。如果使用typescript,需要显示将类型字符串转换为BOX_TYPE类型。 113 | 114 | ## 相关例子 115 | 116 | 1. [增加插入日历事件功能](./calendar.md) (通过用户选择的item,创建不同的box) 117 | 2. [增加插入日期功能](./date.md) (通过快捷指令,输入当前日期) 118 | 3. [增加插入label功能](./label.md) (通过用户选择,插入不同的label) 119 | 4. [自定义box输入界面](./custom-suggest.md) (允许用户输入插入box的数据) 120 | -------------------------------------------------------------------------------- /docs/zh-CN/calendar.md: -------------------------------------------------------------------------------- 1 | # 演示:在编辑器里面插入日历事件 2 | 3 | 运行demo 4 | 5 | ``` 6 | cd h5 7 | npm run calendar 8 | ``` 9 | 10 | [查看源码](../../h5/src/box_calendar.ts) 11 | 12 | 主要代码 13 | 14 | ```ts 15 | const CALENDAR_IMAGE_URL = 'https://www.wiz.cn/wp-content/new-uploads/b75725f0-4008-11eb-8f21-01eb48012b63.jpeg'; 16 | const CALENDAR_BOX_TYPE = 'calendar'; 17 | 18 | interface CalendarBoxData extends BoxData { 19 | text: string; 20 | }; 21 | 22 | function createNode(data: BoxData): BoxNode { 23 | // 24 | const { text } = data as CalendarBoxData; 25 | // 26 | return { 27 | classes: ['box-mention'], 28 | children: [{ 29 | type: 'image', 30 | src: CALENDAR_IMAGE_URL, 31 | attributes: { 32 | class: '.calendar_image', 33 | }, 34 | } as BoxImageChild, { 35 | type: 'text', 36 | text, 37 | } as BoxTextChild], 38 | }; 39 | } 40 | 41 | function handleBoxInserted(editor: Editor, data: BoxData): void { 42 | const calendarData = data as CalendarBoxData; 43 | console.log('calendar box inserted:', calendarData); 44 | } 45 | 46 | function handleBoxClicked(editor: Editor, data: BoxData): void { 47 | const calendarData = data as CalendarBoxData; 48 | alert(`calendar clicked: ${calendarData.text}`); 49 | } 50 | 51 | async function getItems(editor: Editor, keywords: string) { 52 | console.log(keywords); 53 | return [{ 54 | iconUrl: CALENDAR_IMAGE_URL, 55 | text: 'Select one event...', 56 | id: 'selectEvent', 57 | data: '', 58 | }, { 59 | iconUrl: CALENDAR_IMAGE_URL, 60 | text: 'Create one event...', 61 | id: 'createEvent', 62 | data: '', 63 | }]; 64 | } 65 | 66 | function handleBoxItemSelected(editor: Editor, item: AutoSuggestData): void { 67 | // 68 | const pos = editor.saveCaretPos(); 69 | // 70 | if (item.id === 'selectEvent') { 71 | alert('select one event'); 72 | // 73 | } else if (item.id === 'createEvent') { 74 | alert('create one event'); 75 | // 76 | } 77 | // 78 | if (!editor.tryRestoreCaretPos(pos)) { 79 | return; 80 | } 81 | // 82 | editor.insertBox(CALENDAR_BOX_TYPE as BOX_TYPE, null, { 83 | text: new Date().toLocaleDateString(), 84 | }, { 85 | deletePrefix: true, 86 | }); 87 | } 88 | 89 | const calendarBox = { 90 | prefix: '//', 91 | createNode, 92 | getItems, 93 | handleBoxItemSelected, 94 | handleBoxInserted, 95 | handleBoxClicked, 96 | }; 97 | 98 | boxUtils.registerBoxType(CALENDAR_BOX_TYPE as BOX_TYPE, calendarBox); 99 | ``` 100 | 101 | 插入calendar事件例子中,实现了根据用户选择的item,进行不同的响应,然后插入不同(或者相同)的box。 102 | -------------------------------------------------------------------------------- /docs/zh-CN/comment.md: -------------------------------------------------------------------------------- 1 | # 响应评论消息 2 | 3 | 编辑器允许评论。可以通过callback来响应评论创建和回复。 4 | 5 | ```ts 6 | function handleCommentInserted(editor: Editor, commentId: string, commentDocText: string, commentText: string, selectedBlock: SelectedBlock): void { 7 | // commentDocText: 文档中被评论的文字内容 8 | // commentText: 评论内容 9 | console.log(`comment created: ${commentText}`); 10 | assert(selectedBlock); 11 | } 12 | 13 | function handleCommentReplied(editor: Editor, toUserId: string, orgCommentText: string, commentText: string): void { 14 | assert(commentText); 15 | // toUserId:被评论的用户id 16 | // orgCommentText: 被回复的评论内容 17 | // commentText:评论内容 18 | console.log(`comment replied to ${toUserId}: ${commentText}`); 19 | } 20 | 21 | const options = { 22 | ... 23 | callbacks: { 24 | ... 25 | onCommentInserted: handleCommentInserted, 26 | onCommentReplied: handleCommentReplied, 27 | }, 28 | }; 29 | ``` 30 | -------------------------------------------------------------------------------- /docs/zh-CN/complex-block.md: -------------------------------------------------------------------------------- 1 | # 创建自定义的 complex block 2 | 3 | wiz-editor支持增加自定义的complex类型的block。这种block类似与表格,code这样的block。 4 | 这种block里面,又包含了一个或者多个container,每一个container里面,都可以进行独立编辑, 5 | 插入除了complex block之外的所有block(也可以仅允许插入指定类型的block,例如code)。 6 | 7 | 要实现一个complex 类型的block,必须实现下面的方法 8 | 9 | ```ts 10 | export interface Block { 11 | //block options 12 | getBlockOptions?: () => BlockOptions; 13 | createBlockTemplateData: (editor: Editor, options: any) => BlockTemplateData; 14 | createBlockContent: (editor: Editor, id: string, data: BlockData) => BlockContentElement; 15 | updateBlockData: (block: BlockElement, data: BlockData) => void; 16 | saveData: (block: BlockElement) => BlockData; 17 | getChildContainersData?: (block: BlockElement) => ContainerData[]; 18 | getCaretPos: (block: BlockElement, node: Node, nodeOffset: number) => number; 19 | createRange: (block: BlockElement, pos: number) => Range; 20 | getSubContainerInComplexBlock?: (block: BlockElement, element: HTMLElement, type: 'top' | 'right' | 'bottom' | 'left') => ContainerElement | null; 21 | replaceChildrenId?: (doc: EditorDoc, data: DocBlock) => void; 22 | } 23 | ``` 24 | 25 | 实现Block之后,需要进行注册: 26 | 27 | ```ts 28 | import { 29 | blockUtils, 30 | } from 'wiz-editor/client'; 31 | 32 | const testBlock = { 33 | ... 34 | }; 35 | 36 | blockUtils.registerEmbed('test-block' as BLOCK_TYPE, testBlock); 37 | ``` 38 | 39 | demo 40 | 41 | ![complex-block](./assets/complex-block.png) 42 | 43 | ```ts 44 | const TEST_BLOCK_TYPE = 'test'; 45 | // ------------------ create a custom complex block ------- 46 | (() => { 47 | interface TestComplexBlockTemplateData { 48 | imgSrc: string; 49 | }; 50 | 51 | interface TestComplexBlockData extends TestComplexBlockTemplateData, BlockData { 52 | } 53 | 54 | function createBlockTemplateData(editor: Editor, options: TestComplexBlockTemplateData) { 55 | // 56 | const blocks = [ 57 | blockUtils.createBlockData(editor, BLOCK_TYPE.TEXT, { 58 | text: new RichTextDocument([]), 59 | }), 60 | ]; 61 | // 62 | const containerId = editor.createEmptyChildContainerData(blocks, genId()); 63 | const children = [containerId]; 64 | // 65 | return { 66 | children, 67 | ...options, 68 | }; 69 | } 70 | 71 | function createBlockContent(editor: Editor, id: string, data: BlockData): BlockContentElement { 72 | // 73 | assert(data); 74 | const blockData = data as TestComplexBlockData; 75 | const blockContent = document.createElement('div') as unknown as BlockContentElement; 76 | blockContent.style.border = '1px solid'; 77 | blockContent.style.display = 'flex'; 78 | blockContent.style.alignItems = 'end'; 79 | const img = document.createElement('img'); 80 | img.src = blockData.imgSrc; 81 | blockContent.appendChild(img); 82 | // 83 | assert(blockData.children); 84 | assert(blockData.children.length === 1); 85 | const subContainerId = blockData.children[0]; 86 | const containerBlocks = editor.getChildContainerData(subContainerId); 87 | const container = editor.createChildContainer(blockContent, subContainerId, containerBlocks); 88 | assert(container); 89 | return blockContent; 90 | } 91 | 92 | function getChildImage(block: BlockElement): HTMLImageElement { 93 | assert(blockUtils.isBlock(block)); 94 | assert(blockUtils.getBlockType(block) === (TEST_BLOCK_TYPE as any)); 95 | const content = blockUtils.getBlockContent(block); 96 | assert(content.children.length === 2); 97 | assert(content.children[0] instanceof HTMLImageElement); 98 | return content.children[0]; 99 | } 100 | 101 | function getChildContainer(block: BlockElement): ContainerElement { 102 | assert(blockUtils.isBlock(block)); 103 | assert(blockUtils.getBlockType(block) === (TEST_BLOCK_TYPE as any)); 104 | const content = blockUtils.getBlockContent(block); 105 | assert(content.children.length === 2); 106 | const container = content.children[1] as ContainerElement; 107 | return container; 108 | } 109 | 110 | function saveData(block: BlockElement): BlockData { 111 | assert(block); 112 | // 113 | const subContainer = getChildContainer(block); 114 | const subContainerId = containerUtils.getContainerId(subContainer); 115 | const children = [subContainerId]; 116 | const id = blockUtils.getBlockId(block); 117 | const image = getChildImage(block); 118 | // 119 | const blockData: TestComplexBlockData = { 120 | id, 121 | type: TEST_BLOCK_TYPE as any, 122 | text: new RichTextDocument([]), 123 | children, 124 | imgSrc: image.src, 125 | }; 126 | return blockData; 127 | } 128 | 129 | function updateBlockData(block: BlockElement, data: BlockData) { 130 | // 131 | const newData = data as TestComplexBlockData; 132 | // 133 | const oldData = saveData(block) as TestComplexBlockData; 134 | assert(oldData.children); 135 | assert(newData.children); 136 | assert(oldData.children[0] === newData.children[0]); 137 | // 138 | if (oldData.imgSrc !== newData.imgSrc) { 139 | // 140 | const image = getChildImage(block); 141 | image.src = newData.imgSrc; 142 | } 143 | } 144 | 145 | function getChildContainersData(block: BlockElement): ContainerData[] { 146 | // 147 | const content = getChildContainer(block); 148 | const containerId = containerUtils.getContainerId(content); 149 | const blocks: BlockData[] = []; 150 | containerUtils.getAllBlocks(content).forEach((childBlock) => { 151 | const blockData = blockUtils.saveData(childBlock); 152 | blocks.push(blockData); 153 | }); 154 | return [{ 155 | id: containerId, 156 | blocks, 157 | }]; 158 | } 159 | 160 | // eslint-disable-next-line no-unused-vars 161 | function getCaretPos(block: BlockElement, node: Node, nodeOffset: number): number { 162 | const container = getChildContainer(block); 163 | if (node === container) { 164 | assert(nodeOffset === 0 || nodeOffset === 1); 165 | return nodeOffset; 166 | } 167 | // sub blocked has been deleted, ignore 168 | return 0; 169 | } 170 | 171 | function createRange(block: BlockElement, pos: number): Range { 172 | assert(block); 173 | assert(pos === 0 || pos === -1 || pos === 1); 174 | // 175 | const container = getChildContainer(block); 176 | assert(container); 177 | const blocks = containerUtils.getAllBlocks(container); 178 | const childBlock = pos === 0 ? blocks[0] : blocks[blocks.length - 1]; 179 | assert(childBlock); 180 | const offset = pos === 0 ? 0 : -1; 181 | const ret = blockUtils.createRange(childBlock, offset); 182 | return ret; 183 | } 184 | 185 | function getSubContainerInComplexBlock(block: BlockElement, 186 | element: HTMLElement, type: 'top' | 'right' | 'bottom' | 'left') { 187 | // 188 | assert(type); 189 | return null; 190 | } 191 | 192 | function getBlockOptions(): BlockOptions { 193 | return { 194 | textBlock: false, 195 | complexBlock: true, 196 | }; 197 | } 198 | 199 | function replaceChildrenId(editorDoc: EditorDoc, blockData: DocBlock): void { 200 | const doc = editorDoc; 201 | const children = blockData.children; 202 | assert(children); 203 | const oldId = children[0]; 204 | assert(oldId); 205 | const newId = genId(); 206 | doc[newId] = doc[oldId]; 207 | delete doc[oldId]; 208 | // eslint-disable-next-line no-param-reassign 209 | blockData.children = [newId]; 210 | } 211 | 212 | // eslint-disable-next-line no-unused-vars 213 | function executeBlockCommand(block: BlockElement, command: BlockCommand, params?: CommandParams): any { 214 | assert(blockUtils.getBlockType(block) === (TEST_BLOCK_TYPE as any)); 215 | // 216 | } 217 | 218 | // eslint-disable-next-line no-unused-vars 219 | function handleBlockLoaded(block: BlockElement) { 220 | // 221 | } 222 | 223 | function accept(type: BOX_TYPE | BLOCK_TYPE): boolean { 224 | assert(type); 225 | // accept all non-complex blocks 226 | return true; 227 | } 228 | 229 | const TestComplex: Block = { 230 | getBlockOptions, 231 | createBlockTemplateData, 232 | createBlockContent, 233 | updateBlockData, 234 | saveData, 235 | getChildContainersData, 236 | getCaretPos, 237 | createRange, 238 | getSubContainerInComplexBlock, 239 | replaceChildrenId, 240 | executeBlockCommand, 241 | handleBlockLoaded, 242 | accept, 243 | }; 244 | 245 | blockUtils.registerBlockType(TEST_BLOCK_TYPE as any, TestComplex); 246 | })(); 247 | 248 | ... 249 | 250 | document.getElementById('complex-block')?.addEventListener('click', () => { 251 | assert(currentEditor); 252 | const blockData = blockUtils.createBlockData(currentEditor, TEST_BLOCK_TYPE as any, { 253 | imgSrc: CALENDAR_IMAGE_URL, 254 | }); 255 | currentEditor.insertBlock(null, -2, 'test' as any, blockData, { 256 | fromUndo: false, 257 | focusToBlock: true, 258 | localAction: true, 259 | }); 260 | }); 261 | 262 | ``` 263 | 264 | [查看例子](../../h5/src/custom.ts) 265 | -------------------------------------------------------------------------------- /docs/zh-CN/custom-context-menu.md: -------------------------------------------------------------------------------- 1 | # 演示:添加自定义工具栏按钮/菜单 2 | 3 | 在创建编辑器的时候,可以通过选项,增加自定义按钮和菜单项。 4 | 5 | ![自定义右键菜单](./assets/custom-context-menu.png) 6 | 7 | 下面是一个例子: 8 | 9 | ```ts 10 | // 当用户点击菜单的时候,将会执行相应的方法 11 | function handleMenuItemClicked(item: MenuItemData) { 12 | console.log(item); 13 | assert(currentEditor); 14 | if (item.id === 'get-selected-text') { 15 | alert(`selected text: ${currentEditor.getSelectedText()}`); 16 | } else if (item.id === 'add-border') { 17 | currentEditor.applyTextCustomStyle('style-border'); 18 | } else if (item.id === 'add-strikethrough') { 19 | currentEditor.applyTextCustomStyle('style-strikethrough'); 20 | } 21 | ... 22 | } 23 | 24 | function handleGetBlockCommand(editor: Editor, block: BlockElement, detail: SelectionDetail, type: 'fixed' | 'hover' | 'menu'): CommandItemData[] { 25 | if (!blockUtils.isTextTypeBlock(block)) { 26 | return []; 27 | } 28 | // 29 | const ret: CommandItemData[] = []; 30 | if (type === 'menu') { 31 | // 自定义右键菜单 32 | if (detail.collapsed) { 33 | ret.push({ 34 | id: 'toHeading2', 35 | text: '转换为 标题二(demo)', 36 | shortCut: '', 37 | disabled: false, 38 | onClick: handleMenuItemClicked, 39 | }, { 40 | id: 'toOrderedList', 41 | text: '转换为 有序列表(demo)', 42 | shortCut: '', 43 | disabled: false, 44 | onClick: handleMenuItemClicked, 45 | }, { 46 | id: 'toUnorderedList', 47 | text: '转换为 无序列表(demo)', 48 | shortCut: '', 49 | disabled: false, 50 | onClick: handleMenuItemClicked, 51 | }); 52 | } 53 | assert(detail.startBlock); 54 | const blockType = blockUtils.getBlockType(detail.startBlock); 55 | if (blockType === BLOCK_TYPE.LIST) { 56 | ret.push({ 57 | id: 'list/indent', 58 | text: '列表增加缩进(demo)', 59 | shortCut: '', 60 | disabled: false, 61 | onClick: handleMenuItemClicked, 62 | }, { 63 | id: 'list/outdent', 64 | text: '列表减少缩进(demo)', 65 | shortCut: '', 66 | disabled: false, 67 | onClick: handleMenuItemClicked, 68 | }); 69 | } 70 | return ret; 71 | } 72 | 73 | // 自定义浮动工具栏按钮 74 | if (type === 'hover') { 75 | ret.push( 76 | { 77 | id: 'add-border', 78 | text: '添加边框', 79 | shortCut: '', 80 | disabled: false, 81 | onClick: handleMenuItemClicked, 82 | }, 83 | { 84 | id: 'add-strikethrough', 85 | text: '添加删除线', 86 | shortCut: '', 87 | disabled: false, 88 | onClick: handleMenuItemClicked, 89 | }, 90 | ); 91 | } 92 | 93 | // 自定义block 固定显示的按钮(block右侧) 94 | if (type === 'fixed') { 95 | ret.push({ 96 | id: 'insert-project', 97 | text: '插入项目', 98 | shortCut: '', 99 | disabled: false, 100 | icon: '', 101 | data: block, 102 | onClick: handleMenuItemClicked, 103 | }); 104 | } 105 | 106 | return ret; 107 | } 108 | 109 | ... 110 | 111 | const options = { 112 | ... 113 | callbacks: { 114 | ... 115 | // 设置自定义按钮/菜单回调方法 116 | onGetBlockCommand: handleGetBlockCommand, 117 | }, 118 | }; 119 | }; 120 | ``` 121 | 122 | 123 | 124 | -------------------------------------------------------------------------------- /docs/zh-CN/custom-style.md: -------------------------------------------------------------------------------- 1 | # 自定义文字样式 2 | 3 | 选中文字后,可以给文字添加任意的css样式。 4 | 5 | 例如: 6 | 7 | ```ts 8 | editor.applyTextCustomStyle('style-border'); 9 | ``` 10 | 11 | 其中自定义样式名,必须以`style-`开头。然后在页面内(或者引入的css文件内),声明这个样式,例如: 12 | 13 | ```css 14 | .style-border { 15 | border: 1px solid blue; 16 | } 17 | ``` 18 | 19 | 可以通过[自定义菜单](./custom-context-menu.md),来执行添加样式的功能。也可以通过添加工具栏按钮等方式添加样式。 20 | -------------------------------------------------------------------------------- /docs/zh-CN/custom-suggest.md: -------------------------------------------------------------------------------- 1 | # 自定义插入box下拉框 2 | 3 | wiz-editor可以在插入box之前,通过自定义auto suggest,让用户在插入box之前进行内容输入,从而生成特定的box。例如可以让用户在一个日历组件中选择日期,然后再插入。下面是一个例子: 4 | 5 | ```ts 6 | // -------------------- custom render suggest 7 | // 定义box类型 8 | const CUSTOM_SUGGEST_BOX_TYPE = 'custom_render'; 9 | 10 | (() => { 11 | // box数据内容 12 | interface CustomSuggestBoxData extends BoxData { 13 | text: string; 14 | }; 15 | 16 | // 定义box dom结构 17 | function createNode(editor: Editor, data: BoxData): BoxNode { 18 | assert(data); 19 | const boxData = data as CustomSuggestBoxData; 20 | return { 21 | classes: ['box-mention'], 22 | children: [{ 23 | type: 'text', 24 | text: boxData.text, 25 | } as BoxTextChild], 26 | }; 27 | } 28 | 29 | // 当box被插入之后调用 30 | function handleBoxInserted(editor: Editor, data: BoxData, 31 | block: BlockElement, pos: number): void { 32 | assert(pos >= 0); 33 | const boxData = data as CustomSuggestBoxData; 34 | console.log(`project box inserted: ${boxData.text}`); 35 | } 36 | 37 | // 响应box被点击事件 38 | function handleBoxClicked(editor: Editor, data: BoxData): void { 39 | assert(data); 40 | const boxData = data as CustomSuggestBoxData; 41 | alert(`custom suggest clicked: ${boxData.text}`); 42 | } 43 | 44 | // 获取item。只需要返回一个item(我们会自定义这个item的渲染) 45 | async function getItems(editor: Editor, keywords: string): Promise { 46 | console.log(keywords); 47 | return [{ 48 | iconUrl: '', 49 | text: '', 50 | id: 'custom-suggest-id', 51 | data: null, 52 | }]; 53 | } 54 | 55 | // 通过item返回数据,不会被调用。我们会自己控制插入的box数据 56 | function createBoxDataFromItem(editor: Editor, item: AutoSuggestData): BoxTemplateData { 57 | assert(item); 58 | return {}; 59 | } 60 | 61 | // 屏蔽内置的item点击事件,避免点击的时候将auto suggest被自动关闭 62 | function handleClick(event: MouseEvent) { 63 | event.preventDefault(); 64 | event.stopPropagation(); 65 | } 66 | 67 | // 渲染下拉框。 我们只有一个item,在这里返回item的内容 68 | function renderAutoSuggestItem(editor: Editor, suggestData: AutoSuggestData): HTMLElement { 69 | assert(suggestData); 70 | const div = document.createElement('div'); 71 | div.style.minHeight = '100px'; 72 | div.style.minWidth = '300px'; 73 | div.style.width = '100%'; 74 | div.style.cursor = 'auto'; 75 | div.style.border = '1px sold #ccc'; 76 | div.style.backgroundColor = 'white'; 77 | // 78 | div.style.display = 'flex'; 79 | div.style.flexDirection = 'column'; 80 | // 81 | const textArea = document.createElement('textarea'); 82 | div.appendChild(textArea); 83 | textArea.style.userSelect = 'auto'; 84 | textArea.style.flexGrow = '1'; 85 | textArea.style.padding = '8px'; 86 | const button = document.createElement('button'); 87 | button.innerText = 'OK'; 88 | div.appendChild(button); 89 | // 90 | // 屏蔽点击事件。避免autosuggest自动关闭 91 | div.onclick = handleClick; 92 | // 93 | // 注意保存光标位置 94 | const selectionState = editor.saveSelectionState(); 95 | // 96 | setTimeout(() => { 97 | // 自动将焦点设置到textarea里面 98 | textArea.focus(); 99 | }, 100); 100 | // 101 | button.onclick = () => { 102 | const text = textArea.value.replace(/\n/g, ' '); 103 | // 恢复光标 104 | editor.restoreSelectionState(selectionState); 105 | // 根据用户输入的内容,插入box 106 | editor.insertBox(CUSTOM_SUGGEST_BOX_TYPE as BOX_TYPE, null, { 107 | text, 108 | }, {}); 109 | // 关闭auto suggest 110 | editor.closeAutoSuggest(); 111 | }; 112 | // 113 | return div; 114 | } 115 | 116 | const customSuggestBox = { 117 | customSuggest: true, 118 | createNode, 119 | getItems, 120 | handleBoxInserted, 121 | handleBoxClicked, 122 | createBoxDataFromItem, 123 | renderAutoSuggestItem, 124 | }; 125 | 126 | boxUtils.registerBoxType(CUSTOM_SUGGEST_BOX_TYPE as BOX_TYPE, customSuggestBox); 127 | })(); 128 | 129 | ... 130 | 131 | document.getElementById('custom-suggest')?.addEventListener('click', () => { 132 | assert(currentEditor); 133 | // 在光标位置,请求显示auto suggest,让用户输入一个特定的box data 134 | currentEditor.insertBox(CUSTOM_SUGGEST_BOX_TYPE as any, null, {}, { 135 | showAutoSuggest: true, 136 | }); 137 | }); 138 | ``` 139 | 140 | 141 | 如果要创建一个box,可以通过点击按钮直接插入这个box,并且进入编辑状态(类似飞书在任务中插入结束日期的方式),则可以按照下面的方式实现: 142 | 143 | ```ts 144 | // -------------------- project list 145 | const PROJECT_BOX_TYPE = 'project'; 146 | (() => { 147 | interface ProjectBoxData extends BoxData { 148 | projectId: string; 149 | projectName: string; 150 | }; 151 | 152 | function createNode(editor: Editor, data: BoxData): BoxNode { 153 | // 154 | const { projectName } = data as ProjectBoxData; 155 | // 156 | return { 157 | classes: ['box-mention'], 158 | children: [{ 159 | type: 'text', 160 | text: projectName, 161 | } as BoxTextChild], 162 | }; 163 | } 164 | 165 | function handleBoxInserted(editor: Editor, data: BoxData, 166 | block: BlockElement, pos: number): void { 167 | assert(pos >= 0); 168 | const projectData = data as ProjectBoxData; 169 | console.log('project box inserted:', projectData); 170 | } 171 | 172 | function handleBoxClicked(editor: Editor, data: BoxData, block: BlockElement): void { 173 | const projectData = data as ProjectBoxData; 174 | assert(block); 175 | editor.editBox(projectData, block); 176 | } 177 | 178 | const PROJECT_LIST: { 179 | projectId: string; 180 | projectName: string; 181 | }[] = []; 182 | for (let i = 0; i < 10; i++) { 183 | PROJECT_LIST.push({ 184 | projectId: `${i}`, 185 | projectName: `项目${i}`, 186 | }); 187 | } 188 | 189 | async function getItems(editor: Editor, keywords: string): Promise { 190 | assert(keywords !== undefined); 191 | // 192 | return [{ 193 | iconUrl: '', 194 | text: '', 195 | id: '', 196 | data: '', 197 | }]; 198 | } 199 | 200 | function createBoxDataFromItem(editor: Editor, item: AutoSuggestData): BoxTemplateData { 201 | const data: ProjectBoxData = item.data; 202 | return { 203 | projectId: data.projectId, 204 | projectName: data.projectName, 205 | }; 206 | } 207 | 208 | async function createBoxData(editor: Editor) { 209 | assert(editor); 210 | return { 211 | projectId: '', 212 | projectName: 'Please select a project', 213 | }; 214 | } 215 | 216 | // 渲染下拉框。 我们只有一个item,在这里返回item的内容 217 | function renderAutoSuggestItem(editor: Editor, suggestData: AutoSuggestData, options: AutoSuggestOptions): HTMLElement { 218 | assert(suggestData); 219 | assert(options); 220 | assert(options.data); 221 | assert(options.data.boxData); 222 | const boxData = options.data.boxData as ProjectBoxData; 223 | const boxElem = editor.getBoxById(boxData.id); 224 | assert(boxElem); 225 | const block = containerUtils.getParentBlock(boxElem); 226 | assert(block); 227 | // 228 | const div = document.createElement('div'); 229 | domUtils.addClass(div, 'editor-project-box'); 230 | div.onclick = (event) => { 231 | event.stopPropagation(); 232 | }; 233 | // 234 | const projectSelect = domUtils.createElement('select', ['project-control'], div) as HTMLSelectElement; 235 | // 236 | PROJECT_LIST.forEach((project) => { 237 | const option = document.createElement('option'); 238 | option.text = project.projectName; 239 | option.value = project.projectId; 240 | projectSelect.options.add(option); 241 | }); 242 | 243 | projectSelect.onchange = () => { 244 | const index = projectSelect.selectedIndex; 245 | const option = projectSelect.options[index]; 246 | boxData.projectName = option.text; 247 | boxData.projectId = option.value; 248 | editor.updateBoxData(boxData.id, { 249 | ...boxData, 250 | }); 251 | }; 252 | 253 | // eslint-disable-next-line @typescript-eslint/no-shadow 254 | const updateData = (boxData: ProjectBoxData) => { 255 | projectSelect.value = boxData.projectId; 256 | }; 257 | 258 | updateData(boxData); 259 | // 260 | return div; 261 | } 262 | 263 | const projectBox = { 264 | customSuggest: true, 265 | insertDefaultThenEdit: true, // 插入一个默认的box,然后进入编辑状态 266 | createNode, 267 | getItems, 268 | handleBoxInserted, 269 | handleBoxClicked, 270 | createBoxDataFromItem, 271 | createBoxData, 272 | renderAutoSuggestItem, 273 | }; 274 | 275 | boxUtils.registerBoxType(PROJECT_BOX_TYPE as BOX_TYPE, projectBox); 276 | })(); 277 | 278 | 279 | function handleMenuItemClicked(event: Event, item: CommandItemData) { 280 | console.log(item); 281 | assert(currentEditor); 282 | if (item.id === 'insert-project') { 283 | const block = item.data as BlockElement; 284 | if (currentEditor.getSelectionDetail().startBlock !== block) { 285 | currentEditor.selectBlock(block, -1, -1); 286 | } 287 | currentEditor.insertEmptyBox(PROJECT_BOX_TYPE as any); 288 | } 289 | } 290 | 291 | function handleGetBlockCommand(editor: Editor, block: BlockElement, detail: SelectionDetail, type: 'fixed' | 'hover' | 'menu'): CommandItemData[] { 292 | if (!blockUtils.isTextTypeBlock(block)) { 293 | return []; 294 | } 295 | // 在text block 后面增加一个fixed的按钮,点击按钮,可以插入一个project 296 | if (type === 'fixed') { 297 | ret.push({ 298 | id: 'insert-project', 299 | text: '插入项目', 300 | shortCut: '', 301 | disabled: false, 302 | icon: '', 303 | data: block, 304 | onClick: handleMenuItemClicked, 305 | }); 306 | } 307 | 308 | return ret; 309 | } 310 | 311 | // editor options 312 | const options = { 313 | ... 314 | callbacks: { 315 | ... 316 | onGetBlockCommand: handleGetBlockCommand, 317 | }, 318 | }; 319 | ``` 320 | 321 | [完整的例子](../../h5/src/custom.ts) 322 | 323 | 运行demo: 324 | ``` 325 | cd h5 326 | npm run custom 327 | ``` -------------------------------------------------------------------------------- /docs/zh-CN/custom-template.md: -------------------------------------------------------------------------------- 1 | # 自定义模版-(模版临时解决方案,后期将会直接内置该功能) 2 | 3 | 1. 新建一篇文档 4 | 2. 按照要求进行编辑,然后将其中的某些内容替换成key,通过{{key}}定义需要替换的内容 5 | 3. 保存内容,获得文档json数据。这个json数据,就可以当成模版。 6 | 4. 下次新建文档的时候,将模版以及参数传递给编辑器。编辑器将会自动使用模版创建一篇新的文档。 7 | 8 | 9 | 例如模版json文件 10 | 11 | ```ts 12 | const DocTemplate = ` 13 | { 14 | "blocks": [ 15 | { 16 | "text": [ 17 | { 18 | "insert": "{{meet-name}} 会议" 19 | } 20 | ], 21 | "id": "_HNsxMNUe", 22 | "type": "heading", 23 | "level": 1 24 | }, 25 | { 26 | "text": [ 27 | { 28 | "insert": "参会人: {{names}}" 29 | } 30 | ], 31 | "id": "_iSAuPm5m", 32 | "type": "text" 33 | }, 34 | { 35 | "text": [ 36 | { 37 | "insert": "会议日期: {{date}}" 38 | } 39 | ], 40 | "id": "_AgSRfkl_", 41 | "type": "text" 42 | }, 43 | { 44 | "text": [ 45 | { 46 | "insert": "会议内容:" 47 | } 48 | ], 49 | "id": "_t_xqxWwU", 50 | "type": "text" 51 | }, 52 | { 53 | "text": [], 54 | "id": "_YZddVF5R", 55 | "type": "text" 56 | } 57 | ], 58 | "comments": {} 59 | }`; 60 | ``` 61 | 62 | 创建文档 63 | 64 | ```ts 65 | // 生成模版参数 66 | const DocTemplateValues = { 67 | "meet-name": 'XXX会议', 68 | names: 'Steve, zTree, OldHu', 69 | date: new Date().toLocaleDateString(), 70 | }; 71 | 72 | // 设置创建编辑器参数 73 | const options = { 74 | ... 75 | template: JSON.parse(DocTemplate), 76 | templateValues: DocTemplateValues, 77 | }; 78 | 79 | // 创建编辑器 80 | const editor = createEditor(document.getElementById('editor') as HTMLElement, options, auth); 81 | ``` 82 | 83 | 通过这种方式,就可以自动通过模版创建一篇文档,并自动替换模版里面的参数。 84 | -------------------------------------------------------------------------------- /docs/zh-CN/custom-text-toolbar.md: -------------------------------------------------------------------------------- 1 | # 自定义文字浮动工具栏 2 | 3 | wiz editor允许您自己定义选中文字后显示的浮动工具栏。具体方式如下: 4 | 5 | ```ts 6 | class CustomTextToolbar extends TextToolbar { 7 | div: HTMLElement; 8 | 9 | events = new EventEmitter(); 10 | 11 | constructor() { 12 | super(); 13 | const div = document.createElement('div'); 14 | div.style.position = 'absolute'; 15 | div.style.width = '200px'; 16 | div.style.height = '44px'; 17 | div.style.backgroundColor = '#f0f0f0'; 18 | div.style.border = '1px solid #cccccc'; 19 | div.innerText = 'Custom text toolbar'; 20 | this.div = div; 21 | document.body.appendChild(div); 22 | } 23 | 24 | hide(forcedToHide?: boolean): void { 25 | console.log('hide toolbar', forcedToHide); 26 | this.div.style.display = 'none'; 27 | this.events.emit('hide'); 28 | } 29 | 30 | isVisible(): boolean { 31 | return this.div.style.display !== 'none'; 32 | } 33 | 34 | handleMouseMove(event: MouseEvent): boolean { 35 | console.log(event); 36 | return false; 37 | } 38 | 39 | on(event: 'hide' | 'show', callback: (...args: any[]) => void): void { 40 | console.log(event, callback); 41 | this.events.on(event, callback); 42 | } 43 | 44 | isVisibleForTextBlock(): boolean { 45 | return false; 46 | } 47 | 48 | isMyPopover(popover: Popover): boolean { 49 | console.log(popover); 50 | return false; 51 | } 52 | 53 | update(editor: Editor, force: boolean): void { 54 | console.log('update toolbar', force); 55 | // 当鼠标在一个block上面移动的时候,就会调用这个方法 56 | // 获得当前选中的内容 57 | const sel = editor.getSelectionDetail(); 58 | if (sel.collapsed) { 59 | // 是否选中文字 60 | if (this.isVisible()) { 61 | this.hide(); 62 | } 63 | return; 64 | } 65 | // 66 | // 当前是否已经显示 67 | if (this.isVisible()) { 68 | return; 69 | } 70 | // 71 | // 获取选中部分区域 72 | const rect = sel.range.getBoundingClientRect(); 73 | // 显示自定义工具栏 74 | this.div.style.display = ''; 75 | this.div.style.left = `${rect.left}px`; 76 | this.div.style.top = `${rect.top - 44}px`; 77 | // 触发显示事件 78 | this.events.emit('show'); 79 | } 80 | 81 | // 更新工具栏状态。例如当用户点击工具栏上面的某些按钮(粗体/斜体等) 82 | // 此时文字样式已经发生变化,编辑器将会调用这个方法, 83 | // 并且将新的状态传入 (status) 84 | updateStatus(editor: Editor, status: CommandStatus): void { 85 | console.log('update toolbar status', status); 86 | } 87 | } 88 | ``` 89 | 90 | 创建编辑器的时候,将自定义的工具栏传递给编辑器: 91 | 92 | ```ts 93 | const options: EditorOptions = { 94 | serverUrl: WsServerUrl, 95 | template, 96 | templateValues, 97 | textToolbar: new CustomTextToolbar(), 98 | ... 99 | }; 100 | ``` 101 | 102 | 通过这种方式,您就可以用自己的工具栏,来代替编辑器内置的工具栏。 103 | -------------------------------------------------------------------------------- /docs/zh-CN/date.md: -------------------------------------------------------------------------------- 1 | # 演示:在编辑器里面插入日期 2 | 3 | 运行demo 4 | 5 | ``` 6 | cd h5 7 | npm run date 8 | ``` 9 | 10 | [查看源码](../../h5/src/box_date.ts) 11 | 12 | 13 | 主要代码 14 | 15 | ```ts 16 | const DATE_BOX_TYPE = 'date'; 17 | 18 | interface DateBoxData extends BoxData { 19 | text: string; 20 | }; 21 | 22 | function createNode(data: BoxData): BoxNode { 23 | // 24 | const { text } = data as DateBoxData; 25 | // 26 | return { 27 | classes: ['box-mention'], 28 | children: [{ 29 | type: 'text', 30 | text, 31 | } as BoxTextChild], 32 | }; 33 | } 34 | 35 | async function createBoxData(editor: Editor): Promise { 36 | return { 37 | text: new Date().toLocaleDateString(), 38 | }; 39 | } 40 | 41 | const dateBox = { 42 | prefix: 'dd', 43 | createNode, 44 | createBoxData, 45 | }; 46 | 47 | boxUtils.registerBoxType(DATE_BOX_TYPE as BOX_TYPE, dateBox); 48 | ``` 49 | 50 | 插入日期例子中,当用户输入dd,将会快速的插入当前日期。 51 | 52 | -------------------------------------------------------------------------------- /docs/zh-CN/editor-structure.md: -------------------------------------------------------------------------------- 1 | # wiz-editor 编辑器结构 2 | 3 | ![wiz-editor结构](./assets/editor.png) 4 | 5 | ## 基本概念 6 | 7 | 编辑器包含以下三种结构: 8 | 9 | 1. Container 10 | 2. Block 11 | 3. Box 12 | 13 | ### Block 14 | 15 | Block是编辑器里面的一个基础组件,相当于文章里面的一个段落(或者一个图片,一个视频等等)。block有三种类型: 16 | 17 | 1. Text Block 18 | 2. Embed Block 19 | 3. Complex Block 20 | 21 | #### Text Block 22 | 23 | 可以在block里面输入文字,插入提醒等。这种block类似于一个contentEditable的div。用来支持用户输入文字。 24 | 25 | #### Embed Block 26 | 27 | 这种block,通常用来展示内容,而不是接受用户输入。例如显示一个图片,视频,或者插入一个Office文件(可以直接预览)。 28 | 光标可以定位在这种block前后(可以通过delete/backspace删除整个block),但是不能将光标定位在block里面。 29 | 30 | #### Complex Block 31 | 32 | 这种block里面会包含其他的block,例如表格,每一个单元格,都可以独立输入文字,插入图片等内容。 33 | 34 | ### Container 35 | 36 | container,里面包含了一组block。例如编辑器有一个默认的rootContainer,里面包含了所有的block。 37 | 38 | complex block里面,至少应该包含一个子container。例如表格,里面每一个单元格,都是一个独立的container。 39 | 用户可以在这些container里面进行输入。 40 | 41 | ### Box 42 | 43 | Box不是一个block,而是一个更小的组件。Box可以插入到Text Block里面,并作为一个整体进行编辑。例如用户可以插入一个box,然后删除这个box。 44 | 45 | box内部是不允许编辑的。例如用户插入了一个提醒,这个提醒包含了用户头像以及昵称。用户可以将这个提醒直接删除,但是不能更换头像或者修改里面的昵称。 46 | 47 | 如果要修改,需要删除这个提醒重新插入新的提醒。 48 | 49 | 编辑器内置了提醒,标签等box。 50 | 51 | 每一个box,都是由一个span作为容器,里面可以包含若干文字/图片的dom组件。 52 | 53 | 用户可以通过快捷键输入box,也可以通过下拉框选择输入box。 54 | 55 | ## 扩展编辑器 56 | 57 | ### 扩展Block/Box。 58 | 59 | 可以扩展一个基本的block(必须是text/complex/embed三种之一)。 60 | 61 | block的定义如下: 62 | 63 | ```ts 64 | export interface BlockOptions { 65 | textBlock?: boolean; // 是否是文字类型的block 66 | complexBlock?: boolean; // 是否是complex block 67 | } 68 | export interface Block { 69 | // 获取block类型 70 | getBlockOptions?: () => BlockOptions; 71 | // 如果是文字类型的block,需要实现这个方法。返回一个HTMLElement。这个HTMLElement将会作为编辑区域 72 | getTextElement?: (block: BlockElement) => RichTextElement; 73 | // 创建一个block data数据。所有类型的block都需要实现 74 | createBlockTemplateData: (editor: Editor, options: any) => BlockTemplateData; 75 | // 创建block content 元素。其中text block,需要返回一个可编辑的HTMLElement作为文字容器(例如div,li, heading)。对于complex block,可以返回任意HTMLElement(例如table等) 76 | createBlockContent: (editor: Editor, id: string, data: BlockData) => BlockContentElement; 77 | // 通过数据更新block,通常在远程其他用户修改了block之后,会调用这个方法更新本地ui。例如image,远程用户修改了大小,或者src,就会调用这个方法更新图片 78 | updateBlockData: (block: BlockElement, data: BlockData) => void; 79 | // 更新block text。例如远程用户修改了block的文字。 (text类型的block需要实现) 80 | updateBlockText?: (block: BlockElement, data: RichTextOperations) => void; 81 | // 执行文字命令。例如粗体,斜体等。text类型block需要实现 82 | executeTextCommand?: (block: BlockElement, command: TextCommand, start: number, end: number, params?: CommandParams) => any; 83 | // 执行block命令,例如转换为heading,list等 84 | executeBlockCommand?: (block: BlockElement, command: BlockCommand, params?: CommandParams) => any; 85 | // 获取文字状态,例如返回当前文字的样式(粗体,斜体等) 86 | getTextCommandStatus?: (block: BlockElement, offset: number) => TextAttributes; 87 | // 保存数据,例如保存文字等 88 | saveData: (block: BlockElement) => BlockData; 89 | // 响应键盘事件 90 | handleKeydownEvent?: (block: BlockElement, detail: SelectionDetail, event: KeyboardEvent) => boolean; 91 | // 响应文字修改事件(例如input事件) 92 | handleBlockTextChanged?: (block: BlockElement) => boolean; 93 | // 响应block被插入到编辑器的事件(block已经被插入)。编辑器初始化block的时候不会调用这个方法。 94 | handleBlockInserted?: (block: BlockElement, options: InsertBlockOptions) => void; 95 | // 响应block被加载到编辑器的事件,编辑器初始化的时候会调用 96 | handleBlockLoaded?: (block: BlockElement) => void; 97 | // 响应block被删除的事件 98 | handleBlockDeleted?: (container: ContainerElement, index: number, blockData: BlockData, options: DeleteBlockOptions) => void; 99 | // 响应粘贴事件 100 | handlePaste?: (block: BlockElement, data: ClipboardData, detail: SelectionDetail) => boolean; 101 | // 获取子container数据。complex 类型的block必须实现 102 | getChildContainersData?: (block: BlockElement) => ContainerData[]; 103 | // 获取光标位置 104 | getCaretPos: (block: BlockElement, node: Node, nodeOffset: number) => number; 105 | // 根据pos创建range。对于complex block,必须将range设置到子container的block里面。(调用子block的createRange方法) 106 | createRange: (block: BlockElement, pos: number) => Range; 107 | // 响应选中状态改变 108 | handleSelectionChanged?: (block: BlockElement, detail: SelectionDetail, lastEvent: MouseEvent | KeyboardEvent | null, lastPosition: Position) => boolean; 109 | // 通过方向获取子container。例如表格里面,获取某一个子container 110 | getSubContainerInComplexBlock?: (block: BlockElement, element: HTMLElement, type: 'top' | 'right' | 'bottom' | 'left') => ContainerElement | null; 111 | // 获取工具栏选项 112 | getToolbarOptions?: (block: BlockElement, target: Element) => ToolbarOptions | null; 113 | // 获取右键菜单选项 114 | getContextMenuData?: (block: BlockElement, detail: SelectionDetail) => MenuData; 115 | // 通知事件。其中name为事件名称 116 | notify?: (block: BlockElement, name: string, data: any) => void; 117 | // 转换为text 118 | toText?: (block: BlockElement, startOffset?: number, endOffset?: number) => string; 119 | // 替换子container id。将文档里面的子container id替换为新的id。complex block必须实现 120 | replaceChildrenId?: (doc: EditorDoc, data: DocBlock) => void; 121 | // 当在一个text block里面回车的时候,返回新建的block的数据。 122 | createSplittedBlockData?: (block: BlockElement) => BlockTemplateData; 123 | // 是否可以插入新的block或者box。仅针对complex block生效。例如code里面不允许插入图片等。 124 | accept?: (type: BOX_TYPE | BLOCK_TYPE) => boolean; 125 | } 126 | ``` 127 | 128 | 要实现一个text / complex类型的block,需要实现上面的方法并进行注册。 129 | 130 | 而要实现一个embed类型的block,则简单的多,只需要实现下面的接口即可: 131 | 132 | ```ts 133 | export interface EmbedData { 134 | [index: string]: any; 135 | }; 136 | 137 | export interface Embed { 138 | // 返回一个EmbedElement(就是HTMLElement) 139 | createElement(editor: Editor, data: EmbedData): EmbedElement; 140 | // 保存数据: 从EmbedElement里面保存数据 141 | saveData(editor: Editor, embed: EmbedElement): EmbedData; 142 | // 更新数据(例如从服务器更新新的数据)到界面(dom) 143 | updateData(editor: Editor, embed: EmbedElement, data: EmbedData): void; 144 | // 获取工具栏按钮,可以不实现 145 | getToolbarOptions?: (block: BlockElement, target: Element) => ToolbarOptions | null; 146 | }; 147 | ``` 148 | 149 | #### 例子 150 | 151 | 1. [自定义complex block](./complex-block.md) 152 | 2. [自定义embed block](./embed-block.md) 153 | 154 | ### 扩展box 155 | 156 | [如何自定义box](./box.md) 157 | -------------------------------------------------------------------------------- /docs/zh-CN/embed-block.md: -------------------------------------------------------------------------------- 1 | # 创建自定义的 embed block 2 | 3 | wiz-editor支持增加自定义的embed类型的block。这种block类似于图片,视频, Office文件等这样的block,用户可以将光标定位在这种block前面后者后面,但是不能将光标定位在block里面。 4 | 5 | block内容是一个任意的HTMLElement,里面也可以包含任意元素。 6 | 7 | embed block定义: 8 | 9 | ```ts 10 | export interface EmbedData { 11 | [index: string]: any; 12 | }; 13 | 14 | export interface Embed { 15 | // 返回一个EmbedElement(就是HTMLElement) 16 | createElement(editor: Editor, data: EmbedData): EmbedElement; 17 | // 保存数据: 从EmbedElement里面保存数据 18 | saveData(editor: Editor, embed: EmbedElement): EmbedData; 19 | // 更新数据(例如从服务器更新新的数据)到界面(dom) 20 | updateData(editor: Editor, embed: EmbedElement, data: EmbedData): void; 21 | // 获取工具栏按钮,可以不实现 22 | getToolbarOptions?: (block: BlockElement, target: Element) => ToolbarOptions | null; 23 | }; 24 | ``` 25 | 26 | 实现Embed之后,需要进行注册: 27 | 28 | ```ts 29 | import { 30 | embedUtils, 31 | } from 'wiz-editor/client'; 32 | 33 | const buttonsEmbed = { 34 | createElement, 35 | saveData, 36 | updateData, 37 | }; 38 | 39 | embedUtils.registerEmbed('buttons' as EMBED_TYPE, buttonsEmbed); 40 | ``` 41 | 42 | demo 43 | 44 | ![embed-block](./assets/embed-block.png) 45 | 46 | ```ts 47 | // -------------------custom embed block---------------- 48 | 49 | (() => { 50 | interface EmbedButtonsData extends EmbedData { 51 | count?: number; 52 | } 53 | 54 | function handleButtonClick(event: Event) { 55 | const button = event.target as HTMLButtonElement; 56 | alert(`you clicked button ${button.innerText}`); 57 | }; 58 | // 59 | 60 | function createElement(editor: Editor, data: EmbedData): EmbedElement { 61 | assert(data); 62 | const div = document.createElement('div'); 63 | const child = document.createElement('div'); 64 | div.appendChild(child); 65 | // 66 | const buttonsData = data as EmbedButtonsData; 67 | const count = buttonsData.count || 10; 68 | // 69 | div.setAttribute('data-count', `${count}`); 70 | // 71 | for (let i = 0; i < count; i++) { 72 | const button = document.createElement('button'); 73 | button.innerText = `button-${i}`; 74 | button.onclick = handleButtonClick; 75 | child.appendChild(button); 76 | } 77 | // 78 | return div as unknown as EmbedElement; 79 | } 80 | 81 | function saveData(editor: Editor, embed: EmbedElement): EmbedData { 82 | assert(embed instanceof HTMLDivElement); 83 | const count = Number.parseInt(embed.getAttribute('data-count') || '10', 10); 84 | return { 85 | count, 86 | }; 87 | } 88 | 89 | function updateData(editor: Editor, embed: EmbedElement, data: EmbedData): void { 90 | assert(embed instanceof HTMLHRElement); 91 | assert(data); 92 | // 93 | assert(embed.children.length === 1); 94 | const child = embed.children[0]; 95 | child.innerHTML = ''; 96 | // 97 | const buttonsData = data; 98 | const count = buttonsData.count || 10; 99 | // 100 | for (let i = 0; i < count; i++) { 101 | const button = document.createElement('button'); 102 | button.innerText = `button-${i}`; 103 | button.onclick = handleButtonClick; 104 | child.appendChild(button); 105 | } 106 | } 107 | 108 | const buttonsEmbed = { 109 | createElement, 110 | saveData, 111 | updateData, 112 | }; 113 | 114 | embedUtils.registerEmbed('buttons' as EMBED_TYPE, buttonsEmbed); 115 | })(); 116 | 117 | ... 118 | 119 | document.getElementById('buttons')?.addEventListener('click', () => { 120 | assert(currentEditor); 121 | const count = (Date.now() % 5) + 5; 122 | currentEditor.insertEmbed(null, -2, 'buttons' as any, { 123 | count, 124 | }); 125 | }); 126 | ``` 127 | 128 | [查看例子](../../h5/src/custom.ts) 129 | 130 | -------------------------------------------------------------------------------- /docs/zh-CN/handle-message.md: -------------------------------------------------------------------------------- 1 | # 响应编辑器事件 2 | 3 | 在创建编辑器的时候,可以响应编辑器事件: 4 | 5 | ```ts 6 | 7 | export interface EditorOptions { 8 | lang?: LANGS; 9 | serverUrl: string; 10 | ... 11 | callbacks?: { 12 | onLoad?: (editor: Editor, data: EditorDoc) => void; 13 | onError?: (editor: Editor, error: Error) => void; 14 | onSave?: (editor: Editor, data: EditorDoc) => void; 15 | onChange?: (editor: Editor) => void; 16 | onRemoteUserChanged?: (editor: Editor, users: OnlineUsers, change: OnlineUserChange) => void; 17 | onStatusChanged?: (editor: Editor, isDirty: boolean) => void; 18 | onReauth?: (userId: string, docId: string, permission: AuthPermission) => Promise; 19 | onGetMentionItems?: (editor: Editor, keywords: string) => Promise; 20 | onMentionInserted?: (editor: Editor, boxData: MentionBoxData, block: BlockElement, pos: number) => void; 21 | onMentionClicked?: (editor: Editor, boxData: MentionBoxData, block: BlockElement) => void; 22 | onGetTagItems?: (editor: Editor, keywords: string) => Promise; 23 | onTagInserted?: (editor: Editor, tag: string, block: BlockElement, pos: number) => void; 24 | onTagClicked?: (editor: Editor, tag: string) => void; 25 | onCommentInserted?: (editor: Editor, commentId: string, commentDocText: string, commentText: string, selectedBlock: SelectedBlock) => void; 26 | onCommentReplied?: (editor: Editor, toUserId: string, orgCommentText: string, commentText: string) => void; 27 | onRenderAutoSuggestItem?: (editor: Editor, suggestData: AutoSuggestData) => HTMLElement; 28 | onGetBlockCommand?: (editor: Editor, block: BlockElement, detail: SelectionDetail, type: 'fixed' | 'hover' | 'menu') => CommandItemData[]; 29 | onCommandStatusChanged?: (editor: Editor, status: CommandStatus) => void; 30 | onCheckboxChanged?: (editor: Editor, text: string, blockData: BlockData, mentions: BoxData[], calendars: BoxData[]) => void; 31 | onMarkerCreated?: (editor: Editor, text: string, blockData: BlockData) => void; 32 | onQuickInput?: (editor: Editor, event: KeyboardEvent | null, block: BlockElement, text: string, offset: number) => boolean; 33 | onUploadResource?: (editor: Editor, file: File, onProgress: OnProgress) => Promise; 34 | onTitleChanged?: (editor: Editor, docId: string, title: string) => void; 35 | onGetChartJsData?: (editor: Editor, data: string) => Promise; 36 | onUpdateToc?: (editor: Editor, toc: EditorDocToc) => void; 37 | onCustomMessage?: (editor: Editor, data: string) => void; 38 | }; 39 | }; 40 | 41 | ``` 42 | -------------------------------------------------------------------------------- /docs/zh-CN/index.md: -------------------------------------------------------------------------------- 1 | # wiz-editor 多人实时编辑器使用说明 2 | 3 | 直接从git clone或者下载代码,解压缩到磁盘。 4 | 5 | 注意:**需要nodejs 14或者更高的版本** 6 | 7 | ## 运行原生js例子 8 | 9 | 1. 安装和运行 10 | 11 | ```sh 12 | cd h5 13 | npm install 14 | npm start 15 | ``` 16 | 17 | 2. 打开浏览器 18 | 19 | ``` 20 | localhost:9000 21 | ``` 22 | 23 | ## 更多demo 24 | 25 | ```bash 26 | cd h5 27 | # 完整的自定义扩展例子,包含外部工具栏,各种自定义组件等 28 | npm run custom 29 | # 极简编辑器,无外部UI 30 | npm run simple 31 | # 各种自定义box 32 | npm run calendar 33 | npm run date 34 | npm run mention 35 | npm run label 36 | ``` 37 | 38 | ## 在自己的项目中使用wiz-editor 39 | 40 | ### 通过npm安装wiz-editor 41 | npm i wiz-editor 42 | 43 | ### 在项目中使用wiz-editor 44 | 45 | ```ts 46 | import { 47 | createEditor, 48 | Editor, 49 | } from 'wiz-editor/client'; 50 | 51 | // 定义AppID,AppSecret, AppDomain。在自带的测试服务器中,下面三个key不要更改 52 | const AppId = '_LC1xOdRp'; 53 | const AppSecret = '714351167e39568ba734339cc6b997b960ed537153b68c1f7d52b1e87c3be24a'; 54 | const AppDomain = 'wiz.cn'; 55 | 56 | // 初始化服务器地址 57 | const WsServerUrl = window.location.protocol !== 'https:' 58 | ? `ws://${window.location.host}` 59 | : `wss://${window.location.host}`; 60 | 61 | // 定义一个用户。该用户应该是由应用服务器自动获取当前用户身份 62 | // 编辑服务需要提供用户id以及用户的显示名。 63 | const user = { 64 | userId: `${new Date().valueOf()}`, 65 | avatarUrl: 'xxx', 66 | displayName: 'test user', 67 | }; 68 | 69 | // 设置编辑器选项 70 | const options = { 71 | serverUrl: WsServerUrl, 72 | }; 73 | 74 | // 从应用服务器获取一个AccessToken。应用服务器需要负责验证用户对文档的访问权限。 75 | // accessToken采用jwt规范,里面应该包含用户的userId,文档的docId,以及编辑应用的AppId。 76 | // 下面是一个演示例子。在正常情况下,AccessToken应该通过用户自己的应用服务器生成。 77 | // 因为在前端使用JWT加密规范的时候,必须在https协议下面的网页才可以使用。为了演示, 78 | // 我们自带的测试服务器会提供一个虚拟的token生成功能。(启动服务的时候,需要指定--enable-fake-token-api 参数) 79 | // 请勿在正式服务器上面,启用这个参数。 80 | 81 | async function fakeGetAccessTokenFromServer(userId: string, docId: string): Promise { 82 | // 83 | const data = { 84 | userId, 85 | docId, 86 | appId: AppId, 87 | }; 88 | 89 | const fromHexString = (hexString: string) => { 90 | const parts = hexString.match(/.{1,2}/g); 91 | assert(parts); 92 | const arr = parts.map((byte) => parseInt(byte, 16)); 93 | return new Uint8Array(arr); 94 | }; 95 | // 96 | const key = fromHexString(AppSecret); 97 | 98 | try { 99 | const accessToken = await new EncryptJWT(data) 100 | .setProtectedHeader({ alg: 'dir', enc: 'A256GCM' }) 101 | .setIssuedAt() 102 | .setIssuer(AppDomain) 103 | .setExpirationTime('120s') 104 | .encrypt(key); 105 | 106 | return accessToken; 107 | } catch (err) { 108 | const res = await fetch(`http://${window.location.host}/token/${AppId}/${docId}/${userId}`); 109 | const ret = await res.json(); 110 | return ret.token; 111 | } 112 | } 113 | 114 | // 文档id 115 | const docId = 'my-test-doc-id'; 116 | 117 | (async function loadDocument() { 118 | // 验证身份,获取accessToken 119 | const token = await fakeGetAccessTokenFromServer(user.userId, docId); 120 | 121 | // 生成编辑服务需要的认证信息 122 | const auth = { 123 | appId: AppId, 124 | userId: user.userId, 125 | docId, 126 | token, 127 | permission: 'w', 128 | }; 129 | 130 | // 创建编辑器并加载文档 131 | const editor = createEditor(document.getElementById('editor') as HTMLElement, options, auth); 132 | })(); 133 | 134 | ``` 135 | 136 | 通过上面的代码,就可以在自己的应用中,创建一个多人实时协同编辑器。 137 | 上面的代码,可以通过在h5例子下面,运行npm run simple来查看效果。 138 | 139 | [查看源码](../../h5/src/simple.ts) 140 | 141 | ## 扩展编辑器功能 142 | 143 | [编辑器结构](./editor-structure.md) 144 | 145 | ### 自定义@xxx提醒用户功能 146 | 147 | [增加插入提醒功能](./mention.md) 148 | 149 | ### 响应评论消息 150 | 151 | [响应评论消息](./comment.md) 152 | 153 | ### 自定义inline的模块(box) 154 | 155 | [自定义box说明](./box.md) 156 | 157 | ### 自定义block按钮/菜单 158 | 159 | [自定义block按钮/菜单](./custom-context-menu.md) 160 | 161 | ### 自定义文字样式 162 | 163 | [自定义文字样式](./custom-style.md) 164 | 165 | ### 自定义模版 166 | 167 | [自定义文档模版](./custom-template.md) 168 | 169 | ### 自定义block 170 | 171 | [编辑器结构](./editor-structure.md) 172 | 173 | [自定义Embed类型block](./embed-block.md) 174 | 175 | [自定义Complex类型block](./complex-block.md) 176 | 177 | ## 服务端 178 | 179 | [wiz-editor 服务端适配](./server.md) 180 | 181 | [wiz-editor 服务端架构介绍](./server-architecture.md) -------------------------------------------------------------------------------- /docs/zh-CN/label.md: -------------------------------------------------------------------------------- 1 | # 演示:在编辑器里面插入日期 2 | 3 | 运行demo 4 | 5 | ``` 6 | cd h5 7 | npm run label 8 | ``` 9 | 10 | [查看源码](../../h5/src/label_box.ts) 11 | 12 | 主要代码 13 | 14 | ```ts 15 | const LABEL_BOX_TYPE = 'label'; 16 | 17 | interface LabelBoxData extends BoxData { 18 | color: string; 19 | }; 20 | 21 | function createNode(data: BoxData): BoxNode { 22 | // 23 | const { color } = data as LabelBoxData; 24 | // 25 | return { 26 | classes: [`label-${color}`, 'label'], 27 | children: [{ 28 | type: 'text', 29 | text: color, 30 | } as BoxTextChild], 31 | }; 32 | } 33 | 34 | function handleBoxInserted(editor: Editor, data: BoxData): void { 35 | const calendarData = data as LabelBoxData; 36 | console.log('label box inserted:', calendarData); 37 | } 38 | 39 | function handleBoxClicked(editor: Editor, data: BoxData): void { 40 | const calendarData = data as LabelBoxData; 41 | alert(`label clicked: ${calendarData.color}`); 42 | } 43 | 44 | async function getItems(editor: Editor, keywords: string): Promise { 45 | console.log(keywords); 46 | return [{ 47 | iconUrl: '', 48 | text: 'red', 49 | id: 'red', 50 | data: '', 51 | }, { 52 | iconUrl: '', 53 | text: 'green', 54 | id: 'green', 55 | data: '', 56 | }, { 57 | iconUrl: '', 58 | text: 'blue', 59 | id: 'blue', 60 | data: '', 61 | }]; 62 | } 63 | 64 | function createBoxDataFromItem(editor: Editor, item: AutoSuggestData): BoxTemplateData { 65 | const color = item.id; 66 | return { 67 | color, 68 | }; 69 | } 70 | 71 | function renderAutoSuggestItem(editor: Editor, suggestData: AutoSuggestData): HTMLElement { 72 | const div = document.createElement('div'); 73 | div.setAttribute('style', `background-color: ${suggestData.text}; border-radius: 10px; width: 100%; height: 24px`); 74 | return div; 75 | } 76 | 77 | const labelBox = { 78 | prefix: 'll', 79 | createNode, 80 | getItems, 81 | createBoxDataFromItem, 82 | handleBoxInserted, 83 | handleBoxClicked, 84 | renderAutoSuggestItem, 85 | }; 86 | 87 | boxUtils.registerBoxType(LABEL_BOX_TYPE as BOX_TYPE, labelBox); 88 | ``` 89 | 90 | 在这个例子中,演示了如何通过item,来插入一个不同的box。同时还演示了,如果自定义渲染下拉框里面的每一个item。 91 | 92 | -------------------------------------------------------------------------------- /docs/zh-CN/mention.md: -------------------------------------------------------------------------------- 1 | 2 | # 增加提醒(@xxx人)功能 3 | 4 | 运行demo 5 | 6 | ```sh 7 | cd h5 8 | npm run mention 9 | ``` 10 | 11 | [查看源码](../../h5/src/mention.ts) 12 | 13 | wiz-editor内置了提醒功能。如果需要使用这个功能,那么在初始化编辑起的时候,至少需要提供一个方法,用来获取用户列表。 14 | 下面是一个演示例子: 15 | 16 | ```ts 17 | 18 | // 生成一些人名 19 | const NAMES = [ 20 | '龚光杰', 21 | '褚师弟', 22 | '容子矩', 23 | '干光豪', 24 | '葛光佩', 25 | '郁光标', 26 | '吴光胜', 27 | '唐光雄', 28 | '枯荣大师', 29 | '本因大师', 30 | '本观', 31 | '本相', 32 | '本参', 33 | '本尘', 34 | '玄愧', 35 | '玄念', 36 | '玄净', 37 | '慧真', 38 | '慧观', 39 | '慧净', 40 | '慧方', 41 | '慧镜', 42 | '慧轮', 43 | '虚清', 44 | '虚湛', 45 | '虚渊', 46 | '摘星子', 47 | '摩云子', 48 | '天狼子', 49 | '出尘子', 50 | '段延庆', 51 | '叶二娘', 52 | '岳老三', 53 | '云中鹤', 54 | ]; 55 | 56 | const ALL_USERS: AutoSuggestData[] = []; 57 | 58 | // 生成用户列表数据 59 | NAMES.forEach((name) => { 60 | const user = { 61 | iconUrl: 'http://www.wiz.cn/wp-content/new-uploads/2285af20-4006-11eb-8f21-01eb48012b63.jpeg', 62 | text: name, 63 | id: name, 64 | data: name, 65 | }; 66 | ALL_USERS.push(user); 67 | }); 68 | 69 | // 模拟从用户的应用服务器获取用户列表。如果没有关键字,默认返回全部数据 70 | // 在用户输入过程中,会不断的调用该方法。应用应该通过keywords进行过滤 71 | async function fakeGetMentionItems(editor: Editor, keywords: string): Promise { 72 | assert(keywords !== undefined); 73 | console.log(keywords); 74 | if (!keywords) { 75 | return ALL_USERS; 76 | } 77 | return ALL_USERS.filter((user) => user.text.toLowerCase().indexOf(keywords.toLowerCase()) !== -1); 78 | } 79 | 80 | ``` 81 | 82 | 然后在创建编辑器的时候,设置callback方法: 83 | 84 | ```ts 85 | // 设置编辑器选项 86 | const options = { 87 | serverUrl: WsServerUrl, 88 | callbacks: { 89 | onGetMentionItems: fakeGetMentionItems, 90 | }, 91 | }; 92 | 93 | ``` 94 | 95 | 就可以在用户输入@的时候,出现一个下拉列表,并提醒用户选择一个用户进行提醒。 96 | 97 | 业务程序可能会需要在用户创建一个提醒,或者点击提醒的时候进行相应的处理。 98 | 例如,您可以在创建一个提醒的时候,通过企业的消息系统,向相关用户发送一条消息。 99 | 100 | 同样,可以在创建编辑器的时候,设置相应的回调方法: 101 | 102 | ```ts 103 | function handleMentionInserted(editor: Editor, boxData: MentionBoxData, block: BlockElement, pos: number) { 104 | console.log(`mention ${JSON.stringify(boxData)} inserted at ${pos}`); 105 | // 提醒前面的文字 106 | const leftText = blockUtils.toText(block, 0, pos); 107 | // 提醒后面的文字 108 | const rightText = blockUtils.toText(block, pos + 1, -1); 109 | // 定位锚点,可以用来给在文档中定位该提醒 110 | const anchorId = `M${boxData.id}`; 111 | alert(`anchor id: ${anchorId}\n\ncontext text:\n\n${leftText}\n\n${rightText}`); 112 | } 113 | 114 | function handleMentionClicked(editor: Editor, boxData: MentionBoxData) { 115 | alert(`you clicked ${boxData.text} (${boxData.mentionId})`); 116 | } 117 | 118 | // 设置编辑器选项 119 | const options = { 120 | serverUrl: WsServerUrl, 121 | callbacks: { 122 | onGetMentionItems: fakeGetMentionItems, 123 | onMentionInserted: handleMentionInserted, 124 | onMentionClicked: handleMentionClicked, 125 | }, 126 | }; 127 | ``` 128 | 129 | 这样在创建一个提醒,或者点击提醒的时候,就会调用相应的方法。 130 | 131 | 上面的代码,可以通过在h5例子下面,运行npm run mention来查看效果。 132 | 133 | 134 | wiz-editor内置的editor,通过box来实现。具体原理,请参考[box](./box.md)。 135 | -------------------------------------------------------------------------------- /docs/zh-CN/post-custom-message.md: -------------------------------------------------------------------------------- 1 | # 向在线的用户发送自定义消息 2 | 3 | 利用这个功能,可以给正在编辑同一文档的用户发送自定义的消息 4 | 5 | ```ts 6 | 7 | function handleLoad(editor: Editor, data: any): void { 8 | console.log(`${editor.docId()} loaded`); 9 | // 发送一个自定义消息 10 | editor.postCustomMessage('I\' am in.'); 11 | } 12 | 13 | function handleCustomMessage(editor: Editor, data: string) { 14 | console.log(data); 15 | } 16 | 17 | 18 | const options: EditorOptions = { 19 | serverUrl: WsServerUrl, 20 | callbacks: { 21 | // 编辑器成功加载文档 22 | onLoad: handleLoad, 23 | // 接收到远程自定义消息 24 | onCustomMessage: handleCustomMessage, 25 | }, 26 | }; 27 | ``` -------------------------------------------------------------------------------- /docs/zh-CN/server-architecture.md: -------------------------------------------------------------------------------- 1 | # wiz-editor 多人实时编辑器服务端架构介绍 2 | 3 | ## 功能 4 | 5 | wiz-editor 多人实时编辑服务,本质上是一个具有操作变换能力的实时JSON数据库。服务器有能力接受多个客户端对同一JSON数据的修改,对这些修改进行计算,然后生成针对每个客户端独特的计算函数,将函数发给客户端去执行,确保执行后得到一致的结果。 6 | 7 | ![](./assets/server-architecture-2.png?raw=true) 8 | 9 | 服务器会提供与实时协同编辑有关的以下能力: 10 | 11 | - 接收多个客户端提交的数据修改(编辑行为) 12 | - 对客户端提交的修改行为进行临时存储(可配置有效时间),当有临时网络中断的客户端重新连接时,需要使用这些数据 13 | - 基于多个客户端的数据修改行为,计算并生成文档的最新版本 14 | - 基于数据修改,对数据修改进行计算与转换后,再返回给各客户端,由客户端基于这些数据生成多端一致的文档数据 15 | - 针对富文本的修改,做了专门的计算与转换支持,使用json结构表达富文本中的文字、属性等,并支持多端修改的合并 16 | - 对文档的最新版本进行保存 17 | - 对文档的历史版本进行保存 18 | - 对文档相关的资源文件(图片等附件)进行保存,并与版本关联 19 | - 生成文档对应的文本格式(用于全文索引) 20 | - 基于JWT token的权限认证 21 | 22 | 服务器 **不提供** 与编辑无关的能力,比如:用户管理、文档标签、文档父子关系、文件夹等。 23 | 24 | ## 核心数据结构 25 | 26 | 服务器端的核心数据结构包括: 27 | 28 | ### 1. appId, docId与appSecret 29 | 30 | appId是分配给每个租户的一个唯一标识。 31 | 32 | docId是文档的标识。appId与docId合在一起,就可以定位到一篇文档。 33 | 34 | appSecret是与appId对应的密钥。访问服务器的所有功能,都需要在请求中包含基于appId和appSecret生成的token。 35 | 36 | 具体的token生成可参考 [wiz-editor 服务端适配](./server.md) 37 | 38 | ### 2. 文档结构 39 | 40 | 文档采用JSON的格式进行存储。一个文档中可以包含多个容器,每个容器中包含一个block数组,每个block对应编辑器中的一个块。 41 | 42 | block的核心属性是 id 和 type,每个不同 type 的block,会有不同的数据保存在其中,由这个 type 对应的处理逻辑进行处理。一个合法的文档结构如下: 43 | 44 | ```json 45 | { 46 | "blocks": [ 47 | { 48 | "id": "_JNROVvJr", 49 | "type": "text", 50 | "text": [] 51 | }, 52 | { 53 | "text": [], 54 | "id": "_zkCPs_Hx", 55 | "type": "table", 56 | "rows": 1, 57 | "cols": 2, 58 | "cells": [ 59 | "_m63A1XwJ", 60 | "_HNSbtTa2", 61 | ] 62 | }, 63 | { 64 | "id": "_8vVxQePS", 65 | "type": "embed", 66 | "text": null, 67 | "embedType": "image", 68 | "embedData": { 69 | "src": "https://wcdn.wiz.cn/apple-icon.png?v=1" 70 | } 71 | }, 72 | { 73 | "text": [ 74 | ], 75 | "id": "_vA0otj0I", 76 | "type": "text" 77 | }, 78 | { 79 | "text": [ 80 | { 81 | "attributes": { 82 | "box": true, 83 | "id": "_TTP4Hi4W", 84 | "type": "date", 85 | "text": "2020/12/18" 86 | }, 87 | } 88 | ], 89 | "id": "_ns681KAt", 90 | "type": "text" 91 | }, 92 | { 93 | "id": "_nYn2rkcL", 94 | "type": "embed", 95 | "text": null, 96 | "embedType": "mermaid", 97 | "embedData": { 98 | "mermaidText": "\ngraph TD;\n A-->B;\n A-->C;\n B-->D;\n C-->D;\n" 99 | } 100 | }, 101 | ], 102 | "_m63A1XwJ": [ 103 | { 104 | "id": "_pablk9PL", 105 | "type": "embed", 106 | "text": null, 107 | "embedType": "image", 108 | "embedData": { 109 | "src": "https://wcdn.wiz.cn/apple-icon.png?v=1" 110 | } 111 | }, 112 | { 113 | "text": [], 114 | "id": "_KPGOsP3s", 115 | "type": "text" 116 | } 117 | ], 118 | } 119 | ``` 120 | 121 | ## 数据持久化结构 122 | 123 | 数据持久化分为几个不同的存储引擎(适配器): 124 | - 用户操作存储 125 | - 文档最终版本存储 126 | - 文档历史版本存储 127 | - 附件存储 128 | 129 | 其中用户操作存储是临时存储,支持存储于redis或数据库中。其它几个存储都是持久化存储。 130 | 131 | 所有的元数据存储,都支持sqlite、mysql和mongodb三种引擎。文档与附件存储,支持保存副本至对象存储,并按LRU策略清理本地存储。在用户重新编辑的时候,会自动取回本地。 132 | 133 | ## 网络协议与接口 134 | 135 | 网络协议使用websocket协议进行实时通信,对于一些功能性API,使用restful方式提供接口。 136 | 137 | websocket支持启用https和压缩 138 | 139 | ## 负载均衡与高可用 140 | 141 | 服务器架构设计上充分考虑了性能与高可用的需求,整体架构如下: 142 | 143 | ![](assets/server-architecture-1.png?raw=true) 144 | 145 | 图中wiz-editor client访问时,每个请求中都需要包含appId和docId信息,请求到达负载均衡后,会基于一致性哈希算法,将请求分配到一个wiz-editor server上。 146 | 147 | 当后端有服务器down了之后,负载均衡会基于配置好的健康检查策略,将请求转发至新的服务器上。新的服务器会基于后端的redis/数据库/对象存储中存储的数据的最新状态,继续为前端提供服务。 148 | 149 | 后端也可动态增加新的服务器。增加新的服务器后,负载均衡会自动将请求按一致性哈希的算法,在新的服务器组中进行分配。 150 | 151 | 整个集群对外提供一个统一协同编辑服务,这样的一个集群,可正常服务数十万规模的用户。 152 | 153 | 如果用户规模特别大,或者有全球部署的需求,也可以部署多个这样的集群,按一定的策略(比如按哈希、地域,或用户自己选择),将用户分配到不同的集群上。服务器支持数据迁移服务,当用户需要的时候,可以自动将用户的数据在集群之间进行迁移。 154 | -------------------------------------------------------------------------------- /docs/zh-CN/server.md: -------------------------------------------------------------------------------- 1 | # wiz-editor 用户认证 2 | 3 | wiz-editor编辑服务端没有保存任何用户信息。所以一个文档可以由哪些用户访问或者编辑,需要企业的业务服务来确定。 4 | 5 | ## 获得应用AppId和AppSecret 6 | 7 | 企业的应用应该首先向wiz-editor服务申请一个AppId和AppSecret。私有部署的wiz-editor服务,则可以在服务端的配置文件里面找到。 8 | 9 | ## 验证用户身份并确认用户是否可以访问文档 10 | 11 | 首先,企业应用需要根据当前用户的身份,文档Id,来确认用户对文档的权限。 12 | 然后,通过AppId,AppSecret以及用户Id,生成一个JWT规范的access token,并将这个access token返回给前端。 13 | 前端在创建编辑器的时候,将AppId,用户Id以及生成的access token,作为参数传递给编辑器。 14 | 编辑器将会把这些数据,提交给编辑服务。编辑服务会根据用户的AppId,用户Id以及access token来进行验证。如果通过,则可以正常编辑, 15 | 否则将无法编辑。 16 | 17 | ## 演示代码 (NodeJS) 18 | 19 | ### 安装jose 20 | 21 | ```sh 22 | npm i jose 23 | ``` 24 | 25 | ### 验证用户身份,生成access token 26 | 27 | ```ts 28 | 29 | import EncryptJWT from 'jose/jwt/encrypt'; 30 | 31 | const AppId = 'xxx-xxx-xxx'; 32 | // AppSecret 应该通过环境变量/配置文件获取 33 | const AppSecret = 'xxx'; 34 | const AppDomain = 'wiz.cn'; 35 | 36 | async function fakeGetAccessTokenFromServer(userId: string, docId: string): Promise { 37 | // 38 | // userId应该通过当前用户的业务token(cookie等)获取,得到当前用户的身份 39 | // docId可以作为Api参数获取 40 | // 41 | // JWT 自定义数据 42 | const data = { 43 | userId, 44 | docId, 45 | appId: AppId, 46 | }; 47 | 48 | const key = Buffer.from(AppSecret); 49 | 50 | // 生成JWT规范的token,并返回给前端。 51 | const accessToken = await new EncryptJWT(data) 52 | .setProtectedHeader({ alg: 'dir', enc: 'A256GCM' }) 53 | .setIssuedAt() 54 | .setIssuer(AppDomain) 55 | .setExpirationTime('120s') 56 | .encrypt(key); 57 | 58 | return accessToken; 59 | } 60 | ``` 61 | 62 | 企业应用的前端,需要调用该Api,获取到用户的accessToken,然后再创建编辑器。 63 | 64 | 65 | ## 启动服务 66 | 67 | ```js 68 | const { startServer } = require('wiz-editor/server'); 69 | const path = require('path'); 70 | 71 | // 参考node_modules/wiz-editor/config/server.json 文件 72 | const options = { 73 | enableFakeTokenApi: true, 74 | serveStatic: true, 75 | staticDir: path.resolve('./dist'), 76 | }; 77 | 78 | console.log(options); 79 | 80 | startServer(options); 81 | ``` 82 | 83 | ### 启动服务选项 84 | 85 | ```js 86 | { 87 | port: 9000, // 服务端口 88 | serveStatic: true, // 是否发布静态网页服务 89 | staticDir: "/public/www", // 如果发布静态网页,则在此设置路径 90 | allowCORS: false, // 是否允许跨域。如果允许跨域,则网页需要使用https。如果无法使用https(例如开发环境),可以使用代理 91 | storage: { 92 | webhook : { 93 | enable: false, // 是否启用webhook 94 | latestVersionDelay: 30, // 停止文档编辑操作多长时间后,发送最新版本给webhook地址 95 | latestVersionURL: "http://localhost" // webhook地址 96 | }, 97 | } 98 | } 99 | ``` 100 | 101 | #### webhook推送的内容 102 | 103 | 服务器会在每次文档在设定的时间间隔未修改时,将文档转为文本格式,并Post到指定URL,Post过去的数据结构为: 104 | 105 | ```json 106 | { 107 | "appId": "应用Id", 108 | "docId": "文档Id", 109 | "data": "文本数据" 110 | } 111 | ``` 112 | -------------------------------------------------------------------------------- /h5/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wizeditordemo", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "npm-run-all --parallel server watch", 8 | "simple": "npm-run-all --parallel server watch-simple", 9 | "custom": "npm-run-all --parallel server watch-custom", 10 | "custom-mention": "npm-run-all --parallel server watch-custom-mention", 11 | "mention": "npm-run-all --parallel server watch-mention", 12 | "calendar": "npm-run-all --parallel server watch-calendar", 13 | "date": "npm-run-all --parallel server watch-date", 14 | "label": "npm-run-all --parallel server watch-label", 15 | "server": "node ./server -s ./dist --enable-fake-token-api", 16 | "watch": "webpack --watch --config webpack.dev.js", 17 | "watch-simple": "webpack --watch --config webpack.simple.js", 18 | "watch-custom": "webpack --watch --config webpack.custom.js", 19 | "watch-custom-mention": "webpack --watch --config webpack.custom.mention.js", 20 | "watch-mention": "webpack --watch --config webpack.mention.js", 21 | "watch-calendar": "webpack --watch --config webpack.calendar.js", 22 | "watch-date": "webpack --watch --config webpack.date.js", 23 | "watch-label": "webpack --watch --config webpack.label.js" 24 | }, 25 | "devDependencies": { 26 | "html-webpack-plugin": "^4.5.0", 27 | "npm-run-all": "^4.1.5", 28 | "ts-loader": "^8.0.9", 29 | "typescript": "^4.0.5", 30 | "webpack": "^5.10.0", 31 | "webpack-bundle-analyzer": "^4.2.0", 32 | "webpack-cli": "^4.2.0", 33 | "webpack-dev-server": "^3.11.0", 34 | "webpack-merge": "^5.5.0" 35 | }, 36 | "author": "", 37 | "license": "ISC", 38 | "dependencies": { 39 | "jose": "^3.4.0", 40 | "mime": "^1.6.0", 41 | "wiz-editor": "^v0.0.647" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /h5/server.js: -------------------------------------------------------------------------------- 1 | const { startServer } = require('wiz-editor/server'); 2 | const path = require('path'); 3 | 4 | console.log(startServer); 5 | 6 | // 参考node_modules/wiz-editor/config/server.json 文件 7 | const options = { 8 | enableFakeTokenApi: true, 9 | serveStatic: true, 10 | staticDir: path.resolve('./dist'), 11 | }; 12 | 13 | console.log(options); 14 | 15 | startServer(options); 16 | -------------------------------------------------------------------------------- /h5/src/box_calendar.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-alert */ 2 | // eslint-disable-next-line import/no-unresolved 3 | import EncryptJWT from 'jose/jwt/encrypt'; 4 | import { 5 | createEditor, 6 | assert, 7 | boxUtils, 8 | BoxData, 9 | BoxNode, 10 | BoxImageChild, 11 | BoxTextChild, 12 | Editor, 13 | AutoSuggestData, 14 | BOX_TYPE, 15 | } from 'wiz-editor/client'; 16 | import { AuthMessage } from 'wiz-editor/commons/auth-message'; 17 | 18 | function hideElement(id: string) { 19 | const elem = document.getElementById(id); 20 | if (!elem) return; 21 | elem.style.display = 'none'; 22 | } 23 | 24 | hideElement('header'); 25 | hideElement('toolbar'); 26 | 27 | 28 | // -------------------create a custom calendar item---------- 29 | const CALENDAR_IMAGE_URL = 'https://www.wiz.cn/wp-content/new-uploads/b75725f0-4008-11eb-8f21-01eb48012b63.jpeg'; 30 | const CALENDAR_BOX_TYPE = 'calendar'; 31 | 32 | interface CalendarBoxData extends BoxData { 33 | text: string; 34 | }; 35 | 36 | function createNode(editor: Editor, data: BoxData): BoxNode { 37 | // 38 | const { text } = data as CalendarBoxData; 39 | // 40 | return { 41 | classes: ['box-mention'], 42 | children: [{ 43 | type: 'image', 44 | src: CALENDAR_IMAGE_URL, 45 | attributes: { 46 | class: '.calendar_image', 47 | }, 48 | } as BoxImageChild, { 49 | type: 'text', 50 | text, 51 | } as BoxTextChild], 52 | }; 53 | } 54 | 55 | function handleBoxInserted(editor: Editor, data: BoxData): void { 56 | const calendarData = data as CalendarBoxData; 57 | console.log('calendar box inserted:', calendarData); 58 | } 59 | 60 | function handleBoxClicked(editor: Editor, data: BoxData): void { 61 | const calendarData = data as CalendarBoxData; 62 | alert(`calendar clicked: ${calendarData.text}`); 63 | } 64 | 65 | async function getItems(editor: Editor, keywords: string) { 66 | console.log(keywords); 67 | return [{ 68 | iconUrl: CALENDAR_IMAGE_URL, 69 | text: 'Select one event...', 70 | id: 'selectEvent', 71 | data: '', 72 | }, { 73 | iconUrl: CALENDAR_IMAGE_URL, 74 | text: 'Create one event...', 75 | id: 'createEvent', 76 | data: '', 77 | }]; 78 | } 79 | 80 | function handleBoxItemSelected(editor: Editor, item: AutoSuggestData): void { 81 | // 82 | const pos = editor.saveSelectionState(); 83 | // 84 | if (item.id === 'selectEvent') { 85 | alert('select one event'); 86 | // 87 | } else if (item.id === 'createEvent') { 88 | alert('create one event'); 89 | // 90 | } 91 | // 92 | if (!editor.restoreSelectionState(pos)) { 93 | return; 94 | } 95 | // 96 | editor.insertBox(CALENDAR_BOX_TYPE as BOX_TYPE, null, { 97 | text: new Date().toLocaleDateString(), 98 | }, { 99 | deletePrefix: true, 100 | }); 101 | } 102 | 103 | const calendarBox = { 104 | prefix: '//', 105 | createNode, 106 | getItems, 107 | handleBoxItemSelected, 108 | handleBoxInserted, 109 | handleBoxClicked, 110 | }; 111 | 112 | boxUtils.registerBoxType(CALENDAR_BOX_TYPE as BOX_TYPE, calendarBox); 113 | 114 | // 定义AppID,AppSecret, AppDomain。在自带的测试服务器中,下面三个key不要更改 115 | const AppId = '_LC1xOdRp'; 116 | const AppSecret = '714351167e39568ba734339cc6b997b960ed537153b68c1f7d52b1e87c3be24a'; 117 | const AppDomain = 'wiz.cn'; 118 | 119 | // 初始化服务器地址 120 | const WsServerUrl = window.location.protocol !== 'https:' 121 | ? `ws://${window.location.host}` 122 | : `wss://${window.location.host}`; 123 | 124 | // 定义一个用户。该用户应该是有应用服务器自动获取当前用户身份 125 | // 编辑服务需要提供用户id以及用户的显示名。 126 | const user = { 127 | userId: `${new Date().valueOf()}`, 128 | displayName: 'test user', 129 | avatarUrl: 'https://www.wiz.cn/wp-content/new-uploads/2285af20-4006-11eb-8f21-01eb48012b63.jpeg', 130 | }; 131 | 132 | // 设置编辑器选项 133 | const options = { 134 | serverUrl: WsServerUrl, 135 | user, 136 | }; 137 | 138 | // 从应用服务器获取一个AccessToken。应用服务器许需要负责验证用户对文档的访问权限。 139 | // accessToken采用jwt规范,里面应该包含用户的userId,文档的docId,以及编辑应用的AppId。 140 | // 下面是一个演示例子。在正常强况下,AccessToken应该通过用户自己的应用服务器生成。 141 | // 因为在前端使用JWT加密规范的时候,必须在https协议下面的网页才可以使用。为了演示, 142 | // 我们的自带的测试服务器会提供一个虚拟的token生成功能。(启动服务的时候,需要指定--enable-fake-token-api 参数) 143 | // 144 | 145 | async function fakeGetAccessTokenFromServer(userId: string, docId: string): Promise { 146 | // 147 | const data = { 148 | userId, 149 | docId, 150 | appId: AppId, 151 | permission: 'w', 152 | }; 153 | 154 | const fromHexString = (hexString: string) => { 155 | const parts = hexString.match(/.{1,2}/g); 156 | assert(parts); 157 | const arr = parts.map((byte) => parseInt(byte, 16)); 158 | return new Uint8Array(arr); 159 | }; 160 | // 161 | const key = fromHexString(AppSecret); 162 | 163 | try { 164 | const accessToken = await new EncryptJWT(data) 165 | .setProtectedHeader({ alg: 'dir', enc: 'A256GCM' }) 166 | .setIssuedAt() 167 | .setIssuer(AppDomain) 168 | .setExpirationTime('120s') 169 | .encrypt(key); 170 | 171 | return accessToken; 172 | } catch (err) { 173 | const res = await fetch(`http://${window.location.host}/token/${AppId}/${docId}/${userId}`); 174 | const ret = await res.json(); 175 | return ret.token; 176 | } 177 | } 178 | 179 | // 文档id 180 | const docId = 'my-test-doc-id-box-calendar'; 181 | 182 | (async function loadDocument() { 183 | // 验证身份,获取accessToken 184 | const token = await fakeGetAccessTokenFromServer(user.userId, docId); 185 | 186 | // 生成编辑服务需要的认证信息 187 | const auth: AuthMessage = { 188 | appId: AppId, 189 | ...user, 190 | docId, 191 | token, 192 | permission: 'w', 193 | }; 194 | 195 | // 创建编辑器并加载文档 196 | const editor = createEditor(document.getElementById('editor') as HTMLElement, options, auth); 197 | })(); 198 | -------------------------------------------------------------------------------- /h5/src/box_custom_mention.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-alert */ 2 | // eslint-disable-next-line import/no-unresolved 3 | import EncryptJWT from 'jose/jwt/encrypt'; 4 | import { 5 | createEditor, 6 | assert, 7 | AutoSuggestData, 8 | MentionBoxData, 9 | blockUtils, 10 | boxUtils, 11 | BlockElement, 12 | EditorOptions, 13 | Editor, 14 | BoxData, 15 | BoxNode, 16 | BoxTextChild, 17 | BOX_TYPE, 18 | AutoSuggestOptions, 19 | } from 'wiz-editor/client'; 20 | import { AuthMessage } from 'wiz-editor/commons/auth-message'; 21 | 22 | function hideElement(id: string) { 23 | const elem = document.getElementById(id); 24 | if (!elem) return; 25 | elem.style.display = 'none'; 26 | } 27 | 28 | hideElement('header'); 29 | hideElement('toolbar'); 30 | 31 | // 定义AppID,AppSecret, AppDomain。在自带的测试服务器中,下面三个key不要更改 32 | const AppId = '_LC1xOdRp'; 33 | const AppSecret = '714351167e39568ba734339cc6b997b960ed537153b68c1f7d52b1e87c3be24a'; 34 | const AppDomain = 'wiz.cn'; 35 | 36 | // 初始化服务器地址 37 | const WsServerUrl = window.location.protocol !== 'https:' 38 | ? `ws://${window.location.host}` 39 | : `wss://${window.location.host}`; 40 | 41 | // 定义一个用户。该用户应该是有应用服务器自动获取当前用户身份 42 | // 编辑服务需要提供用户id以及用户的显示名。 43 | const user = { 44 | avatarUrl: 'https://www.wiz.cn/wp-content/new-uploads/2285af20-4006-11eb-8f21-01eb48012b63.jpeg', 45 | userId: `${new Date().valueOf()}`, 46 | displayName: 'test user', 47 | }; 48 | 49 | const NAMES = [ 50 | '龚光杰', 51 | '褚师弟', 52 | '容子矩', 53 | '干光豪', 54 | '葛光佩', 55 | '郁光标', 56 | '吴光胜', 57 | '唐光雄', 58 | '枯荣大师', 59 | '本因大师', 60 | '本观', 61 | '本相', 62 | '本参', 63 | '本尘', 64 | '玄愧', 65 | '玄念', 66 | '玄净', 67 | '慧真', 68 | '慧观', 69 | '慧净', 70 | '慧方', 71 | '慧镜', 72 | '慧轮', 73 | '虚清', 74 | '虚湛', 75 | '虚渊', 76 | '摘星子', 77 | '摩云子', 78 | '天狼子', 79 | '出尘子', 80 | '段延庆', 81 | '叶二娘', 82 | '岳老三', 83 | '云中鹤', 84 | ]; 85 | 86 | const ALL_USERS: AutoSuggestData[] = []; 87 | 88 | NAMES.forEach((name) => { 89 | const user = { 90 | iconUrl: 'http://www.wiz.cn/wp-content/new-uploads/2285af20-4006-11eb-8f21-01eb48012b63.jpeg', 91 | text: name, 92 | id: name, 93 | data: name, 94 | }; 95 | ALL_USERS.push(user); 96 | }); 97 | 98 | 99 | 100 | // -------------------create a custom calendar item---------- 101 | const CUSTOM_MENTION_BOX_TYPE = 'custom-mention'; 102 | 103 | interface CustomMentionData extends BoxData { 104 | customType: 'mention' | 'document', 105 | text: string; 106 | }; 107 | 108 | function createNode(editor: Editor, data: BoxData): BoxNode { 109 | // 110 | const { customType, text } = data as CustomMentionData; 111 | // 112 | if (customType === 'document') { 113 | return { 114 | classes: ['box-custom-mention'], 115 | children: [{ 116 | type: 'text', 117 | text: 'this is online document: ', 118 | } as BoxTextChild, { 119 | type: 'text', 120 | text, 121 | } as BoxTextChild], 122 | } 123 | } 124 | // 125 | return { 126 | classes: ['box-custom-mention'], 127 | children: [{ 128 | type: 'text', 129 | text: 'this is mention: ', 130 | } as BoxTextChild, { 131 | type: 'text', 132 | text, 133 | } as BoxTextChild], 134 | }; 135 | } 136 | 137 | function handleBoxInserted(editor: Editor, data: BoxData): void { 138 | const calendarData = data as CustomMentionData; 139 | console.log('custom mention box inserted:', calendarData); 140 | } 141 | 142 | function handleBoxClicked(editor: Editor, data: BoxData): void { 143 | const calendarData = data as CustomMentionData; 144 | alert(`custom mention clicked: ${calendarData.text}`); 145 | } 146 | 147 | async function getItems(editor: Editor, keywords: string) { 148 | console.log(keywords); 149 | const elem = document.getElementById('custom-mention-item'); 150 | if (elem) { 151 | elem.innerText = keywords; 152 | } 153 | // 响应键盘消息,如果返回空数组,autoSuggest将会关闭。 154 | // 155 | return [{ 156 | iconUrl: '', 157 | text: '', 158 | id: 'custom-mention-item', 159 | data: '', 160 | }]; 161 | } 162 | 163 | function handleBoxItemSelected(editor: Editor, item: AutoSuggestData): void { 164 | } 165 | 166 | 167 | let mentionBoxElement: HTMLElement | null = null; 168 | 169 | // 渲染下拉框。 我们只有一个item,在这里返回item的内容 170 | function renderAutoSuggestItem(editor: Editor, suggestData: AutoSuggestData, options: AutoSuggestOptions): HTMLElement { 171 | // 172 | if (!mentionBoxElement) { 173 | const elem = document.createElement('div'); 174 | elem.style.width = '300px'; 175 | elem.style.height = '300px'; 176 | elem.style.border = '1px solid #ccc'; 177 | elem.style.backgroundColor = '#f0f0f0'; 178 | elem.id = 'custom-mention-item'; 179 | mentionBoxElement = elem; 180 | } 181 | // 182 | return mentionBoxElement; 183 | } 184 | 185 | function handleAutoSuggestHidden() { 186 | if (mentionBoxElement) { 187 | mentionBoxElement.innerText = ''; 188 | } 189 | } 190 | 191 | const customMentionBox = { 192 | prefix: '@', 193 | customSuggest: true, 194 | createNode, 195 | getItems, 196 | handleBoxItemSelected, 197 | handleBoxInserted, 198 | handleBoxClicked, 199 | renderAutoSuggestItem, 200 | handleAutoSuggestHidden, 201 | }; 202 | 203 | boxUtils.registerBoxType(CUSTOM_MENTION_BOX_TYPE as BOX_TYPE, customMentionBox); 204 | 205 | // 设置编辑器选项 206 | const options: EditorOptions = { 207 | serverUrl: WsServerUrl, 208 | disableMentions: true, 209 | callbacks: { 210 | }, 211 | }; 212 | 213 | // 从应用服务器获取一个AccessToken。应用服务器需要负责验证用户对文档的访问权限。 214 | // accessToken采用jwt规范,里面应该包含用户的userId,文档的docId,以及编辑应用的AppId。 215 | // 下面是一个演示例子。在正常情况下,AccessToken应该通过用户自己的应用服务器生成。 216 | // 因为在前端使用JWT加密规范的时候,必须在https协议下面的网页才可以使用。为了演示, 217 | // 我们自带的测试服务器会提供一个虚拟的token生成功能。(启动服务的时候,需要指定--enable-fake-token-api 参数) 218 | // 请勿在正式服务器上面,启用这个参数。 219 | 220 | async function fakeGetAccessTokenFromServer(userId: string, docId: string): Promise { 221 | // 222 | const data = { 223 | userId, 224 | docId, 225 | appId: AppId, 226 | permission: 'w', 227 | }; 228 | 229 | const fromHexString = (hexString: string) => { 230 | const parts = hexString.match(/.{1,2}/g); 231 | assert(parts); 232 | const arr = parts.map((byte) => parseInt(byte, 16)); 233 | return new Uint8Array(arr); 234 | }; 235 | // 236 | const key = fromHexString(AppSecret); 237 | 238 | try { 239 | const accessToken = await new EncryptJWT(data) 240 | .setProtectedHeader({ alg: 'dir', enc: 'A256GCM' }) 241 | .setIssuedAt() 242 | .setIssuer(AppDomain) 243 | .setExpirationTime('120s') 244 | .encrypt(key); 245 | 246 | return accessToken; 247 | } catch (err) { 248 | const res = await fetch(`http://${window.location.host}/token/${AppId}/${docId}/${userId}`); 249 | const ret = await res.json(); 250 | return ret.token; 251 | } 252 | } 253 | 254 | // 文档id 255 | const docId = 'my-test-doc-id-mention'; 256 | 257 | (async function loadDocument() { 258 | // 验证身份,获取accessToken 259 | const token = await fakeGetAccessTokenFromServer(user.userId, docId); 260 | 261 | // 生成编辑服务需要的认证信息 262 | const auth: AuthMessage = { 263 | appId: AppId, 264 | ...user, 265 | docId, 266 | token, 267 | permission: 'w', 268 | }; 269 | 270 | // 创建编辑器并加载文档 271 | const editor = createEditor(document.getElementById('editor') as HTMLElement, options, auth); 272 | })(); 273 | -------------------------------------------------------------------------------- /h5/src/box_date.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-alert */ 2 | // eslint-disable-next-line import/no-unresolved 3 | import EncryptJWT from 'jose/jwt/encrypt'; 4 | import { 5 | createEditor, 6 | assert, 7 | boxUtils, 8 | BoxData, 9 | BoxNode, 10 | BoxTextChild, 11 | Editor, 12 | BoxTemplateData, 13 | BOX_TYPE, 14 | EditorOptions, 15 | } from 'wiz-editor/client'; 16 | import { AuthMessage } from 'wiz-editor/commons/auth-message'; 17 | 18 | function hideElement(id: string) { 19 | const elem = document.getElementById(id); 20 | if (!elem) return; 21 | elem.style.display = 'none'; 22 | } 23 | 24 | hideElement('header'); 25 | hideElement('toolbar'); 26 | 27 | 28 | // -------------------create a custom date item---------- 29 | const DATE_BOX_TYPE = 'date'; 30 | 31 | interface DateBoxData extends BoxData { 32 | text: string; 33 | }; 34 | 35 | function createNode(editor: Editor, data: BoxData): BoxNode { 36 | // 37 | const { text } = data as DateBoxData; 38 | // 39 | return { 40 | classes: ['box-mention'], 41 | children: [{ 42 | type: 'text', 43 | text, 44 | } as BoxTextChild], 45 | }; 46 | } 47 | 48 | function handleBoxInserted(editor: Editor, data: BoxData): void { 49 | const dateData = data as DateBoxData; 50 | console.log('date box inserted:', dateData); 51 | } 52 | 53 | function handleBoxClicked(editor: Editor, data: BoxData): void { 54 | const dateData = data as DateBoxData; 55 | alert(`date clicked: ${dateData.text}`); 56 | } 57 | 58 | async function createBoxData(editor: Editor): Promise { 59 | return { 60 | text: new Date().toLocaleDateString(), 61 | }; 62 | } 63 | 64 | 65 | const dateBox = { 66 | prefix: 'dd', 67 | createNode, 68 | handleBoxInserted, 69 | handleBoxClicked, 70 | createBoxData, 71 | }; 72 | 73 | boxUtils.registerBoxType(DATE_BOX_TYPE as BOX_TYPE, dateBox); 74 | 75 | // 定义AppID,AppSecret, AppDomain。在自带的测试服务器中,下面三个key不要更改 76 | const AppId = '_LC1xOdRp'; 77 | const AppSecret = '714351167e39568ba734339cc6b997b960ed537153b68c1f7d52b1e87c3be24a'; 78 | const AppDomain = 'wiz.cn'; 79 | 80 | // 初始化服务器地址 81 | const WsServerUrl = window.location.protocol !== 'https:' 82 | ? `ws://${window.location.host}` 83 | : `wss://${window.location.host}`; 84 | 85 | // 定义一个用户。该用户应该是有应用服务器自动获取当前用户身份 86 | // 编辑服务需要提供用户id以及用户的显示名。 87 | const user = { 88 | userId: `${new Date().valueOf()}`, 89 | displayName: 'test user', 90 | avatarUrl: 'https://www.wiz.cn/wp-content/new-uploads/2285af20-4006-11eb-8f21-01eb48012b63.jpeg', 91 | }; 92 | 93 | // 设置编辑器选项 94 | const options: EditorOptions = { 95 | serverUrl: WsServerUrl, 96 | }; 97 | 98 | // 从应用服务器获取一个AccessToken。应用服务器许需要负责验证用户对文档的访问权限。 99 | // accessToken采用jwt规范,里面应该包含用户的userId,文档的docId,以及编辑应用的AppId。 100 | // 下面是一个演示例子。在正常强况下,AccessToken应该通过用户自己的应用服务器生成。 101 | // 因为在前端使用JWT加密规范的时候,必须在https协议下面的网页才可以使用。为了演示, 102 | // 我们的自带的测试服务器会提供一个虚拟的token生成功能。(启动服务的时候,需要指定--enable-fake-token-api 参数) 103 | // 104 | 105 | async function fakeGetAccessTokenFromServer(userId: string, docId: string): Promise { 106 | // 107 | const data = { 108 | userId, 109 | docId, 110 | appId: AppId, 111 | permission: 'w', 112 | }; 113 | 114 | const fromHexString = (hexString: string) => { 115 | const parts = hexString.match(/.{1,2}/g); 116 | assert(parts); 117 | const arr = parts.map((byte) => parseInt(byte, 16)); 118 | return new Uint8Array(arr); 119 | }; 120 | // 121 | const key = fromHexString(AppSecret); 122 | 123 | try { 124 | const accessToken = await new EncryptJWT(data) 125 | .setProtectedHeader({ alg: 'dir', enc: 'A256GCM' }) 126 | .setIssuedAt() 127 | .setIssuer(AppDomain) 128 | .setExpirationTime('120s') 129 | .encrypt(key); 130 | 131 | return accessToken; 132 | } catch (err) { 133 | const res = await fetch(`http://${window.location.host}/token/${AppId}/${docId}/${userId}`); 134 | const ret = await res.json(); 135 | return ret.token; 136 | } 137 | } 138 | 139 | // 文档id 140 | const docId = 'my-test-doc-id-box-date'; 141 | 142 | (async function loadDocument() { 143 | // 验证身份,获取accessToken 144 | const token = await fakeGetAccessTokenFromServer(user.userId, docId); 145 | 146 | // 生成编辑服务需要的认证信息 147 | const auth: AuthMessage = { 148 | appId: AppId, 149 | ...user, 150 | docId, 151 | token, 152 | permission: 'w', 153 | }; 154 | 155 | // 创建编辑器并加载文档 156 | const editor = createEditor(document.getElementById('editor') as HTMLElement, options, auth); 157 | })(); 158 | -------------------------------------------------------------------------------- /h5/src/box_label.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-alert */ 2 | // eslint-disable-next-line import/no-unresolved 3 | import EncryptJWT from 'jose/jwt/encrypt'; 4 | import { 5 | createEditor, 6 | assert, 7 | boxUtils, 8 | BoxData, 9 | BoxNode, 10 | BoxTemplateData, 11 | BoxTextChild, 12 | Editor, 13 | AutoSuggestData, 14 | BOX_TYPE, 15 | EditorOptions, 16 | } from 'wiz-editor/client'; 17 | import { AuthMessage } from 'wiz-editor/commons/auth-message'; 18 | 19 | function hideElement(id: string) { 20 | const elem = document.getElementById(id); 21 | if (!elem) return; 22 | elem.style.display = 'none'; 23 | } 24 | 25 | hideElement('header'); 26 | hideElement('toolbar'); 27 | 28 | 29 | // -------------------create a custom label item---------- 30 | const LABEL_BOX_TYPE = 'label'; 31 | 32 | interface LabelBoxData extends BoxData { 33 | color: string; 34 | }; 35 | 36 | function createNode(editor: Editor, data: BoxData): BoxNode { 37 | // 38 | const { color } = data as LabelBoxData; 39 | // 40 | return { 41 | classes: [`label-${color}`, 'label'], 42 | children: [{ 43 | type: 'text', 44 | text: color, 45 | } as BoxTextChild], 46 | }; 47 | } 48 | 49 | function handleBoxInserted(editor: Editor, data: BoxData): void { 50 | const calendarData = data as LabelBoxData; 51 | console.log('label box inserted:', calendarData); 52 | } 53 | 54 | function handleBoxClicked(editor: Editor, data: BoxData): void { 55 | const calendarData = data as LabelBoxData; 56 | alert(`label clicked: ${calendarData.color}`); 57 | } 58 | 59 | async function getItems(editor: Editor, keywords: string): Promise { 60 | console.log(keywords); 61 | return [{ 62 | iconUrl: '', 63 | text: 'red', 64 | id: 'red', 65 | data: '', 66 | }, { 67 | iconUrl: '', 68 | text: 'green', 69 | id: 'green', 70 | data: '', 71 | }, { 72 | iconUrl: '', 73 | text: 'blue', 74 | id: 'blue', 75 | data: '', 76 | }]; 77 | } 78 | 79 | function createBoxDataFromItem(editor: Editor, item: AutoSuggestData): BoxTemplateData { 80 | const color = item.id; 81 | return { 82 | color, 83 | }; 84 | } 85 | 86 | function renderAutoSuggestItem(editor: Editor, suggestData: AutoSuggestData): HTMLElement { 87 | const div = document.createElement('div'); 88 | div.setAttribute('style', `background-color: ${suggestData.text}; border-radius: 10px; width: 100%; height: 24px`); 89 | return div; 90 | } 91 | 92 | const labelBox = { 93 | prefix: 'll', 94 | createNode, 95 | getItems, 96 | createBoxDataFromItem, 97 | handleBoxInserted, 98 | handleBoxClicked, 99 | renderAutoSuggestItem, 100 | }; 101 | 102 | boxUtils.registerBoxType(LABEL_BOX_TYPE as BOX_TYPE, labelBox); 103 | 104 | // 定义AppID,AppSecret, AppDomain。在自带的测试服务器中,下面三个key不要更改 105 | const AppId = '_LC1xOdRp'; 106 | const AppSecret = '714351167e39568ba734339cc6b997b960ed537153b68c1f7d52b1e87c3be24a'; 107 | const AppDomain = 'wiz.cn'; 108 | 109 | // 初始化服务器地址 110 | const WsServerUrl = window.location.protocol !== 'https:' 111 | ? `ws://${window.location.host}` 112 | : `wss://${window.location.host}`; 113 | 114 | // 定义一个用户。该用户应该是有应用服务器自动获取当前用户身份 115 | // 编辑服务需要提供用户id以及用户的显示名。 116 | const user = { 117 | userId: `${new Date().valueOf()}`, 118 | displayName: 'test user', 119 | avatarUrl: 'https://www.wiz.cn/wp-content/new-uploads/2285af20-4006-11eb-8f21-01eb48012b63.jpeg', 120 | }; 121 | 122 | // 设置编辑器选项 123 | const options: EditorOptions = { 124 | serverUrl: WsServerUrl, 125 | }; 126 | 127 | // 从应用服务器获取一个AccessToken。应用服务器需要负责验证用户对文档的访问权限。 128 | // accessToken采用jwt规范,里面应该包含用户的userId,文档的docId,以及编辑应用的AppId。 129 | // 下面是一个演示例子。在正常情况下,AccessToken应该通过用户自己的应用服务器生成。 130 | // 因为在前端使用JWT加密规范的时候,必须在https协议下面的网页才可以使用。为了演示, 131 | // 我们自带的测试服务器会提供一个虚拟的token生成功能。(启动服务的时候,需要指定--enable-fake-token-api 参数) 132 | // 请勿在正式服务器上面,启用这个参数。 133 | 134 | async function fakeGetAccessTokenFromServer(userId: string, docId: string): Promise { 135 | // 136 | const data = { 137 | userId, 138 | docId, 139 | appId: AppId, 140 | permission: 'w', 141 | }; 142 | 143 | const fromHexString = (hexString: string) => { 144 | const parts = hexString.match(/.{1,2}/g); 145 | assert(parts); 146 | const arr = parts.map((byte) => parseInt(byte, 16)); 147 | return new Uint8Array(arr); 148 | }; 149 | // 150 | const key = fromHexString(AppSecret); 151 | 152 | try { 153 | const accessToken = await new EncryptJWT(data) 154 | .setProtectedHeader({ alg: 'dir', enc: 'A256GCM' }) 155 | .setIssuedAt() 156 | .setIssuer(AppDomain) 157 | .setExpirationTime('120s') 158 | .encrypt(key); 159 | 160 | return accessToken; 161 | } catch (err) { 162 | const res = await fetch(`http://${window.location.host}/token/${AppId}/${docId}/${userId}`); 163 | const ret = await res.json(); 164 | return ret.token; 165 | } 166 | } 167 | 168 | // 文档id 169 | const docId = 'my-test-doc-id-box-label'; 170 | 171 | (async function loadDocument() { 172 | // 验证身份,获取accessToken 173 | const token = await fakeGetAccessTokenFromServer(user.userId, docId); 174 | 175 | // 生成编辑服务需要的认证信息 176 | const auth: AuthMessage = { 177 | appId: AppId, 178 | ...user, 179 | docId, 180 | token, 181 | permission: 'w', 182 | }; 183 | 184 | // 创建编辑器并加载文档 185 | const editor = createEditor(document.getElementById('editor') as HTMLElement, options, auth); 186 | })(); 187 | -------------------------------------------------------------------------------- /h5/src/index.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-alert */ 2 | // eslint-disable-next-line import/no-unresolved 3 | import { 4 | LANGS, 5 | createEditor, 6 | assert, 7 | AutoSuggestData, 8 | docData2Text, 9 | EditorOptions, 10 | Editor, 11 | } from 'wiz-editor/client'; 12 | import { AuthMessage, AuthPermission } from 'wiz-editor/commons/auth-message'; 13 | 14 | function hideElement(id: string) { 15 | const elem = document.getElementById(id); 16 | if (!elem) return; 17 | elem.style.display = 'none'; 18 | } 19 | 20 | hideElement('header'); 21 | hideElement('toolbar'); 22 | 23 | const AppId = '_LC1xOdRp'; 24 | 25 | // --------------------------- mention data ---------------- 26 | const NAMES = [ 27 | '龚光杰', 28 | '褚师弟', 29 | '容子矩', 30 | '干光豪', 31 | '葛光佩', 32 | '郁光标', 33 | '吴光胜', 34 | '唐光雄', 35 | '枯荣大师', 36 | '本因大师', 37 | '本观', 38 | '本相', 39 | '本参', 40 | '本尘', 41 | '玄愧', 42 | '玄念', 43 | '玄净', 44 | '慧真', 45 | '慧观', 46 | '慧净', 47 | '慧方', 48 | '慧镜', 49 | '慧轮', 50 | '虚清', 51 | '虚湛', 52 | '虚渊', 53 | '摘星子', 54 | '摩云子', 55 | '天狼子', 56 | '出尘子', 57 | '段延庆', 58 | '叶二娘', 59 | '岳老三', 60 | '云中鹤', 61 | ]; 62 | 63 | const ALL_USERS = [{ 64 | iconUrl: 'https://www.wiz.cn/wp-content/new-uploads/e89745c0-3f7a-11eb-8f21-01eb48012b63.jpeg', 65 | text: 'Steve', 66 | id: 'weishijun@wiz.cn', 67 | data: '', 68 | }, { 69 | iconUrl: 'https://www.wiz.cn/wp-content/new-uploads/2285af20-4006-11eb-8f21-01eb48012b63.jpeg', 70 | text: 'zTree', 71 | id: 'zqg@wiz.cn', 72 | data: '', 73 | }]; 74 | 75 | NAMES.forEach((name) => { 76 | const user = { 77 | iconUrl: 'https://www.wiz.cn/wp-content/new-uploads/2285af20-4006-11eb-8f21-01eb48012b63.jpeg', 78 | text: name, 79 | id: name, 80 | data: name, 81 | }; 82 | ALL_USERS.push(user); 83 | }); 84 | 85 | async function fakeGetMentionItems(editor: Editor, keywords: string): Promise { 86 | assert(keywords !== undefined); 87 | console.log(keywords); 88 | if (!keywords) { 89 | return ALL_USERS; 90 | } 91 | return ALL_USERS.filter((user) => user.text.toLowerCase().indexOf(keywords.toLowerCase()) !== -1); 92 | } 93 | 94 | const urlQuery = new URLSearchParams(window.location.search); 95 | 96 | const WsServerUrl = window.location.protocol !== 'https:' 97 | ? `ws://${window.location.host}` 98 | : `wss://${window.location.host}`; 99 | 100 | const user = { 101 | avatarUrl: 'https://www.wiz.cn/wp-content/new-uploads/2285af20-4006-11eb-8f21-01eb48012b63.jpeg', 102 | userId: `${new Date().valueOf()}`, 103 | displayName: NAMES[new Date().valueOf() % NAMES.length], 104 | }; 105 | 106 | function replaceUrl(docId: string) { 107 | const now = window.location.href; 108 | if (now.endsWith(docId)) return; 109 | // 110 | const newUrl = `${window.location.origin}/?id=${docId}`; 111 | window.history.pushState({}, '', newUrl); 112 | } 113 | 114 | function handleSave(editor: Editor, data: any) { 115 | console.log(JSON.stringify(data, null, 2)); 116 | const text = docData2Text(data); 117 | console.log('------------------- document text --------------------'); 118 | console.log(text); 119 | console.log('------------------------------------------------------'); 120 | } 121 | 122 | function handleLoad(editor: Editor, data: any): void { 123 | console.log(`${editor.docId()} loaded`); 124 | assert(data); 125 | replaceUrl(editor.docId()); 126 | } 127 | 128 | function handleError(editor: Editor, error: Error): void { 129 | console.log(`${editor.docId()} error: ${error}`); 130 | alert(error); 131 | } 132 | 133 | async function fakeGetAccessTokenFromServer(userId: string, docId: string, permission: AuthPermission): Promise { 134 | // 135 | const res = await fetch(`http://${window.location.host}/token/${AppId}/${docId}/${userId}`); 136 | const ret = await res.json(); 137 | return ret.token; 138 | } 139 | 140 | async function loadDocument(docId: string) { 141 | const options: EditorOptions = { 142 | lang: 'zh-CN', 143 | serverUrl: WsServerUrl, 144 | placeholder: '请输入笔记正文', 145 | callbacks: { 146 | onSave: handleSave, 147 | onLoad: handleLoad, 148 | onError: handleError, 149 | onGetMentionItems: fakeGetMentionItems, 150 | }, 151 | }; 152 | 153 | const token = await fakeGetAccessTokenFromServer(user.userId, docId, 'w'); 154 | const auth: AuthMessage = { 155 | ...user, 156 | appId: AppId, 157 | userId: user.userId, 158 | permission: 'w', 159 | docId, 160 | token, 161 | }; 162 | 163 | createEditor(document.getElementById('editor') as HTMLElement, options, auth); 164 | } 165 | 166 | const docId = urlQuery.get('id') || '_ny1Adsk2'; 167 | loadDocument(docId); 168 | -------------------------------------------------------------------------------- /h5/src/mention.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-alert */ 2 | // eslint-disable-next-line import/no-unresolved 3 | import EncryptJWT from 'jose/jwt/encrypt'; 4 | import { 5 | createEditor, 6 | assert, 7 | AutoSuggestData, 8 | MentionBoxData, 9 | blockUtils, 10 | BlockElement, 11 | EditorOptions, 12 | Editor, 13 | } from 'wiz-editor/client'; 14 | import { AuthMessage } from 'wiz-editor/commons/auth-message'; 15 | 16 | function hideElement(id: string) { 17 | const elem = document.getElementById(id); 18 | if (!elem) return; 19 | elem.style.display = 'none'; 20 | } 21 | 22 | hideElement('header'); 23 | hideElement('toolbar'); 24 | 25 | // 定义AppID,AppSecret, AppDomain。在自带的测试服务器中,下面三个key不要更改 26 | const AppId = '_LC1xOdRp'; 27 | const AppSecret = '714351167e39568ba734339cc6b997b960ed537153b68c1f7d52b1e87c3be24a'; 28 | const AppDomain = 'wiz.cn'; 29 | 30 | // 初始化服务器地址 31 | const WsServerUrl = window.location.protocol !== 'https:' 32 | ? `ws://${window.location.host}` 33 | : `wss://${window.location.host}`; 34 | 35 | // 定义一个用户。该用户应该是有应用服务器自动获取当前用户身份 36 | // 编辑服务需要提供用户id以及用户的显示名。 37 | const user = { 38 | avatarUrl: 'https://www.wiz.cn/wp-content/new-uploads/2285af20-4006-11eb-8f21-01eb48012b63.jpeg', 39 | userId: `${new Date().valueOf()}`, 40 | displayName: 'test user', 41 | }; 42 | 43 | const NAMES = [ 44 | '龚光杰', 45 | '褚师弟', 46 | '容子矩', 47 | '干光豪', 48 | '葛光佩', 49 | '郁光标', 50 | '吴光胜', 51 | '唐光雄', 52 | '枯荣大师', 53 | '本因大师', 54 | '本观', 55 | '本相', 56 | '本参', 57 | '本尘', 58 | '玄愧', 59 | '玄念', 60 | '玄净', 61 | '慧真', 62 | '慧观', 63 | '慧净', 64 | '慧方', 65 | '慧镜', 66 | '慧轮', 67 | '虚清', 68 | '虚湛', 69 | '虚渊', 70 | '摘星子', 71 | '摩云子', 72 | '天狼子', 73 | '出尘子', 74 | '段延庆', 75 | '叶二娘', 76 | '岳老三', 77 | '云中鹤', 78 | ]; 79 | 80 | const ALL_USERS: AutoSuggestData[] = []; 81 | 82 | NAMES.forEach((name) => { 83 | const user = { 84 | iconUrl: 'http://www.wiz.cn/wp-content/new-uploads/2285af20-4006-11eb-8f21-01eb48012b63.jpeg', 85 | text: name, 86 | id: name, 87 | data: name, 88 | }; 89 | ALL_USERS.push(user); 90 | }); 91 | 92 | 93 | async function fakeGetMentionItems(editor: Editor, keywords: string): Promise { 94 | assert(keywords !== undefined); 95 | console.log(keywords); 96 | if (!keywords) { 97 | return ALL_USERS; 98 | } 99 | return ALL_USERS.filter((user) => user.text.toLowerCase().indexOf(keywords.toLowerCase()) !== -1); 100 | } 101 | 102 | function handleMentionInserted(editor: Editor, boxData: MentionBoxData, block: BlockElement, pos: number) { 103 | console.log(`mention ${JSON.stringify(boxData)} inserted at ${pos}`); 104 | const leftText = blockUtils.toText(block, 0, pos); 105 | const rightText = blockUtils.toText(block, pos + 1, -1); 106 | alert(`context text:\n\n${leftText}\n\n${rightText}`); 107 | } 108 | 109 | function handleMentionClicked(editor: Editor, boxData: MentionBoxData) { 110 | alert(`you clicked ${boxData.text} (${boxData.mentionId})`); 111 | } 112 | 113 | // 设置编辑器选项 114 | const options: EditorOptions = { 115 | serverUrl: WsServerUrl, 116 | callbacks: { 117 | onGetMentionItems: fakeGetMentionItems, 118 | onMentionInserted: handleMentionInserted, 119 | onMentionClicked: handleMentionClicked, 120 | }, 121 | }; 122 | 123 | // 从应用服务器获取一个AccessToken。应用服务器需要负责验证用户对文档的访问权限。 124 | // accessToken采用jwt规范,里面应该包含用户的userId,文档的docId,以及编辑应用的AppId。 125 | // 下面是一个演示例子。在正常情况下,AccessToken应该通过用户自己的应用服务器生成。 126 | // 因为在前端使用JWT加密规范的时候,必须在https协议下面的网页才可以使用。为了演示, 127 | // 我们自带的测试服务器会提供一个虚拟的token生成功能。(启动服务的时候,需要指定--enable-fake-token-api 参数) 128 | // 请勿在正式服务器上面,启用这个参数。 129 | 130 | async function fakeGetAccessTokenFromServer(userId: string, docId: string): Promise { 131 | // 132 | const data = { 133 | userId, 134 | docId, 135 | appId: AppId, 136 | permission: 'w', 137 | }; 138 | 139 | const fromHexString = (hexString: string) => { 140 | const parts = hexString.match(/.{1,2}/g); 141 | assert(parts); 142 | const arr = parts.map((byte) => parseInt(byte, 16)); 143 | return new Uint8Array(arr); 144 | }; 145 | // 146 | const key = fromHexString(AppSecret); 147 | 148 | try { 149 | const accessToken = await new EncryptJWT(data) 150 | .setProtectedHeader({ alg: 'dir', enc: 'A256GCM' }) 151 | .setIssuedAt() 152 | .setIssuer(AppDomain) 153 | .setExpirationTime('120s') 154 | .encrypt(key); 155 | 156 | return accessToken; 157 | } catch (err) { 158 | const res = await fetch(`http://${window.location.host}/token/${AppId}/${docId}/${userId}`); 159 | const ret = await res.json(); 160 | return ret.token; 161 | } 162 | } 163 | 164 | // 文档id 165 | const docId = 'my-test-doc-id-mention'; 166 | 167 | (async function loadDocument() { 168 | // 验证身份,获取accessToken 169 | const token = await fakeGetAccessTokenFromServer(user.userId, docId); 170 | 171 | // 生成编辑服务需要的认证信息 172 | const auth: AuthMessage = { 173 | appId: AppId, 174 | ...user, 175 | docId, 176 | token, 177 | permission: 'w', 178 | }; 179 | 180 | // 创建编辑器并加载文档 181 | const editor = createEditor(document.getElementById('editor') as HTMLElement, options, auth); 182 | })(); 183 | -------------------------------------------------------------------------------- /h5/src/simple.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-alert */ 2 | // eslint-disable-next-line import/no-unresolved 3 | import EncryptJWT from 'jose/jwt/encrypt'; 4 | import { 5 | createEditor, 6 | assert, 7 | } from 'wiz-editor/client'; 8 | import { AuthMessage } from 'wiz-editor/commons/auth-message'; 9 | 10 | function hideElement(id: string) { 11 | const elem = document.getElementById(id); 12 | if (!elem) return; 13 | elem.style.display = 'none'; 14 | } 15 | 16 | hideElement('header'); 17 | hideElement('toolbar'); 18 | 19 | // 定义AppID,AppSecret, AppDomain。在自带的测试服务器中,下面三个key不要更改 20 | const AppId = '_LC1xOdRp'; 21 | const AppSecret = '714351167e39568ba734339cc6b997b960ed537153b68c1f7d52b1e87c3be24a'; 22 | const AppDomain = 'wiz.cn'; 23 | 24 | // 初始化服务器地址 25 | const WsServerUrl = window.location.protocol !== 'https:' 26 | ? `ws://${window.location.host}` 27 | : `wss://${window.location.host}`; 28 | 29 | // 定义一个用户。该用户应该是有应用服务器自动获取当前用户身份 30 | // 编辑服务需要提供用户id以及用户的显示名。 31 | const user = { 32 | userId: `${new Date().valueOf()}`, 33 | displayName: 'test user', 34 | avatarUrl: 'https://www.wiz.cn/wp-content/new-uploads/2285af20-4006-11eb-8f21-01eb48012b63.jpeg', 35 | }; 36 | 37 | // 设置编辑器选项 38 | const options = { 39 | serverUrl: WsServerUrl, 40 | user, 41 | }; 42 | 43 | // 从应用服务器获取一个AccessToken。应用服务器需要负责验证用户对文档的访问权限。 44 | // accessToken采用jwt规范,里面应该包含用户的userId,文档的docId,以及编辑应用的AppId。 45 | // 下面是一个演示例子。在正常情况下,AccessToken应该通过用户自己的应用服务器生成。 46 | // 因为在前端使用JWT加密规范的时候,必须在https协议下面的网页才可以使用。为了演示, 47 | // 我们自带的测试服务器会提供一个虚拟的token生成功能。(启动服务的时候,需要指定--enable-fake-token-api 参数) 48 | // 请勿在正式服务器上面,启用这个参数。 49 | 50 | async function fakeGetAccessTokenFromServer(userId: string, docId: string): Promise { 51 | // 52 | const data = { 53 | userId, 54 | docId, 55 | appId: AppId, 56 | permission: 'w', 57 | }; 58 | 59 | const fromHexString = (hexString: string) => { 60 | const parts = hexString.match(/.{1,2}/g); 61 | assert(parts); 62 | const arr = parts.map((byte) => parseInt(byte, 16)); 63 | return new Uint8Array(arr); 64 | }; 65 | // 66 | const key = fromHexString(AppSecret); 67 | 68 | try { 69 | const accessToken = await new EncryptJWT(data) 70 | .setProtectedHeader({ alg: 'dir', enc: 'A256GCM' }) 71 | .setIssuedAt() 72 | .setIssuer(AppDomain) 73 | .setExpirationTime('120s') 74 | .encrypt(key); 75 | 76 | return accessToken; 77 | } catch (err) { 78 | const res = await fetch(`http://${window.location.host}/token/${AppId}/${docId}/${userId}`); 79 | const ret = await res.json(); 80 | return ret.token; 81 | } 82 | } 83 | 84 | // 文档id 85 | const docId = 'my-test-doc-id-simple'; 86 | 87 | (async function loadDocument() { 88 | // 验证身份,获取accessToken 89 | const token = await fakeGetAccessTokenFromServer(user.userId, docId); 90 | 91 | // 生成编辑服务需要的认证信息 92 | const auth: AuthMessage = { 93 | appId: AppId, 94 | docId, 95 | token, 96 | permission: 'w', 97 | ...user, 98 | }; 99 | 100 | // 创建编辑器并加载文档 101 | const editor = createEditor(document.getElementById('editor') as HTMLElement, options, auth); 102 | })(); 103 | -------------------------------------------------------------------------------- /h5/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | 5 | /* Basic Options */ 6 | // "incremental": true, /* Enable incremental compilation */ 7 | "target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */ 8 | "module": "es6", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ 9 | "lib": [ 10 | "es2017", 11 | "dom" 12 | ], /* Specify library files to be included in the compilation. */ 13 | // "allowJs": true, /* Allow javascript files to be compiled. */ 14 | // "checkJs": true, /* Report errors in .js files. */ 15 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 16 | // "declaration": true, /* Generates corresponding '.d.ts' file. */ 17 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 18 | "sourceMap": true, /* Generates corresponding '.map' file. */ 19 | // "outFile": "./", /* Concatenate and emit output to single file. */ 20 | "outDir": "./dist", /* Redirect output structure to the directory. */ 21 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 22 | // "composite": true, /* Enable project compilation */ 23 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 24 | // "removeComments": true, /* Do not emit comments to output. */ 25 | // "noEmit": true, /* Do not emit outputs. */ 26 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 27 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 28 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 29 | 30 | /* Strict Type-Checking Options */ 31 | "strict": true, /* Enable all strict type-checking options. */ 32 | "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 33 | // "strictNullChecks": true, /* Enable strict null checks. */ 34 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 35 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 36 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 37 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 38 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 39 | 40 | /* Additional Checks */ 41 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 42 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 43 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 44 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 45 | 46 | /* Module Resolution Options */ 47 | "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 48 | "baseUrl": "./src", /* Base directory to resolve non-absolute module names. */ 49 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 50 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 51 | "typeRoots": [ 52 | "node_modules/@types" 53 | ], /* List of folders to include type definitions from. */ 54 | // "types": [], /* Type declaration files to be included in compilation. */ 55 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 56 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 57 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 58 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 59 | 60 | /* Source Map Options */ 61 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 62 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 63 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 64 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 65 | 66 | /* Experimental Options */ 67 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 68 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 69 | 70 | /* Advanced Options */ 71 | "skipLibCheck": true, /* Skip type checking of declaration files. */ 72 | "forceConsistentCasingInFileNames": true, /* Disallow inconsistently-cased references to the same file. */ 73 | "paths": { 74 | "commons/*": ["../../commons/*"] 75 | }, 76 | "resolveJsonModule": true 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /h5/webpack.calendar.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const { merge } = require('webpack-merge'); 3 | const path = require('path'); 4 | const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer'); 5 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 6 | 7 | const pluginsArray = [ 8 | // fix "process is not defined" error: 9 | // (do "npm install process" before running the build) 10 | new webpack.ProvidePlugin({ 11 | process: 'process/browser', 12 | }), 13 | new HtmlWebpackPlugin({ 14 | filename: 'index.html', 15 | template: 'index.html', 16 | }), 17 | ]; 18 | 19 | const common = { 20 | entry: './src/box_calendar.ts', 21 | module: { 22 | rules: [ 23 | { 24 | test: /\.tsx?$/, 25 | use: 'ts-loader', 26 | exclude: /node_modules/, 27 | }, 28 | { 29 | test: /\.css$/, 30 | // include: path.join(__dirname, 'src/static/css'), 31 | use: [ 32 | 'style-loader', 33 | { 34 | loader: 'typings-for-css-modules-loader', 35 | options: { 36 | modules: false, 37 | namedExport: false, 38 | }, 39 | }, 40 | ], 41 | }, 42 | ], 43 | }, 44 | resolve: { 45 | extensions: ['.tsx', '.ts', '.js'], 46 | alias: { 47 | commons: path.resolve(__dirname, '../commons'), 48 | }, 49 | }, 50 | plugins: pluginsArray, 51 | output: { 52 | filename: 'libs/[name].[contenthash].js', 53 | path: path.resolve(__dirname, `./dist`), 54 | globalObject: 'this', 55 | }, 56 | }; 57 | 58 | 59 | module.exports = (env) => { 60 | const pluginsArray = [ 61 | new webpack.DefinePlugin({ 62 | NODE_ENV: JSON.stringify('development'), 63 | }), 64 | ]; 65 | 66 | if (env && env.analyze) { 67 | pluginsArray.push(new BundleAnalyzerPlugin()); 68 | } 69 | 70 | return merge(common, { 71 | mode: 'development', 72 | devtool: 'inline-source-map', 73 | devServer: { 74 | host: '0.0.0.0', 75 | disableHostCheck: true, 76 | contentBase: path.join(__dirname, 'dist'), 77 | compress: true, 78 | port: 3000, 79 | }, 80 | plugins: pluginsArray, 81 | }); 82 | }; 83 | -------------------------------------------------------------------------------- /h5/webpack.custom.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const { merge } = require('webpack-merge'); 3 | const path = require('path'); 4 | const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer'); 5 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 6 | 7 | const pluginsArray = [ 8 | // fix "process is not defined" error: 9 | // (do "npm install process" before running the build) 10 | new webpack.ProvidePlugin({ 11 | process: 'process/browser', 12 | }), 13 | new HtmlWebpackPlugin({ 14 | filename: 'index.html', 15 | template: 'index.html', 16 | }), 17 | ]; 18 | 19 | const common = { 20 | entry: './src/custom.ts', 21 | module: { 22 | rules: [ 23 | { 24 | test: /\.tsx?$/, 25 | use: 'ts-loader', 26 | exclude: /node_modules/, 27 | }, 28 | { 29 | test: /\.css$/, 30 | // include: path.join(__dirname, 'src/static/css'), 31 | use: [ 32 | 'style-loader', 33 | { 34 | loader: 'typings-for-css-modules-loader', 35 | options: { 36 | modules: false, 37 | namedExport: false, 38 | }, 39 | }, 40 | ], 41 | }, 42 | ], 43 | }, 44 | resolve: { 45 | extensions: ['.tsx', '.ts', '.js'], 46 | alias: { 47 | commons: path.resolve(__dirname, '../commons'), 48 | }, 49 | }, 50 | plugins: pluginsArray, 51 | output: { 52 | filename: 'libs/[name].[contenthash].js', 53 | path: path.resolve(__dirname, `./dist`), 54 | globalObject: 'this', 55 | }, 56 | }; 57 | 58 | 59 | module.exports = (env) => { 60 | const pluginsArray = [ 61 | new webpack.DefinePlugin({ 62 | NODE_ENV: JSON.stringify('development'), 63 | }), 64 | ]; 65 | 66 | if (env && env.analyze) { 67 | pluginsArray.push(new BundleAnalyzerPlugin()); 68 | } 69 | 70 | return merge(common, { 71 | mode: 'development', 72 | devtool: 'inline-source-map', 73 | devServer: { 74 | host: '0.0.0.0', 75 | disableHostCheck: true, 76 | contentBase: path.join(__dirname, 'dist'), 77 | compress: true, 78 | port: 3000, 79 | }, 80 | plugins: pluginsArray, 81 | }); 82 | }; 83 | -------------------------------------------------------------------------------- /h5/webpack.custom.mention.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const { merge } = require('webpack-merge'); 3 | const path = require('path'); 4 | const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer'); 5 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 6 | 7 | const pluginsArray = [ 8 | // fix "process is not defined" error: 9 | // (do "npm install process" before running the build) 10 | new webpack.ProvidePlugin({ 11 | process: 'process/browser', 12 | }), 13 | new HtmlWebpackPlugin({ 14 | filename: 'index.html', 15 | template: 'index.html', 16 | }), 17 | ]; 18 | 19 | const common = { 20 | entry: './src/box_custom_mention.ts', 21 | module: { 22 | rules: [ 23 | { 24 | test: /\.tsx?$/, 25 | use: 'ts-loader', 26 | exclude: /node_modules/, 27 | }, 28 | { 29 | test: /\.css$/, 30 | // include: path.join(__dirname, 'src/static/css'), 31 | use: [ 32 | 'style-loader', 33 | { 34 | loader: 'typings-for-css-modules-loader', 35 | options: { 36 | modules: false, 37 | namedExport: false, 38 | }, 39 | }, 40 | ], 41 | }, 42 | ], 43 | }, 44 | resolve: { 45 | extensions: ['.tsx', '.ts', '.js'], 46 | alias: { 47 | commons: path.resolve(__dirname, '../commons'), 48 | }, 49 | }, 50 | plugins: pluginsArray, 51 | output: { 52 | filename: 'libs/[name].[contenthash].js', 53 | path: path.resolve(__dirname, `./dist`), 54 | globalObject: 'this', 55 | }, 56 | }; 57 | 58 | 59 | module.exports = (env) => { 60 | const pluginsArray = [ 61 | new webpack.DefinePlugin({ 62 | NODE_ENV: JSON.stringify('development'), 63 | }), 64 | ]; 65 | 66 | if (env && env.analyze) { 67 | pluginsArray.push(new BundleAnalyzerPlugin()); 68 | } 69 | 70 | return merge(common, { 71 | mode: 'development', 72 | devtool: 'inline-source-map', 73 | devServer: { 74 | host: '0.0.0.0', 75 | disableHostCheck: true, 76 | contentBase: path.join(__dirname, 'dist'), 77 | compress: true, 78 | port: 3000, 79 | }, 80 | plugins: pluginsArray, 81 | }); 82 | }; 83 | -------------------------------------------------------------------------------- /h5/webpack.date.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const { merge } = require('webpack-merge'); 3 | const path = require('path'); 4 | const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer'); 5 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 6 | 7 | const pluginsArray = [ 8 | // fix "process is not defined" error: 9 | // (do "npm install process" before running the build) 10 | new webpack.ProvidePlugin({ 11 | process: 'process/browser', 12 | }), 13 | new HtmlWebpackPlugin({ 14 | filename: 'index.html', 15 | template: 'index.html', 16 | }), 17 | ]; 18 | 19 | const common = { 20 | entry: './src/box_date.ts', 21 | module: { 22 | rules: [ 23 | { 24 | test: /\.tsx?$/, 25 | use: 'ts-loader', 26 | exclude: /node_modules/, 27 | }, 28 | { 29 | test: /\.css$/, 30 | // include: path.join(__dirname, 'src/static/css'), 31 | use: [ 32 | 'style-loader', 33 | { 34 | loader: 'typings-for-css-modules-loader', 35 | options: { 36 | modules: false, 37 | namedExport: false, 38 | }, 39 | }, 40 | ], 41 | }, 42 | ], 43 | }, 44 | resolve: { 45 | extensions: ['.tsx', '.ts', '.js'], 46 | alias: { 47 | commons: path.resolve(__dirname, '../commons'), 48 | }, 49 | }, 50 | plugins: pluginsArray, 51 | output: { 52 | filename: 'libs/[name].[contenthash].js', 53 | path: path.resolve(__dirname, `./dist`), 54 | globalObject: 'this', 55 | }, 56 | }; 57 | 58 | 59 | module.exports = (env) => { 60 | const pluginsArray = [ 61 | new webpack.DefinePlugin({ 62 | NODE_ENV: JSON.stringify('development'), 63 | }), 64 | ]; 65 | 66 | if (env && env.analyze) { 67 | pluginsArray.push(new BundleAnalyzerPlugin()); 68 | } 69 | 70 | return merge(common, { 71 | mode: 'development', 72 | devtool: 'inline-source-map', 73 | devServer: { 74 | host: '0.0.0.0', 75 | disableHostCheck: true, 76 | contentBase: path.join(__dirname, 'dist'), 77 | compress: true, 78 | port: 3000, 79 | }, 80 | plugins: pluginsArray, 81 | }); 82 | }; 83 | -------------------------------------------------------------------------------- /h5/webpack.dev.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const { merge } = require('webpack-merge'); 3 | const path = require('path'); 4 | const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer'); 5 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 6 | 7 | const pluginsArray = [ 8 | // fix "process is not defined" error: 9 | // (do "npm install process" before running the build) 10 | new webpack.ProvidePlugin({ 11 | process: 'process/browser', 12 | }), 13 | new HtmlWebpackPlugin({ 14 | filename: 'index.html', 15 | template: 'index.html', 16 | }), 17 | ]; 18 | 19 | const common = { 20 | entry: './src/index.ts', 21 | module: { 22 | rules: [ 23 | { 24 | test: /\.tsx?$/, 25 | use: 'ts-loader', 26 | exclude: /node_modules/, 27 | }, 28 | { 29 | test: /\.css$/, 30 | // include: path.join(__dirname, 'src/static/css'), 31 | use: [ 32 | 'style-loader', 33 | { 34 | loader: 'typings-for-css-modules-loader', 35 | options: { 36 | modules: false, 37 | namedExport: false, 38 | }, 39 | }, 40 | ], 41 | }, 42 | ], 43 | }, 44 | resolve: { 45 | extensions: ['.tsx', '.ts', '.js'], 46 | alias: { 47 | commons: path.resolve(__dirname, '../commons'), 48 | }, 49 | }, 50 | plugins: pluginsArray, 51 | output: { 52 | filename: 'libs/[name].[contenthash].js', 53 | path: path.resolve(__dirname, `./dist`), 54 | globalObject: 'this', 55 | }, 56 | }; 57 | 58 | 59 | module.exports = (env) => { 60 | const pluginsArray = [ 61 | new webpack.DefinePlugin({ 62 | NODE_ENV: JSON.stringify('development'), 63 | }), 64 | ]; 65 | 66 | if (env && env.analyze) { 67 | pluginsArray.push(new BundleAnalyzerPlugin()); 68 | } 69 | 70 | return merge(common, { 71 | mode: 'development', 72 | devtool: 'inline-source-map', 73 | devServer: { 74 | host: '0.0.0.0', 75 | disableHostCheck: true, 76 | contentBase: path.join(__dirname, 'dist'), 77 | compress: true, 78 | port: 3000, 79 | }, 80 | plugins: pluginsArray, 81 | }); 82 | }; 83 | -------------------------------------------------------------------------------- /h5/webpack.label.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const { merge } = require('webpack-merge'); 3 | const path = require('path'); 4 | const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer'); 5 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 6 | 7 | const pluginsArray = [ 8 | // fix "process is not defined" error: 9 | // (do "npm install process" before running the build) 10 | new webpack.ProvidePlugin({ 11 | process: 'process/browser', 12 | }), 13 | new HtmlWebpackPlugin({ 14 | filename: 'index.html', 15 | template: 'index.html', 16 | }), 17 | ]; 18 | 19 | const common = { 20 | entry: './src/box_label.ts', 21 | module: { 22 | rules: [ 23 | { 24 | test: /\.tsx?$/, 25 | use: 'ts-loader', 26 | exclude: /node_modules/, 27 | }, 28 | { 29 | test: /\.css$/, 30 | // include: path.join(__dirname, 'src/static/css'), 31 | use: [ 32 | 'style-loader', 33 | { 34 | loader: 'typings-for-css-modules-loader', 35 | options: { 36 | modules: false, 37 | namedExport: false, 38 | }, 39 | }, 40 | ], 41 | }, 42 | ], 43 | }, 44 | resolve: { 45 | extensions: ['.tsx', '.ts', '.js'], 46 | alias: { 47 | commons: path.resolve(__dirname, '../commons'), 48 | }, 49 | }, 50 | plugins: pluginsArray, 51 | output: { 52 | filename: 'libs/[name].[contenthash].js', 53 | path: path.resolve(__dirname, `./dist`), 54 | globalObject: 'this', 55 | }, 56 | }; 57 | 58 | 59 | module.exports = (env) => { 60 | const pluginsArray = [ 61 | new webpack.DefinePlugin({ 62 | NODE_ENV: JSON.stringify('development'), 63 | }), 64 | ]; 65 | 66 | if (env && env.analyze) { 67 | pluginsArray.push(new BundleAnalyzerPlugin()); 68 | } 69 | 70 | return merge(common, { 71 | mode: 'development', 72 | devtool: 'inline-source-map', 73 | devServer: { 74 | host: '0.0.0.0', 75 | disableHostCheck: true, 76 | contentBase: path.join(__dirname, 'dist'), 77 | compress: true, 78 | port: 3000, 79 | }, 80 | plugins: pluginsArray, 81 | }); 82 | }; 83 | -------------------------------------------------------------------------------- /h5/webpack.mention.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const { merge } = require('webpack-merge'); 3 | const path = require('path'); 4 | const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer'); 5 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 6 | 7 | const pluginsArray = [ 8 | // fix "process is not defined" error: 9 | // (do "npm install process" before running the build) 10 | new webpack.ProvidePlugin({ 11 | process: 'process/browser', 12 | }), 13 | new HtmlWebpackPlugin({ 14 | filename: 'index.html', 15 | template: 'index.html', 16 | }), 17 | ]; 18 | 19 | const common = { 20 | entry: './src/mention.ts', 21 | module: { 22 | rules: [ 23 | { 24 | test: /\.tsx?$/, 25 | use: 'ts-loader', 26 | exclude: /node_modules/, 27 | }, 28 | { 29 | test: /\.css$/, 30 | // include: path.join(__dirname, 'src/static/css'), 31 | use: [ 32 | 'style-loader', 33 | { 34 | loader: 'typings-for-css-modules-loader', 35 | options: { 36 | modules: false, 37 | namedExport: false, 38 | }, 39 | }, 40 | ], 41 | }, 42 | ], 43 | }, 44 | resolve: { 45 | extensions: ['.tsx', '.ts', '.js'], 46 | alias: { 47 | commons: path.resolve(__dirname, '../commons'), 48 | }, 49 | }, 50 | plugins: pluginsArray, 51 | output: { 52 | filename: 'libs/[name].[contenthash].js', 53 | path: path.resolve(__dirname, `./dist`), 54 | globalObject: 'this', 55 | }, 56 | }; 57 | 58 | 59 | module.exports = (env) => { 60 | const pluginsArray = [ 61 | new webpack.DefinePlugin({ 62 | NODE_ENV: JSON.stringify('development'), 63 | }), 64 | ]; 65 | 66 | if (env && env.analyze) { 67 | pluginsArray.push(new BundleAnalyzerPlugin()); 68 | } 69 | 70 | return merge(common, { 71 | mode: 'development', 72 | devtool: 'inline-source-map', 73 | devServer: { 74 | host: '0.0.0.0', 75 | disableHostCheck: true, 76 | contentBase: path.join(__dirname, 'dist'), 77 | compress: true, 78 | port: 3000, 79 | }, 80 | plugins: pluginsArray, 81 | }); 82 | }; 83 | -------------------------------------------------------------------------------- /h5/webpack.simple.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const { merge } = require('webpack-merge'); 3 | const path = require('path'); 4 | const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer'); 5 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 6 | 7 | const pluginsArray = [ 8 | // fix "process is not defined" error: 9 | // (do "npm install process" before running the build) 10 | new webpack.ProvidePlugin({ 11 | process: 'process/browser', 12 | }), 13 | new HtmlWebpackPlugin({ 14 | filename: 'index.html', 15 | template: 'index.html', 16 | }), 17 | ]; 18 | 19 | const common = { 20 | entry: './src/simple.ts', 21 | module: { 22 | rules: [ 23 | { 24 | test: /\.tsx?$/, 25 | use: 'ts-loader', 26 | exclude: /node_modules/, 27 | }, 28 | { 29 | test: /\.css$/, 30 | // include: path.join(__dirname, 'src/static/css'), 31 | use: [ 32 | 'style-loader', 33 | { 34 | loader: 'typings-for-css-modules-loader', 35 | options: { 36 | modules: false, 37 | namedExport: false, 38 | }, 39 | }, 40 | ], 41 | }, 42 | ], 43 | }, 44 | resolve: { 45 | extensions: ['.tsx', '.ts', '.js'], 46 | alias: { 47 | commons: path.resolve(__dirname, '../commons'), 48 | }, 49 | }, 50 | plugins: pluginsArray, 51 | output: { 52 | filename: 'libs/[name].[contenthash].js', 53 | path: path.resolve(__dirname, `./dist`), 54 | globalObject: 'this', 55 | }, 56 | }; 57 | 58 | 59 | module.exports = (env) => { 60 | const pluginsArray = [ 61 | new webpack.DefinePlugin({ 62 | NODE_ENV: JSON.stringify('development'), 63 | }), 64 | ]; 65 | 66 | if (env && env.analyze) { 67 | pluginsArray.push(new BundleAnalyzerPlugin()); 68 | } 69 | 70 | return merge(common, { 71 | mode: 'development', 72 | devtool: 'inline-source-map', 73 | devServer: { 74 | host: '0.0.0.0', 75 | disableHostCheck: true, 76 | contentBase: path.join(__dirname, 'dist'), 77 | compress: true, 78 | port: 3000, 79 | }, 80 | plugins: pluginsArray, 81 | }); 82 | }; 83 | -------------------------------------------------------------------------------- /local/README.md: -------------------------------------------------------------------------------- 1 | # wiz-editor 本地模式 2 | 3 | 无需服务器,完全作为一个纯粹的富文本编辑器使用。 4 | 5 | ## 运行 6 | 7 | ```bash 8 | npm start 9 | ``` 10 | 11 | 浏览器里面打开: http://localhost:3000 -------------------------------------------------------------------------------- /local/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | wiz-editor demo 7 | 8 | 9 | 10 | 170 | 171 | 172 |
173 |
174 |
175 |
176 |
177 |
178 |
179 |
180 |
181 | GitHub 182 |
183 |
184 |
185 |
186 |
187 |
188 |
189 | 190 | 192 | 193 | -------------------------------------------------------------------------------- /local/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wizeditordemo", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "dev": "webpack-dev-server --config webpack.dev.js", 8 | "start": "webpack-dev-server --config webpack.dev.js" 9 | }, 10 | "devDependencies": { 11 | "@types/file-saver": "^2.0.1", 12 | "html-webpack-plugin": "^4.5.0", 13 | "npm-run-all": "^4.1.5", 14 | "ts-loader": "^8.0.9", 15 | "typescript": "^4.0.5" 16 | }, 17 | "author": "", 18 | "license": "ISC", 19 | "dependencies": { 20 | "file-saver": "^2.0.5", 21 | "webpack": "^5.24.1", 22 | "webpack-cli": "3.3.12", 23 | "webpack-dev-server": "^3.11.2", 24 | "webpack-merge": "^5.7.3", 25 | "wiz-editor": "^v0.0.428" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /local/src/local.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-alert */ 2 | // eslint-disable-next-line import/no-unresolved 3 | import { saveAs } from 'file-saver'; 4 | import { 5 | Editor, 6 | createEditorPromise, 7 | assert, 8 | BlockElement, 9 | blockUtils, 10 | containerUtils, 11 | CommandItemData, 12 | MenuItem, 13 | domUtils, 14 | getEditor, 15 | AuthMessage, 16 | OnProgress, 17 | EditorOptions, 18 | SelectionDetail, 19 | EditorDoc, 20 | } from 'wiz-editor/client'; 21 | 22 | const AppId = ''; 23 | 24 | const user1 = { 25 | avatarUrl: 'https://www.wiz.cn/wp-content/new-uploads/2285af20-4006-11eb-8f21-01eb48012b63.jpeg', 26 | userId: 'test', 27 | displayName: 'test', 28 | }; 29 | 30 | // --------------------------- mention data ---------------- 31 | 32 | function createLoadDataMenuItem(block: BlockElement) { 33 | const menuItem = MenuItem.createElement(document.documentElement, { 34 | id: '', 35 | text: 'Load data...', 36 | }); 37 | const input = document.createElement('input'); 38 | input.type = 'file'; 39 | input.accept = 'application/json'; 40 | domUtils.addClass(input, 'menu-item-input'); 41 | // 42 | menuItem.appendChild(input); 43 | // 44 | input.onchange = (event: Event) => { 45 | // eslint-disable-next-line @typescript-eslint/no-shadow 46 | assert(block); 47 | const editor = getEditor(block); 48 | const container = containerUtils.getParentContainer(block); 49 | const index = containerUtils.getBlockIndex(block); 50 | if (!blockUtils.isEmptyTextBlock(block)) { 51 | // eslint-disable-next-line no-param-reassign 52 | block = editor.insertTextBlock(container, index + 1, ''); 53 | } 54 | // 55 | assert(event.target); 56 | if (input.files && input.files.length > 0) { 57 | const file = input.files[0]; 58 | const reader = new FileReader(); 59 | reader.readAsText(file); 60 | reader.onload = () => { 61 | const data = JSON.parse(reader.result as string); 62 | // eslint-disable-next-line no-use-before-define 63 | loadDocument(document.getElementById('editor') as HTMLElement, '', data); 64 | }; 65 | input.files = null; 66 | input.value = ''; 67 | } 68 | }; 69 | return menuItem; 70 | } 71 | 72 | function handleGetBlockCommand(editor: Editor, block: BlockElement, detail: SelectionDetail, type: 'fixed' | 'hover' | 'menu'): CommandItemData[] { 73 | if (type === 'menu') { 74 | // 75 | const loadDataMenuItem = createLoadDataMenuItem(block); 76 | // 77 | return [{ 78 | id: '', 79 | text: 'Load data', 80 | order: 200, 81 | element: loadDataMenuItem, 82 | onClick: () => {}, 83 | }, { 84 | id: '', 85 | text: 'Save data', 86 | order: 200, 87 | onClick: () => { 88 | const editor = getEditor(block); 89 | const data = JSON.stringify(editor.data()); 90 | const titleBlock = containerUtils.getBlockByIndex(editor.rootContainer(), 0); 91 | const title = blockUtils.getBlockPlainText(titleBlock); 92 | const blob = new Blob([data], { type: 'text/plain;charset=utf-8' }); 93 | saveAs(blob, `${title}.json`); 94 | }, 95 | }]; 96 | } 97 | return []; 98 | } 99 | 100 | function replaceUrl(docId: string) { 101 | // eslint-disable-next-line @typescript-eslint/no-shadow 102 | const now = window.location.href; 103 | if (now.endsWith(docId)) return; 104 | // 105 | const newUrl = `${window.location.origin}${window.location.pathname}?id=${docId}`; 106 | window.history.pushState({}, '', newUrl); 107 | // 108 | localStorage.setItem('lastDocId', docId); 109 | } 110 | 111 | function handleUploadResource(editor: Editor, file: File, onProgress: OnProgress): Promise { 112 | onProgress!; 113 | return domUtils.fileToDataUrl(file); 114 | } 115 | // 116 | 117 | async function loadDocument(element: HTMLElement, docId: string, initLocalData?: EditorDoc): Promise { 118 | // 119 | const auth: AuthMessage = { 120 | appId: AppId, 121 | ...user1, 122 | permission: 'w', 123 | docId, 124 | token: '', 125 | }; 126 | 127 | // 128 | const options: EditorOptions = { 129 | local: true, 130 | initLocalData, 131 | serverUrl: '', 132 | placeholder: 'Please enter document title', 133 | // markdownOnly: true, 134 | lineNumber: true, 135 | titleInEditor: true, 136 | hideComments: true, 137 | callbacks: { 138 | onGetBlockCommand: handleGetBlockCommand, 139 | onUploadResource: handleUploadResource, 140 | }, 141 | }; 142 | const editor = await createEditorPromise(element, options, auth); 143 | assert(editor); 144 | return editor; 145 | } 146 | 147 | loadDocument(document.getElementById('editor') as HTMLElement, '').then((editor) => { 148 | replaceUrl(editor.auth.docId); 149 | }); 150 | -------------------------------------------------------------------------------- /local/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | 5 | /* Basic Options */ 6 | // "incremental": true, /* Enable incremental compilation */ 7 | "target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */ 8 | "module": "es6", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ 9 | "lib": [ 10 | "es2017", 11 | "dom" 12 | ], /* Specify library files to be included in the compilation. */ 13 | // "allowJs": true, /* Allow javascript files to be compiled. */ 14 | // "checkJs": true, /* Report errors in .js files. */ 15 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 16 | // "declaration": true, /* Generates corresponding '.d.ts' file. */ 17 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 18 | "sourceMap": true, /* Generates corresponding '.map' file. */ 19 | // "outFile": "./", /* Concatenate and emit output to single file. */ 20 | "outDir": "./dist", /* Redirect output structure to the directory. */ 21 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 22 | // "composite": true, /* Enable project compilation */ 23 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 24 | // "removeComments": true, /* Do not emit comments to output. */ 25 | // "noEmit": true, /* Do not emit outputs. */ 26 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 27 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 28 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 29 | 30 | /* Strict Type-Checking Options */ 31 | "strict": true, /* Enable all strict type-checking options. */ 32 | "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 33 | // "strictNullChecks": true, /* Enable strict null checks. */ 34 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 35 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 36 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 37 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 38 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 39 | 40 | /* Additional Checks */ 41 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 42 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 43 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 44 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 45 | 46 | /* Module Resolution Options */ 47 | "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 48 | "baseUrl": "./src", /* Base directory to resolve non-absolute module names. */ 49 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 50 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 51 | "typeRoots": [ 52 | "node_modules/@types" 53 | ], /* List of folders to include type definitions from. */ 54 | // "types": [], /* Type declaration files to be included in compilation. */ 55 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 56 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 57 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 58 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 59 | 60 | /* Source Map Options */ 61 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 62 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 63 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 64 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 65 | 66 | /* Experimental Options */ 67 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 68 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 69 | 70 | /* Advanced Options */ 71 | "skipLibCheck": true, /* Skip type checking of declaration files. */ 72 | "forceConsistentCasingInFileNames": true, /* Disallow inconsistently-cased references to the same file. */ 73 | "resolveJsonModule": true 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /local/webpack.dev.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const { merge } = require('webpack-merge'); 3 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 4 | 5 | const common = { 6 | entry: './src/local.ts', 7 | module: { 8 | rules: [ 9 | { 10 | test: /\.tsx?$/, 11 | use: 'ts-loader', 12 | exclude: /node_modules/, 13 | }, 14 | { 15 | test: /\.css$/, 16 | use: [ 17 | 'style-loader', 18 | { 19 | loader: 'typings-for-css-modules-loader', 20 | options: { 21 | modules: false, 22 | namedExport: false, 23 | }, 24 | }, 25 | ], 26 | }, 27 | ], 28 | }, 29 | resolve: { 30 | extensions: ['.tsx', '.ts', '.js'], 31 | }, 32 | plugins: [ 33 | // fix "process is not defined" error: 34 | // (do "npm install process" before running the build) 35 | new webpack.ProvidePlugin({ 36 | process: 'process/browser', 37 | }), 38 | new HtmlWebpackPlugin({ 39 | filename: 'index.html', 40 | template: 'index.html', 41 | }), 42 | new webpack.DefinePlugin({ 43 | NODE_ENV: JSON.stringify('development'), 44 | }), 45 | ], 46 | }; 47 | 48 | 49 | module.exports = () => { 50 | return merge(common, { 51 | mode: 'development', 52 | devtool: 'inline-source-map', 53 | devServer: { 54 | port: 3000, 55 | }, 56 | }); 57 | }; 58 | -------------------------------------------------------------------------------- /react/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /react/README.md: -------------------------------------------------------------------------------- 1 | ```bash 2 | yarn 3 | yarn start 4 | ``` 5 | 6 | localhost:9000 -------------------------------------------------------------------------------- /react/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-demo", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^5.11.6", 7 | "@testing-library/react": "^11.2.2", 8 | "@testing-library/user-event": "^12.5.0", 9 | "jose": "^3.4.0", 10 | "react": "^17.0.1", 11 | "react-dom": "^17.0.1", 12 | "react-scripts": "4.0.1", 13 | "web-vitals": "^0.2.4", 14 | "wiz-editor-react": "^v0.0.428" 15 | }, 16 | "scripts": { 17 | "build": "react-scripts build", 18 | "test": "react-scripts test", 19 | "eject": "react-scripts eject", 20 | "start": "npm-run-all --parallel server watch", 21 | "server": "node ./server --enable-fake-token-api", 22 | "watch": "cra-build-watch" 23 | }, 24 | "eslintConfig": { 25 | "extends": [ 26 | "react-app", 27 | "react-app/jest" 28 | ] 29 | }, 30 | "browserslist": { 31 | "production": [ 32 | ">0.2%", 33 | "not dead", 34 | "not op_mini all" 35 | ], 36 | "development": [ 37 | "last 1 chrome version", 38 | "last 1 firefox version", 39 | "last 1 safari version" 40 | ] 41 | }, 42 | "devDependencies": { 43 | "cra-build-watch": "^3.4.0", 44 | "npm-run-all": "^4.1.5" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /react/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WizTeam/wiz-editor/bbe0787308acf8e667404dca322492b0ef941f69/react/public/favicon.ico -------------------------------------------------------------------------------- /react/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 13 | 14 | 18 | 19 | 28 | React App 29 | 30 | 31 | 32 |
33 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /react/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WizTeam/wiz-editor/bbe0787308acf8e667404dca322492b0ef941f69/react/public/logo192.png -------------------------------------------------------------------------------- /react/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WizTeam/wiz-editor/bbe0787308acf8e667404dca322492b0ef941f69/react/public/logo512.png -------------------------------------------------------------------------------- /react/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /react/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /react/server.js: -------------------------------------------------------------------------------- 1 | const { startServer } = require('wiz-editor/server'); 2 | const path = require('path'); 3 | 4 | console.log(startServer); 5 | 6 | // 参考node_modules/wiz-editor/config/server.json 文件 7 | const options = { 8 | enableFakeTokenApi: true, 9 | serveStatic: true, 10 | staticDir: path.resolve('./build'), 11 | }; 12 | 13 | console.log(options); 14 | 15 | startServer(options); 16 | -------------------------------------------------------------------------------- /react/src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | height: 40vmin; 7 | pointer-events: none; 8 | } 9 | 10 | @media (prefers-reduced-motion: no-preference) { 11 | .App-logo { 12 | animation: App-logo-spin infinite 20s linear; 13 | } 14 | } 15 | 16 | .App-header { 17 | background-color: #282c34; 18 | min-height: 100vh; 19 | display: flex; 20 | flex-direction: column; 21 | align-items: center; 22 | justify-content: center; 23 | font-size: calc(10px + 2vmin); 24 | color: white; 25 | } 26 | 27 | .App-link { 28 | color: #61dafb; 29 | } 30 | 31 | @keyframes App-logo-spin { 32 | from { 33 | transform: rotate(0deg); 34 | } 35 | to { 36 | transform: rotate(360deg); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /react/src/App.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react' 2 | import { WizEditor, domUtils } from 'wiz-editor-react' 3 | 4 | const appId = ''; 5 | 6 | function handleUploadResource(editor, file) { 7 | return domUtils.fileToDataUrl(file); 8 | } 9 | 10 | const App = () => { 11 | const [docId, setDocId] = React.useState('doc-json-1'); 12 | const [docData, setDocData] = React.useState(null); 13 | const editorRef = React.useRef(null); 14 | // 15 | useEffect(() => { 16 | const initData = localStorage.getItem(docId); 17 | setDocData(initData ? JSON.parse(initData) : null); 18 | }, [docId]); 19 | 20 | const options = { 21 | local: true, 22 | initLocalData: docData ? docData : undefined, 23 | titleInEditor: true, 24 | allowDarkMode: false, 25 | serverUrl: '', 26 | callbacks: { 27 | onUploadResource: handleUploadResource, 28 | } 29 | }; 30 | 31 | function handleCreated(editor) { 32 | editorRef.current = editor; 33 | } 34 | 35 | function loadDocument(id) { 36 | if (editorRef.current) { 37 | const id = editorRef.current.docId(); 38 | const data = editorRef.current.data(); 39 | localStorage.setItem(id, JSON.stringify(data)); 40 | } 41 | setDocId(id); 42 | const initData = localStorage.getItem(id); 43 | setDocData(initData ? JSON.parse(initData) : null); 44 | } 45 | 46 | return
47 |
51 | 58 | 65 |
66 | 83 |
84 | } 85 | 86 | export default App 87 | -------------------------------------------------------------------------------- /react/src/App.test.js: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react'; 2 | import App from './App'; 3 | 4 | test('renders learn react link', () => { 5 | render(); 6 | const linkElement = screen.getByText(/learn react/i); 7 | expect(linkElement).toBeInTheDocument(); 8 | }); 9 | -------------------------------------------------------------------------------- /react/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | 15 | #status { 16 | min-height: 24px; 17 | display: none; 18 | } 19 | 20 | .space { 21 | flex: 1; 22 | } 23 | 24 | 25 | #header { 26 | border-bottom: 1px solid #eee; 27 | background: #fff; 28 | height: 40px; 29 | } 30 | 31 | #toolbar { 32 | justify-content: center; 33 | } 34 | 35 | .tools { 36 | display: flex; 37 | align-items: center; 38 | padding: 8px 16px; 39 | } 40 | 41 | .tools * { 42 | box-sizing: border-box; 43 | } 44 | 45 | .tools .toolbar-button { 46 | padding: 4px; 47 | cursor: pointer; 48 | border-radius: 4px; 49 | color: #333; 50 | background-color: rgba(255, 255, 255, 0.56); 51 | margin: 0 4px; 52 | user-select: none; 53 | } 54 | 55 | .tools .toolbar-button.icon-button { 56 | width: 32px; 57 | height: 32px; 58 | } 59 | 60 | .tools .toolbar-button:hover { 61 | background-color: rgba(0, 0, 0, 0.04); 62 | } 63 | 64 | .tools .toolbar-button svg { 65 | fill: currentColor; 66 | } 67 | 68 | .tools .split-line { 69 | width: 1px; 70 | height: 32px; 71 | background-color: #eee; 72 | margin: 0 8px; 73 | } 74 | 75 | #addPage { 76 | color: #448aff; 77 | } 78 | 79 | #curUserNames { 80 | font-weight: bold; 81 | } 82 | #otherUserNames { 83 | color: #afafaf; 84 | font-size: 14px; 85 | margin-left: 24px; 86 | } -------------------------------------------------------------------------------- /react/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App'; 5 | import reportWebVitals from './reportWebVitals'; 6 | 7 | ReactDOM.render( 8 | 9 | 10 | , 11 | document.getElementById('root') 12 | ); 13 | 14 | // If you want to start measuring performance in your app, pass a function 15 | // to log results (for example: reportWebVitals(console.log)) 16 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 17 | reportWebVitals(); 18 | -------------------------------------------------------------------------------- /react/src/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /react/src/reportWebVitals.js: -------------------------------------------------------------------------------- 1 | const reportWebVitals = onPerfEntry => { 2 | if (onPerfEntry && onPerfEntry instanceof Function) { 3 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 4 | getCLS(onPerfEntry); 5 | getFID(onPerfEntry); 6 | getFCP(onPerfEntry); 7 | getLCP(onPerfEntry); 8 | getTTFB(onPerfEntry); 9 | }); 10 | } 11 | }; 12 | 13 | export default reportWebVitals; 14 | -------------------------------------------------------------------------------- /react/src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /wiki-react/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /wiki-react/README.md: -------------------------------------------------------------------------------- 1 | # Wiki React 2 | 3 | 使用编辑器的react库: wiz-editor-react 4 | 5 | ## 运行 6 | 7 | ```bash 8 | npm start 9 | ``` 10 | 11 | 浏览器里面打开: http://localhost:9000 -------------------------------------------------------------------------------- /wiki-react/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wiki-page-demo", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@material-ui/core": "^4.11.3", 7 | "@testing-library/jest-dom": "^5.11.4", 8 | "@testing-library/react": "^11.1.0", 9 | "@testing-library/user-event": "^12.1.10", 10 | "lodash.isequal": "^4.5.0", 11 | "react": "^17.0.2", 12 | "react-dom": "^17.0.2", 13 | "react-scripts": "4.0.3", 14 | "web-vitals": "^1.0.1", 15 | "wiz-editor-react": "^v0.0.428" 16 | }, 17 | "scripts": { 18 | "build": "react-scripts build", 19 | "test": "react-scripts test", 20 | "eject": "react-scripts eject", 21 | "start": "npm-run-all --parallel server watch", 22 | "server": "node ./server --enable-fake-token-api", 23 | "watch": "cra-build-watch --output-filename=js/bundle-[hash].js --disable-chunks" 24 | }, 25 | "eslintConfig": { 26 | "extends": [ 27 | "react-app", 28 | "react-app/jest" 29 | ] 30 | }, 31 | "browserslist": { 32 | "production": [ 33 | ">0.2%", 34 | "not dead", 35 | "not op_mini all" 36 | ], 37 | "development": [ 38 | "last 1 chrome version", 39 | "last 1 firefox version", 40 | "last 1 safari version" 41 | ] 42 | }, 43 | "devDependencies": { 44 | "cra-build-watch": "^3.4.0", 45 | "npm-run-all": "^4.1.5" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /wiki-react/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WizTeam/wiz-editor/bbe0787308acf8e667404dca322492b0ef941f69/wiki-react/public/favicon.ico -------------------------------------------------------------------------------- /wiki-react/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 13 | 14 | 23 | Wiki react 24 | 25 | 26 | 27 |
28 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /wiki-react/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WizTeam/wiz-editor/bbe0787308acf8e667404dca322492b0ef941f69/wiki-react/public/logo192.png -------------------------------------------------------------------------------- /wiki-react/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WizTeam/wiz-editor/bbe0787308acf8e667404dca322492b0ef941f69/wiki-react/public/logo512.png -------------------------------------------------------------------------------- /wiki-react/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /wiki-react/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /wiki-react/server.js: -------------------------------------------------------------------------------- 1 | const { startServer } = require('wiz-editor/server'); 2 | const path = require('path'); 3 | 4 | console.log(startServer); 5 | 6 | // 参考node_modules/wiz-editor/config/server.json 文件 7 | const options = { 8 | enableFakeTokenApi: true, 9 | serveStatic: true, 10 | staticDir: path.resolve('./build'), 11 | }; 12 | 13 | console.log(options); 14 | 15 | startServer(options); 16 | -------------------------------------------------------------------------------- /wiki-react/src/App.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --style-color-0: #d83931; 3 | --style-color-1: #de7802; 4 | --style-color-2: #dc9b04; 5 | --style-color-3: #2ea121; 6 | --style-color-4: #245bdb; 7 | --style-color-5: #6425d0; 8 | --style-color-6: #8f959e; 9 | 10 | --style-bg-color-0: rgb(251, 191, 188); 11 | --style-bg-color-1: rgba(254, 212, 164, 0.8); 12 | --style-bg-color-2: rgba(255, 246, 122, 0.8); 13 | --style-bg-color-3: rgba(183, 237, 177, 0.8); 14 | --style-bg-color-4: rgba(186, 206, 253, 0.7); 15 | --style-bg-color-5: rgba(205, 178, 250, 0.7); 16 | --style-bg-color-6: rgba(222, 224, 227, 0.8); 17 | --style-bg-color-7: rgb(247, 105, 100); 18 | --style-bg-color-8: rgb(255, 165, 61); 19 | --style-bg-color-9: rgb(255, 233, 40); 20 | --style-bg-color-10: rgb(98, 210, 86); 21 | --style-bg-color-11: rgba(78, 131, 253, 0.55); 22 | --style-bg-color-12: rgba(147, 90, 246, 0.55); 23 | --style-bg-color-13: rgb(187, 191, 196); 24 | } 25 | 26 | .editor-main .editor-container.root-container { 27 | max-width: 1024px; 28 | } 29 | 30 | body { 31 | display: flex; 32 | flex-direction: column; 33 | width: 100%; 34 | } 35 | 36 | .scroll-container { 37 | width: 100%; 38 | height: calc(100vh - 128px); 39 | overflow: auto; 40 | margin-top: 128px; 41 | } 42 | 43 | #root header { 44 | width: 100%; 45 | height: 64px; 46 | background-color: #2F323E; 47 | display: flex; 48 | align-items: center; 49 | position: fixed; 50 | left: 0; 51 | top: 0; 52 | z-index: 99; 53 | } 54 | 55 | #root header .logo { 56 | font-size: 20px; 57 | color: #fbfbfb; 58 | margin: 0 33px 0 26px; 59 | } 60 | 61 | #root header .btn-create-doc { 62 | background-color: #5177FF; 63 | border-radius: 4px; 64 | font-size: 16px; 65 | color: #ffffff; 66 | cursor: pointer; 67 | padding: 5px 10px; 68 | font-weight: 600; 69 | } 70 | 71 | #root header .btn-create-doc:hover { 72 | background-color: #98adf7; 73 | } 74 | 75 | .btn-create-doc span, .btn-share-doc span { 76 | margin-left: 5px; 77 | } 78 | 79 | .remote-user-container { 80 | margin-left: auto; 81 | display: flex; 82 | } 83 | 84 | .remote-user-container .avatar { 85 | margin: 0 5px; 86 | } 87 | 88 | .remote-user-container .avatar img { 89 | width: 28px; 90 | } 91 | 92 | .btn-share-doc { 93 | color: #ffffff; 94 | font-size: 16px; 95 | font-weight: 600; 96 | cursor: pointer; 97 | margin: 0 120px 0 80px; 98 | display: flex; 99 | align-items: center; 100 | } 101 | 102 | .tool-container { 103 | width: 100%; 104 | height: 64px; 105 | background-color: #ffffff; 106 | box-shadow: 0px 4px 5px rgba(0, 0, 0, 0.05); 107 | display: flex; 108 | align-items: center; 109 | justify-content: center; 110 | position: fixed; 111 | left: 0; 112 | top: 64px; 113 | z-index: 99; 114 | } 115 | 116 | .tool-container .split-line { 117 | width: 1px; 118 | height: 24px; 119 | background-color: #e5e5e5; 120 | margin: 0 20px; 121 | } 122 | 123 | .tool-container .split-line:last-child { 124 | display: none; 125 | } 126 | 127 | nav .toolbar-icon { 128 | fill: #505F79; 129 | cursor: pointer; 130 | margin: 0 5px; 131 | border-radius: 4px; 132 | width: 24px; 133 | height: 24px; 134 | border: 0; 135 | padding: 0; 136 | background-color: transparent; 137 | } 138 | 139 | nav .toolbar-icon:focus { 140 | outline: none; 141 | } 142 | 143 | nav .toolbar-icon:hover:not(.disabled) { 144 | background-color: #ECEFF4; 145 | } 146 | 147 | nav .toolbar-icon.disabled { 148 | fill: #C6CFDE; 149 | cursor: no-drop; 150 | } 151 | 152 | nav .toolbar-icon.active { 153 | fill: #5177FF; 154 | } -------------------------------------------------------------------------------- /wiki-react/src/App.test.js: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react'; 2 | import App from './App'; 3 | 4 | test('renders learn react link', () => { 5 | render(); 6 | const linkElement = screen.getByText(/learn react/i); 7 | expect(linkElement).toBeInTheDocument(); 8 | }); 9 | -------------------------------------------------------------------------------- /wiki-react/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /wiki-react/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App'; 5 | import reportWebVitals from './reportWebVitals'; 6 | 7 | ReactDOM.render( 8 | 9 | 10 | , 11 | document.getElementById('root') 12 | ); 13 | 14 | // If you want to start measuring performance in your app, pass a function 15 | // to log results (for example: reportWebVitals(console.log)) 16 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 17 | reportWebVitals(); 18 | -------------------------------------------------------------------------------- /wiki-react/src/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /wiki-react/src/reportWebVitals.js: -------------------------------------------------------------------------------- 1 | const reportWebVitals = onPerfEntry => { 2 | if (onPerfEntry && onPerfEntry instanceof Function) { 3 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 4 | getCLS(onPerfEntry); 5 | getFID(onPerfEntry); 6 | getFCP(onPerfEntry); 7 | getLCP(onPerfEntry); 8 | getTTFB(onPerfEntry); 9 | }); 10 | } 11 | }; 12 | 13 | export default reportWebVitals; 14 | -------------------------------------------------------------------------------- /wiki-react/src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /wiki/README.md: -------------------------------------------------------------------------------- 1 | # Wiki 页面编辑器模拟页面 2 | 3 | ## 运行 4 | 5 | ```bash 6 | yarn 7 | cd client 8 | yarn 9 | cd .. 10 | cd server 11 | yarn 12 | cd .. 13 | yarn dev 14 | ``` 15 | 16 | 运行后可以在浏览器内打开: http://localhost:9000 17 | 18 | 19 | -------------------------------------------------------------------------------- /wiki/client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wiki", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "dev": "webpack --watch --config webpack.dev.js", 8 | "build": "webpack --config webpack.prod.js", 9 | "test": "echo \"Error: no test specified\" && exit 1" 10 | }, 11 | "author": "", 12 | "license": "ISC", 13 | "dependencies": { 14 | "wiz-editor": "^v0.0.428" 15 | }, 16 | "devDependencies": { 17 | "html-webpack-plugin": "^5.3.1", 18 | "ts-loader": "^8.0.18", 19 | "typescript": "^4.2.3", 20 | "webpack": "^5.28.0", 21 | "webpack-cli": "^4.5.0" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /wiki/client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | 5 | /* Basic Options */ 6 | // "incremental": true, /* Enable incremental compilation */ 7 | "target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */ 8 | "module": "es6", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ 9 | "lib": [ 10 | "es2017", 11 | "dom" 12 | ], /* Specify library files to be included in the compilation. */ 13 | // "allowJs": true, /* Allow javascript files to be compiled. */ 14 | // "checkJs": true, /* Report errors in .js files. */ 15 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 16 | "declaration": true, /* Generates corresponding '.d.ts' file. */ 17 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 18 | "sourceMap": true, /* Generates corresponding '.map' file. */ 19 | // "outFile": "./", /* Concatenate and emit output to single file. */ 20 | "outDir": "./dist", /* Redirect output structure to the directory. */ 21 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 22 | // "composite": true, /* Enable project compilation */ 23 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 24 | // "removeComments": true, /* Do not emit comments to output. */ 25 | // "noEmit": true, /* Do not emit outputs. */ 26 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 27 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 28 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 29 | 30 | /* Strict Type-Checking Options */ 31 | "strict": true, /* Enable all strict type-checking options. */ 32 | "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 33 | // "strictNullChecks": true, /* Enable strict null checks. */ 34 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 35 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 36 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 37 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 38 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 39 | 40 | /* Additional Checks */ 41 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 42 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 43 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 44 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 45 | 46 | /* Module Resolution Options */ 47 | "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 48 | "baseUrl": ".", /* Base directory to resolve non-absolute module names. */ 49 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 50 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 51 | "typeRoots": [ 52 | "node_modules/@types" 53 | ], /* List of folders to include type definitions from. */ 54 | // "types": [], /* Type declaration files to be included in compilation. */ 55 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 56 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 57 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 58 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 59 | 60 | /* Source Map Options */ 61 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 62 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 63 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 64 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 65 | 66 | /* Experimental Options */ 67 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 68 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 69 | 70 | /* Advanced Options */ 71 | "skipLibCheck": true, /* Skip type checking of declaration files. */ 72 | "forceConsistentCasingInFileNames": true, /* Disallow inconsistently-cased references to the same file. */ 73 | "paths": { 74 | "commons/*": ["../commons/*"], 75 | "src/*": ["./src/*"], 76 | }, 77 | "resolveJsonModule": true, 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /wiki/client/webpack.dev.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const path = require('path'); 3 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 4 | 5 | module.exports = { 6 | entry: './src/index.ts', 7 | mode: 'development', 8 | module: { 9 | rules: [ 10 | { 11 | test: /\.tsx?$/, 12 | use: 'ts-loader', 13 | exclude: /node_modules/, 14 | }, 15 | { 16 | test: /\.css$/, 17 | // include: path.join(__dirname, 'src/static/css'), 18 | use: [ 19 | 'style-loader', 20 | { 21 | loader: 'typings-for-css-modules-loader', 22 | options: { 23 | modules: false, 24 | namedExport: false, 25 | }, 26 | }, 27 | ], 28 | }, 29 | ], 30 | }, 31 | resolve: { 32 | extensions: ['.tsx', '.ts', '.js'], 33 | }, 34 | plugins: [ 35 | new webpack.ProvidePlugin({ 36 | process: 'process/browser', 37 | }), 38 | new HtmlWebpackPlugin({ 39 | filename: 'index.html', 40 | template: 'wiki.html', 41 | }), 42 | ], 43 | output: { 44 | filename: 'libs/[name].[contenthash].js', 45 | path: path.resolve(__dirname, `../server/dist`), 46 | globalObject: 'this', 47 | }, 48 | }; 49 | -------------------------------------------------------------------------------- /wiki/client/webpack.prod.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const path = require('path'); 3 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 4 | 5 | module.exports = { 6 | entry: './src/index.ts', 7 | mode: 'production', 8 | module: { 9 | rules: [ 10 | { 11 | test: /\.tsx?$/, 12 | use: 'ts-loader', 13 | exclude: /node_modules/, 14 | }, 15 | { 16 | test: /\.css$/, 17 | // include: path.join(__dirname, 'src/static/css'), 18 | use: [ 19 | 'style-loader', 20 | { 21 | loader: 'typings-for-css-modules-loader', 22 | options: { 23 | modules: false, 24 | namedExport: false, 25 | }, 26 | }, 27 | ], 28 | }, 29 | ], 30 | }, 31 | resolve: { 32 | extensions: ['.tsx', '.ts', '.js'], 33 | }, 34 | plugins: [ 35 | new webpack.ProvidePlugin({ 36 | process: 'process/browser', 37 | }), 38 | new HtmlWebpackPlugin({ 39 | filename: 'index.html', 40 | template: 'wiki.html', 41 | }), 42 | ], 43 | output: { 44 | filename: 'libs/[name].[contenthash].js', 45 | path: path.resolve(__dirname, `../server/dist`), 46 | globalObject: 'this', 47 | }, 48 | }; 49 | -------------------------------------------------------------------------------- /wiki/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wiki", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "dev-client": "cd client && yarn dev", 8 | "dev-server": "cd server && yarn dev", 9 | "dev": "npm-run-all --parallel dev-client dev-server", 10 | "test": "echo \"Error: no test specified\" && exit 1" 11 | }, 12 | "author": "", 13 | "license": "ISC", 14 | "dependencies": { 15 | "npm-run-all": "^4.1.5" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /wiki/server/index.js: -------------------------------------------------------------------------------- 1 | const { startServer } = require("wiz-editor/server"); 2 | const path = require("path"); 3 | 4 | // 参考node_modules/wiz-editor/config/server.json 文件 5 | const options = { 6 | port: 80, 7 | enableFakeTokenApi: true, // 仅用于demo,测试和生产环境,都不要启用这个功能,具体token生成方式,请在自己的业务中实现。 8 | serveStatic: true, // 发布静态文件 9 | staticDir: path.resolve("./dist"), //静态文件目录 10 | database: { 11 | use: "sqlite", 12 | opUse: "memory", 13 | mysql: { 14 | host: "localhost", 15 | port: 3306, 16 | user: "root", 17 | password: "root", 18 | database: "ot", 19 | connectionLimit: 100, 20 | }, 21 | }, 22 | }; 23 | 24 | console.log(options); 25 | 26 | startServer(options); 27 | -------------------------------------------------------------------------------- /wiki/server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "server", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "dev": "node index.js", 8 | "start": "node index.js", 9 | "test": "echo \"Error: no test specified\" && exit 1" 10 | }, 11 | "author": "", 12 | "license": "ISC", 13 | "dependencies": { 14 | "wiz-editor": "^v0.0.428" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /wiki/server/pm2.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | apps: [ 3 | { 4 | name: "ot_server_80", 5 | script: "index.js", 6 | args: "-p 80", 7 | cwd: "/root/wiz-editor/wiki/server", 8 | instance_var: "INSTANCE_ID", 9 | time: false, 10 | env: { 11 | FORCE_DEBUG: 1, 12 | }, 13 | }, 14 | ], 15 | }; 16 | --------------------------------------------------------------------------------