Canvas-Editor 实现类似 Word 协同编辑

前言

        对于word的协同编辑,已经构思很久了,但是没有找到合适的插件。今天推荐基于canvas/svg 的富文本编辑器  canvas-editor,能实现类似word的基础功能,如果后续有更好的,也会及时更新。

Canvas-Editor

效果图

官方文档

canvas-editor | rich text editor by canvas/svgrich text editor by canvas/svgicon-default.png?t=N7T8https://hufe.club/canvas-editor-docs/

 官方DEMO 

canvas-editoricon-default.png?t=N7T8https://hufe.club/canvas-editor/

Gitee

canvas-editor: 同步自https://github.com/Hufe921/canvas-editoricon-default.png?t=N7T8https://gitee.com/mr-jinhui/canvas-editor

 前置条件与实现思路

        虽然canvas-editor做的还不错,API都比较完善,但是对协同部分还是空缺,因此我们此次的重点是实现协同部分的代码,难免会修改源码部分。因此,我们需要阅读源码,实现 ts 代码的编写,修改其源码,实现协同。

下载源码并运行

        大家可以直接从 github下载 ,也可以从刚才给的 gitee 下。

npm i  // 下载相关依赖

npm run dev // 启动服务

npm run build // 打包项目

        启动后,能出来与demo一致的页面,即完成了这一步。

实现用户选区

        用户闪烁的光标目前还没有思路实现,后面会攻克技术难点,但是用户选取可以通过API实现:

         但是这个API会导致我的选取也会发生改变,因此,不能直接使用,需要添加新的API

        简单解释一下文件,command文件向外暴露了API, command 指向 commandAdapt 文件,Adapt 文件中,有需要的全部对象,包括 画布、选取对象等,可以直接进行底层绘制。

  public setUserRange(startIndex: number, endIndex: number, payload?: string) {if (startIndex < 0 || endIndex < 0 || endIndex < startIndex) returnconst isReadonly = this.draw.isReadonly()if (isReadonly) return// 根据 index 获取 domList 设置颜色const elementList = this.draw.getElementList()for (let i = startIndex; i <= endIndex; i++) {elementList[i].highlight = payload||'#F5EEA0'}this.draw.render({isSetCursor: false,isCompute: false})}

         这样用户选取,才不会影响我的选取,而取消选取就是设置透明色即可。

  // 用户取消选取public setUserUnRange(startIndex: number, endIndex: number) {if (startIndex < 0 || endIndex < 0 || endIndex < startIndex) returnconst isReadonly = this.draw.isReadonly()if (isReadonly) return// 根据 index 获取 domList 设置颜色const elementList = this.draw.getElementList()for (let i = startIndex; i <= endIndex; i++) {elementList[i].highlight = 'transparent'}this.draw.render({isSetCursor: false,isCompute: false})}

         用户的光标是无状态的,因此需要记录光标信息,不然我重新设置了选取,上次的选取是需要取消哦,这个后面再说。

搭建CRDT

        协同的核心就是数据一致性,因此,我们需要根据现有的数据结构实现CRDT。

新建yjs文件

// editor/core/websocket
import * as Y from 'yjs'
import { WebsocketProvider } from 'y-websocket'
import { IWebsocketProviderStatus } from '../../interface/Websocket'export class Ydoc {private ydoc: Y.Docprivate ymap: Y.Map<unknown>private ytext: Y.Textprivate provider: any | undefinedprivate connect: boolean | undefinedprivate url: stringprivate roomname: stringconstructor(url: string, roomname: string) {console.log('new Ydoc')this.url = urlthis.roomname = roomnamethis.connect = false// 创建 YDoc 文档this.ydoc = new Y.Doc()this.ymap = this.ydoc.getMap('map')this.ytext = this.ydoc.getText('text')this.ymap.observe(() => {})this.ytext.observe(() => {})// 【方案二】 websocket 方式实现协同(已自己搭建 websocket 服务)this.provider = new WebsocketProvider(this.url, this.roomname, this.ydoc)// 监听链接状态F·this.provider.on('status', (event: IWebsocketProviderStatus) => {let { status } = eventif (status === 'connected') this.connect = trueelse this.connect = false})}public disConnection() {if (!this.connect) returnthis.provider.disconnect()}
}

初始化 yjs 

        入口文件 index.ts 实现创建并传参

 // 创建 websocketif (ydocInfo) {let { url, roomname, userid, username, color } = ydocInfoif (!url || !roomname || !userid || !username)throw Error('参数错误,url、roomname、userid、username必传!')// 1. 如果存在,则创建协同ydoc = new Ydoc(url, roomname, userid, this.command, color)Reflect.set(window, 'ydoc', ydoc)console.log(`用户${username}初始化`)ydoc.userInitEditor(`用户${username}`)}

         这样,整个编辑器需要实现协同的地方,都能调用 ydoc 实现。

实现用户登录

        Yjs 的基本使用中,通过Map设置数据,observe观察器实现数据获取,协同部分不懂得可以看上一篇文章:

深度解析 Yjs 协同编辑原理【看这篇就够了】_深度 解析yjs原理-CSDN博客文章浏览阅读1k次,点赞21次,收藏16次。本文带大家分析了Yjs的API、y-websocket 的实现原理、Yjs的应用及底层协同模型,并使用Logic Flow 简单实现了其协同。大致的协同实现都有类似的思想,大家以后需要协同的场景,希望也能自行开发。_深度 解析yjs原理https://blog.csdn.net/weixin_47746452/article/details/135079472?spm=1001.2014.3001.5501

        这样,用户每次初始化 Editor的时候,都会广播其他用户:

实现用户选区

        用户每次操作鼠标抬起,都会触发setRangeStyle事件:

         因此,在这个事件中捕获用户的选区操作;

         yjs中则是正常转发,然后调用上面实现的选区API:

 public userRange({ data }: IYMapObserve) {let { startIndex, endIndex, userid, color } = datathis.command.setUserRange(startIndex, endIndex, userid, color)}

        效果如下:

 实现用户取消选区

        现在的选区还是有bug的,用户退出后,无法识别,还有就是单击时,无法优化选区。

        如上图,我点击时,理论上只占用一个格子,不应该有选区【用户光标目前还没能实现】  if (startIndex === endIndex) return 如果点击的开始与结束相同,则不进行渲染。还有用户退出时,清空用户选区:

         实现删除历史选区,并删除lastRange 记录即可。

实现文本输入与删除

       CanvasEvent监听了input 事件,实现监听用户的输入,修改参数实现在draw 中获取用户数据,文档变化时,会调用 draw 中的方法:

        因此,在这里通过yjs广播事件,修改参数后,就能拿到用户新增的数据了:

 // 内容区变化public contentChangeHandle(payload: IEditorData) {/*** 因此在这里需要重新解析用户的选区设置,不然会导致选区异常 BUG*/// 这里要解析 userRangelet { header, footer, main } = payloadmain.forEach(item => {if (item.userRange) {delete item.highlightdelete item.userRange}})this.setValue({ header, footer, main })}

        实现效果:

        删除实现:

        keydown.ts 中对每个事件做了监听,在该文件实现广播,还是拿到本地的数据,进行数据解析,重新渲染。

 

        效果如下:

 

实现样式协同

        样式的协同,就是基于API实现的,因为在main.ts中,所有的菜单栏操作,都是基于API实现,因此,我们需要在API调用处,进行统一处理即可

  // 选区样式改变public rangeStyleChange(payload: IRangeStyle) {// 样式只能针对 用户的当前选区// 直接使用 element 的事件机制let { startIndex = 0, endIndex = 0, attr, value } = payloadconst isReadonly = this.draw.isReadonly()if (isReadonly) returnif (startIndex === endIndex) return// 根据 index 获取 domList 设置颜色const elementList = this.draw.getElementList()for (let i = startIndex; i <= endIndex; i++) {let el = elementList[i]if (el) {switch (attr) {case 'color':value ? (el.color = <string | undefined>value) : delete el.colorbreakcase 'bold':value ? (el.bold = true) : delete el.boldbreakcase 'italic':value ? (el.italic = true) : delete el.italicbreakcase 'fontSize':breakcase 'underline':value ? (el.underline = true) : delete el.underlinebreakcase 'highlight':// 这里还有BUG,因为用户选区结束又被设置透明value? (el.highlight = <string | undefined>value): delete el.highlightbreakdefault:break}}}this.draw.render({isSetCursor: false,isCompute: false})}

        效果如下:

        用户协同选区与高亮冲突了,这个还得在想办法处理。

打包在项目中使用

        想要打包,需要注释 main.ts 中的window.onload 事件,将Editor 暴露到window身上

        打包后,将dist 放置到项目 public/libs.canvas-editor下【如果你打包报错,基本上是TS语法检查的问题 let const 引入没用的模块等

        这样已经实现了基本的协同编辑了,至于说 菜单栏、目录,其实也是它自己加上的,然后调用API实现:

         剩下的就是自行实现菜单栏,调用API即可。

 总结

        对这个文章简单说一下:

  1. 这个版本的代码肯定是粗糙的哈,大家稍微谅解一下,自己的TS还有点差;
  2. 功能实现上还有些缺陷,有些功能底层限制了,修改起来难度非常大,比如协同选区问题,后续会再优化;
  3. 协同的底层一定是数据一致性、广播监听、调用相应API实现相同功能;
  4. 后续可能会完善这部分代码,争取能实现基本的、稳定的协同环境,包括也会更新在 mpoe 项目中,有一个稳定版本支撑协同编辑;
  5. 文章在书写过程中,会发现BUG,然后调整代码,可能会出现页面与实际代码不匹配,大家以实际代码为主哈
  6. 也会持续关注大家的问题与需求,大家可以提一些好的建议。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/642152.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

redis-exporter grafana面板配置

一、前言 关于使用tensuns自带的grafana监控模板&#xff0c;监控redis-exporter接口会有一些数据丢失的问题&#xff0c;需要自行修改一下grafana模板的json 二、修改模板 redis grafana模板id&#xff1a;17507 主要是针对cpu使用率和内存使用率做一个说明&#xff0c;因为…

Acwing-语法基础练习

目录 1. 非常基础的C (面向程序) 框架 2. 一些基础数据类型 3.变量的输入输出 4.ACWing题库-第1题&#xff1a;AB 5.四则运算(只整理一部分较难的) 6.数据类型转换 寒假自学用,记录Acwing题目。 语言&#xff1a;C 1. 非常基础的C (面向程序) 框架 #include <iostre…

STM32CubeMX教程26 FatFs 文件系统 - W25Q128读写

文章目录 1、准备材料2、实验目标3、实验流程3.0、前提知识3.1、CubeMX相关配置3.1.0、工程基本配置3.1.1、时钟树配置3.1.2、外设参数配置3.1.3、外设中断配置 3.2、生成代码3.2.0、配置Project Manager页面3.2.1、外设初始化调用流程3.2.2、外设中断调用流程3.2.3、添加其他必…

C#winform上位机开发学习笔记7-串口助手的波特率参数设置功能添加

1.功能描述 上位机与下位机进行通讯时需要用到波特率设置功能&#xff0c;以及尝试与下位机实体进行通讯。 2.代码部分 步骤1&#xff1a;串口开启按钮事件中添加代码 serialPort1.BaudRate Convert.ToInt32(comboBox14.Text, 10);//将十进制的文本转换为32位整型赋值给串…

揭秘真相!成都力寰璨泓科技有限公司抖音小店究竟是否可靠?

在互联网电商繁荣发展的今天&#xff0c;抖音小店作为新兴的电商平台&#xff0c;吸引了众多商家和消费者的目光。在这其中&#xff0c;成都力寰璨泓科技有限公司的抖音小店尤为引人注目。那么&#xff0c;这家公司在抖音小店的运营是否可靠呢&#xff1f;本文将为你揭开真相。…

Raspbian安装云台

Raspbian安装云台 1. 源由2. 选型3. 组装4. 调试4.1 python3-print问题4.2 python函数入参类型错误4.3 缺少mjpg-streamer可执行文件4.4 缺失编译头文件和库4.5 python库缺失4.6 图像无法显示&#xff0c;但libcamera-jpeg测试正常4.7 异常IOCTL报错4.8 Git问题 5. 效果5.1 WEB…

制作高端的电子杂志神器推荐

根据市场调查数据显示&#xff0c;越来越多的消费者开始青睐电子杂志这种阅读方式。相比传统纸质杂志&#xff0c;电子杂志具有更高的阅读体验、更便捷的分享和传播方式以及更环保的阅读方式。此外&#xff0c;越来越多的企业也开始重视电子杂志的宣传作用&#xff0c;将其作为…

安裝火狐和穀歌流覽器插件FoxyProxy管理海外動態IP代理

代理生態系統擁有大量有用的實用程式&#xff0c;使海外代理IP代理設置的使用變得簡單起來。其中一種類型叫做代理管理工具&#xff0c;像FoxyProxy就是該工具集比較受歡迎的。 本文將全面解析FoxyProxy擴展的功能和特性、Foxyproxy怎麼下載、以及如何在穀歌流覽器和火狐流覽器…

14、Kafka ------ kafka 核心API 之 流API(就是把一个主题的消息 导流 到另一个主题里面去)

目录 kafka 核心API 之 流APIKafka流API的作用&#xff1a;流API的核心API&#xff1a;使用流API编程的大致步骤如下&#xff1a;代码演示 流API 用法MessageStream 流API 代码演示消息从 test1主题 导流到 test2主题演示使用匿名内部类对消息进行处理Topology 拓扑结构 讲解 代…

Linux之快速入门(CentOS 7)

文章目录 一、Linux目录结构二、常用命令2.1 切换用户2.2查看ip地址2.3 cd2.4 目录查看2.5 查看文件内容2.6 创建目录及文件2.72.82.93.0 一、Linux目录结构 目录作用/bin是 Binaries (二进制文件) 的缩写,这个目录存放着最经常使用的命令/dev是 Device(设备) 的缩写,该目录下存…

【办公类-22-01】20240123 UIBOT逐一提取CSDN质量分

背景需求&#xff1a; 最近每天传2份Python&#xff0c;发现平均分从73.5降到了72.7。网上搜索一下原因&#xff0c;发现每篇CSDN都有一个评分&#xff08;以下是查分网站&#xff09; https://www.csdn.net/qchttps://www.csdn.net/qc 但是一篇一篇查询&#xff0c;显然太繁…

Java 数据结构篇-实现红黑树的核心方法

&#x1f525;博客主页&#xff1a; 【小扳_-CSDN博客】 ❤感谢大家点赞&#x1f44d;收藏⭐评论✍ 文章目录 1.0 红黑树的说明 2.0 红黑树的特性 3.0 红黑树的成员变量及其构造方法 4.0 实现红黑树的核心方法 4.1 红黑树内部类的核心方法 &#xff08;1&#xff09;判断当前…

软件工程实验报告(完整)

博主介绍&#xff1a;✌全网粉丝喜爱、前后端领域优质创作者、本质互联网精神、坚持优质作品共享、掘金/腾讯云/阿里云等平台优质作者、擅长前后端项目开发和毕业项目实战✌有需要可以联系作者我哦&#xff01; &#x1f345;附上相关C语言版源码讲解&#x1f345; &#x1f44…

openGauss学习笔记-205 openGauss 数据库运维-常见故障定位案例-业务运行时整数转换错

文章目录 openGauss学习笔记-205 openGauss 数据库运维-常见故障定位案例-业务运行时整数转换错205.1 业务运行时整数转换错205.1.1 问题现象205.1.2 原因分析205.1.3 处理办法 openGauss学习笔记-205 openGauss 数据库运维-常见故障定位案例-业务运行时整数转换错 205.1 业务…

Java21 + SpringBoot3集成easy-captcha实现验证码显示和登录校验

文章目录 前言相关技术简介easy-captcha 实现步骤引入maven依赖定义实体类定义登录服务类定义登录控制器前端登录页面实现测试和验证 总结附录使用Session缓存验证码前端登录页面实现代码 前言 近日心血来潮想做一个开源项目&#xff0c;目标是做一款可以适配多端、功能完备的…

虚拟机下载docker

一&#xff0c;Docker简介 百科说&#xff1a;Docker 是一个开源的应用容器引擎&#xff0c;让开发者可以打包他们的应用以及依赖包到一个可移植的容器中&#xff0c;然后发布到任何流行的Linux机器上&#xff0c;也可以实现虚拟化&#xff0c;容器是完全使用沙箱机制&#xff…

CentOS 7安装全解析

目录 一.centos安装1.1 下载镜像文件1.2 安装 二.远程连接&#xff0c;换源2.1 下载并且使用MobaXterm2.2 远程连接2.3 换源 一.centos安装 1.1 下载镜像文件 https://mirrors.aliyun.com/centos/7/isos/x86_64/ 下载即可 1.2 安装 二.远程连接&#xff0c;换源 2.1 下载并…

租幻兽帕鲁Palworld服务器多少钱?

使用腾讯云服务器搭建搭建幻兽帕鲁Palworld如何选择服务器配置&#xff1f;腾讯云百科txybk.com建议幻兽帕鲁选择腾讯云轻量应用服务器4核16G14M带宽&#xff0c;Ubuntu/Debian系统。如何收费&#xff1f; 腾讯云幻兽帕鲁服务器活动 https://curl.qcloud.com/oRMoSucP 轻量应用…

C#,入门教程(28)——文件夹(目录)、文件读(Read)与写(Write)的基础知识

上一篇&#xff1a; C#&#xff0c;入门教程(27)——应用程序&#xff08;Application&#xff09;的基础知识https://blog.csdn.net/beijinghorn/article/details/125094837 C#知识比你的预期简单的多&#xff0c;但也远远超乎你的想象&#xff01; 与文件相关的知识&#xf…

记一次低级且重大的Presto运维事故

本文纯属虚构&#xff0c;旨在提醒各位别犯类似低级错误。 如有雷同&#xff0c;说的就是你&#xff01; 文章目录 前言事件回顾后续总结 前言 首先&#xff0c;要重视运维工作和离职人员的交接工作&#xff0c;这个不必多说。一将无能&#xff0c;累死三军&#xff01; 接下来…