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.rhkb.cn/news/244442.html

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

相关文章

终极解决Flutter项目运行ios项目报错Without CocoaPods, plugins will not work on iOS or macOS.

前言 最近在开发Flutter项目&#xff0c;运行ios环境的时候报错没有CocoaPods&#xff0c;安卓环境可以正常运行&#xff0c;当时一脸懵逼&#xff0c;网上搜索了一下&#xff0c;有给我讲原理的&#xff0c;还有让我安装这插件那插件的&#xff0c;最终把电脑搞得卡死&#x…

【spring】代码生成器

&#x1f4dd;个人主页&#xff1a;五敷有你 &#x1f525;系列专栏&#xff1a;spring ⛺️稳中求进&#xff0c;晒太阳 代码生成器&#xff08;本质IO流&#xff09; 在mybatis的逆向工程生成model和mapper接口和xml文件后&#xff0c;还需要反复的写Service的接口和…

UE5 C++学习笔记 FString FName FText相互转换

1.FString 是UE里的String。最接近std::string, 唯一可以修改的字符串类型。性能更低 TEXT(string) TEXT宏&#xff0c;作用是将字符串转换成Unicode&#xff0c;切记UE中使用字符串输出要使用该宏 2. FName 是UE里特有的类型。它更注重于表示名称不区分大小写&#xff0c;不…

元数据管理在数据仓库中的实践应用

一、什么是数据仓库的元数据管理? 1、什么是元数据? 元数据(Metadata),又称中介数据、中继数据,为描述数据的数据(data about data)。 抽象的描述:一组用于描述数据的数据组,该数据组的一切信息都描述了该数据的某方面特征,则该数据组即可被称为元数据。 举几个…

MyBatis的逆向工程的创建,generator插件的使用和可能出现的一些问题,生成的实体类多出.java 1 .java 2这种拓展文件的处理方案

目录 创建逆向工程的步骤 ①添加依赖和插件 ②创建MyBatis的核心配置文件 ③创建逆向工程的配置文件 ④执行MBG插件的generate目标 数据库版本8有可能出现的问题&#xff1a; 1、生成的实体类多了.java 1 .java 2的拓展文件... 2、生成的属性与表中字段不匹配&#xff…

kafka(一)快速入门

一、kafka&#xff08;一&#xff09;是什么&#xff1f; kafka是一个分布式、支持分区、多副本&#xff0c;基于zookeeper协调的分布式消息系统&#xff1b; 二、应用场景 日志收集&#xff1a;一个公司可以用Kafka收集各种服务的log&#xff0c;通过kafka推送到各种存储系统…

在markdown中添加视频的两种方法

查看专栏目录 Network 灰鸽宝典专栏主要关注服务器的配置&#xff0c;前后端开发环境的配置&#xff0c;编辑器的配置&#xff0c;网络服务的配置&#xff0c;网络命令的应用与配置&#xff0c;windows常见问题的解决等。 文章目录 方式一源代码: 方式二结尾语网络的梦想 markd…

多维时序 | Matlab实现GWO-TCN-Multihead-Attention灰狼算法优化时间卷积网络结合多头注意力机制多变量时间序列预测

多维时序 | Matlab实现GWO-TCN-Multihead-Attention灰狼算法优化时间卷积网络结合多头注意力机制多变量时间序列预测 目录 多维时序 | Matlab实现GWO-TCN-Multihead-Attention灰狼算法优化时间卷积网络结合多头注意力机制多变量时间序列预测效果一览基本介绍程序设计参考资料 效…

【设计并实现一个满足 LRU (最近最少使用) 缓存约束的数据结构】

文章目录 一、什么是LRU&#xff1f;二、LinkedHashMap 实现LRU缓存三、手写LRU 一、什么是LRU&#xff1f; LRU是Least Recently Used的缩写&#xff0c;意为最近最少使用。它是一种缓存淘汰策略&#xff0c;用于在缓存满时确定要被替换的数据块。LRU算法认为&#xff0c;最近…

多输入多输出 | Matlab实现SSA-CNN麻雀算法优化卷积神经网络多输入多输出预测

多输入多输出 | Matlab实现SSA-CNN麻雀算法优化卷积神经网络多输入多输出预测 目录 多输入多输出 | Matlab实现SSA-CNN麻雀算法优化卷积神经网络多输入多输出预测预测效果基本介绍模型背景程序设计参考资料 预测效果 基本介绍 Matlab实现SSA-CNN麻雀算法优化卷积神经网络多输入…

cmd输入python直接弹出windows应用商店

明明已经安装好了python&#xff0c;并且也确认配置好了python的环境变量&#xff0c;但是在cmd里输入python后&#xff0c;直接弹出windows商店&#xff0c;python获取界面&#xff0c;其实只需要关闭系统里的应用执行别名设置&#xff0c;最近出来的电脑系统里是自带开启了py…

.NET国产化改造探索(六)、银河麒麟操作系统中安装多个.NET版本

随着时代的发展以及近年来信创工作和…废话就不多说了&#xff0c;这个系列就是为.NET遇到国产化需求的一个闭坑系列。接下来&#xff0c;看操作。 上一篇文章介绍了如何在银河麒麟操作系统上&#xff0c;使用Nginx.NET程序实现自启动。本文介绍下如何在一个环境中&#xff0c;…

【React】组件性能优化、高阶组件

文章目录 React性能优化SCUReact更新机制keys的优化render函数被调用shouldComponentUpdatePureComponentshallowEqual方法高阶组件memo 获取DOM方式refs如何使用refref的类型 受控和非受控组件认识受控组件非受控组件 React的高阶组件认识高阶函数高阶组件的定义应用一 – pro…

Object.prototype.toString.call个人理解

文章目录 这段代码的常见用处参考文献&#xff1a; 拆分理解1、Object.prototype.toString小问题参考文献&#xff1a; 2、call函数的作用参考文献 3、继续深入一些&#xff08;这部分内容是个人理解&#xff0c;没有明确文献支撑&#xff09; 这段代码的常见用处 Object.prot…

云轴科技ZStack位列IDC云系统软件市场教育行业TOP2

近日&#xff0c;全球IT市场研究和咨询公司IDC发布 《中国云系统软件市场跟踪报告2023H1》 ZStack作为产品化的云基础软件提供商 位居云系统软件市场第一梯队 市场份额位列独立云厂商*第一 营收同比增速最快 教育行业TOP2 在教育行业&#xff0c;云计算已成为教育行业信息化的…

Chatgpt+Comfyui绘图源码说明及本地部署文档

其他文档地址&#xff1a; ChatgptComfyui绘图源码运营文档 ChatgptComfyui绘图源码线上部署文档 一、源码说明 1、源码目录说明 app_home&#xff1a;app官网源码chatgpt-java&#xff1a;管理后台服务端源码、用户端的服务端源码chatgpt-pc&#xff1a;电脑网页前端源码cha…

机器学习 | 掌握Matplotlib的可视化图表操作

Matplotlib是python的一个数据可视化库&#xff0c;用于创建静态、动态和交互式图表。它可以制作多种类型的图表&#xff0c;如折线图、散点图、柱状图、饼图、直方图、3D 图形等。以渐进、交互式方式实现数据可视化。当然博主也不能面面俱到的讲解到所有内容&#xff0c;详情请…

Qt 拖拽事件示例

一、引子 拖拽这个动作,在桌面应用程序中是非常实用和具有很友好的交互体验的。我们常见的譬如有,将文件拖拽到某个窗口打开,或者拖拽文件到指定位置上传;在绘图软件中,选中某个模板、并拖拽到画布上,画布上变回绘制该模板的图像… 诸如此类,数不胜数。 那么,在Qt中我…

初识k8s(概述、原理、安装)

文章目录 概述由来主要功能 K8S架构架构图组件说明ClusterMasterNodekubectl 组件处理流程 K8S概念组成PodPod控制器ReplicationController&#xff08;副本控制器&#xff09;ReplicaSet &#xff08;副本集&#xff09;DeploymentStatefulSet &#xff08;有状态副本集&#…

Spring Security工作原理(三)

在认证之间保存请求 如处理安全异常中所示,当请求没有认证且需要认证资源时,需要保存请求以便在认证成功后重新请求受保护的资源。在Spring Security中,这是通过使用RequestCache实现来保存HttpServletRequest来实现的。 RequestCache HttpServletRequest被保存在Request…