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

news2024/12/23 19:26:13

前言

        对于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) return
    const 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) return
    const 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.Doc
  private ymap: Y.Map<unknown>
  private ytext: Y.Text
  private provider: any | undefined
  private connect: boolean | undefined
  private url: string
  private roomname: string

  constructor(url: string, roomname: string) {
    console.log('new Ydoc')
    this.url = url
    this.roomname = roomname
    this.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 } = event
      if (status === 'connected') this.connect = true
      else this.connect = false
    })
  }

  public disConnection() {
    if (!this.connect) return
    this.provider.disconnect()
  }
}

初始化 yjs 

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

 // 创建 websocket
    if (ydocInfo) {
      let { url, roomname, userid, username, color } = ydocInfo
      if (!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 } = data
    this.command.setUserRange(startIndex, endIndex, userid, color)
  }

        效果如下:

 实现用户取消选区

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

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

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

实现文本输入与删除

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

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

 // 内容区变化
  public contentChangeHandle(payload: IEditorData) {
    /**
     * 因此在这里需要重新解析用户的选区设置,不然会导致选区异常 BUG
     */
    // 这里要解析 userRange
    let { header, footer, main } = payload

    main.forEach(item => {
      if (item.userRange) {
        delete item.highlight
        delete 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 } = payload
    const isReadonly = this.draw.isReadonly()
    if (isReadonly) return
    if (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.color
            break

          case 'bold':
            value ? (el.bold = true) : delete el.bold
            break

          case 'italic':
            value ? (el.italic = true) : delete el.italic
            break

          case 'fontSize':
            break

          case 'underline':
            value ? (el.underline = true) : delete el.underline
            break

          case 'highlight':
            // 这里还有BUG,因为用户选区结束又被设置透明
            value
              ? (el.highlight = <string | undefined>value)
              : delete el.highlight
            break
          default:
            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.coloradmin.cn/o/1407666.html

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

相关文章

C语言|算术操作符相关题目

下面代码的结果是&#xff1a;( ) #include <stdio.h> int main() {int a, b, c;a 5;c a;b c, c, a, a;b a c;printf("a %d b %d c %d\n:", a, b, c);return 0; }A.a 8 b 23 c 8 B.a 9 b 23 c 8 C.a 9 b 25 c 8 D.a 9 b 24 c 8 解析&…

UART接口简介

UART(UniversalAsynchronousReceiver/Transmitter)&#xff0c;即通用异步收发器&#xff0c;它包括了RS232、RS449、RS423、RS422和RS485等接口标准规范和总线标准规范&#xff0c;即UART是异步串行通信口的总称。而RS232、RS449、RS423、RS422和RS485等&#xff0c;是对应各种…

使用QT写个自用的串口助手

遇到一个默认波特率1.5M的终端设备&#xff0c;看了下手上常用的串口助手竟然没有这个选项&#xff0c;所以干脆自己用QT手撕一个。 开发环境&#xff1a;QT 5.12.0 mingw64 一、创建工程 1、新建创建QMainWindow工程&#xff0c;基类可以选择QMainWindow也可以选择Qwiget&a…

ATF(TF-A)安全通告TF-V11——恶意的SDEI SMC可能导致越界内存读取(CVE-2023-49100)

目录 一、ATF(TF-A)安全通告TFV-11 (CVE-2023-49100) 二、透过事务看本质SDEI是干啥的呢&#xff1f; 三、CVE-2023-49100 1、GICv2 systems 2、GICv3 systems 四、漏洞修复 一、ATF(TF-A)安全通告TFV-11 (CVE-2023-49100) Title 恶意的SDEI SMC可能导致越界内存读取&am…

SwiftUI 打造酷炫流光边框 + 微光滑动闪烁的 3D 透视滚动卡片墙

功能需求 有时候我们希望自己的 App 能向用户展示与众不同、富有创造力的酷炫视觉效果: 如上图所示,我们制作了一款流光边框 + 微光滑动闪烁的 3D 透视卡片滚动效果。这是怎么做到的呢? 在本篇博文中,您将学到以下内容 功能需求1. 3D 透视滚动2. 灵动边框流光效果3. 背景…

np.bincount函数的用法

官网写的非常清晰了&#xff0c; 返回数组的数量比x中的最大值大1&#xff0c;它给出了每个索引值在x中出现的次数。下面&#xff0c;我举个例子让大家更好的理解一下&#xff1a; np.bincount(np.array([0, 1, 1, 3, 2, 1, 7])) array([1, 3, 1, 1, 0, 0, 0, 1])最大值是7&a…

5.3 内容管理模块 - 课程发布、任务调度、页面静态化、熔断降级

内容管理模块 - 课程发布 - 任务调度、熔断降级、页面静态化 文章目录 内容管理模块 - 课程发布 - 任务调度、熔断降级、页面静态化一、课程发布 - 任务调度1.1 添加Maven依赖1.2 XxlJobConfig配置文件1.3 消息处理抽象类 MessageProcessAbstract1.4 课程发布任务类 CoursePubl…

webpack常用配置

1.webpack概念 ​ 本质上&#xff0c;webpack 是一个用于现代 JavaScript 应用程序的 静态模块打包工具。当 webpack 处理应用程序时&#xff0c;它会在内部从一个或多个入口点构建一个 依赖图(dependency graph)&#xff0c;然后将你项目中所需的每一个模块组合成一个或多个 …

go语言(十六)----tag

package mainimport ("fmt""reflect" )type resume struct {Name string info:"name" doc:"我的名字"Sex string info:"sex" }func findTag(str interface{}) {t : reflect.TypeOf(str).Elem()for i : 0;i < t.NumField…

Go后端开发 -- 即时通信系统

Go后端开发 – 即时通信系统 文章目录 Go后端开发 -- 即时通信系统一、即时通信系统1.整体框架介绍2.基础server构建3.用户上线及广播功能4.用户消息广播机制5.用户业务封装6.用户在线查询7.修改用户名8.超时强踢9.私聊功能10.完整代码 二、客户端实现1.建立连接2.命令行解析3.…

阿里云幻兽帕鲁服务器租用价格表,免费?

幻兽帕鲁异常火爆自建幻兽帕鲁服务器不卡又稳定&#xff0c;继腾讯云推出幻兽帕鲁自建服务器教程和4核16G幻兽帕鲁专用特价游戏服务器后&#xff0c;阿里云坐不住了&#xff0c;直接推出特价4核32G和4核16G的palworld专属游戏机&#xff0c;另外还可以申请免费3个月的4核8G无影…

C语言或C++通过IShellLinkA创建或解析lnk快捷方式(使用char字符数组)

本例程用到的COM接口有IShellLinkA和IPersistFile。 请注意因为函数参数的类型不为BSTR&#xff0c;所以这两个接口可直接传char *或wchar_t *字符串&#xff0c;不需要提前转化为BSTR类型。 C语言的写法&#xff1a; /* 这个程序只能在C编译器下编译成功, 请确保源文件的扩展…

智慧之光:ChatGPT 引领工作效率新纪元

随着科技的不断发展&#xff0c;人工智能&#xff08;AI&#xff09;已经逐渐融入我们的日常生活和工作中。其中&#xff0c;ChatGPT 作为一种先进的 AI 技术&#xff0c;正逐步改变我们的工作方式&#xff0c;提升我们的工作效率。本文灸哥将介绍如何利用ChatGPT提升工作效率&…

intellij idea怎么设置中文

CtrlAltS快捷键打开Settings界面选择Plugins在搜索部分搜索chinese&#xff0c;选择下方的Chinese&#xff08;simplified&#xff09;Language下载最后重启软件即可

数据结构:完全二叉树(递归实现)

如果完全二叉树的深度为h&#xff0c;那么除了第h层外&#xff0c;其他层的节点个数都是满的&#xff0c;第h层的节点都靠左排列。 完全二叉树的编号方法是从上到下&#xff0c;从左到右&#xff0c;根节点为1号节点&#xff0c;设完全二叉树的节点数为sum&#xff0c;某节点编…

leetcode-hot100双指针专题

第一题&#xff1a;移动零 题目链接 283. 移动零 - 力扣&#xff08;LeetCode&#xff09; 解题思路 我们创建两个指针i,j&#xff0c;第一次遍历的时候指针j用来记录当前面有多少非0元素。即遍历的时候每遇到一个非0元素就将其往数组左边挪&#xff0c;第一次遍历完后&…

解决国内 github.com 打不开的准确方法

** 下载watt toolkit&#xff0c; 选择‘github’&#xff0c;点击‘一键加速’&#xff0c;很简单方便 **

【阿里云服务器数据迁移】 同一个账号 不同区域服务器

前言 假如说一台云服务器要过期了,现在新买了一台,有的人会烦恼又要将重新在新的服务器上装环境,部署上线旧服务器上的网站项目, 但是不必烦恼,本文将介绍如何快速将就旧的服务器上的数据迁移到新的服务器上. 包括所有的环境和网站项目噢 ! 步骤 (1) 创建旧服务器自定义镜像…

Llama2中文大模型——牛刀小试

文章目录 Llama2中文大模型——牛刀小试前言更新库导包加载模型对话问答-1对话问答-2对话问答-3对话问答-4对话问答-5 Llama2中文大模型——牛刀小试 前言 Meta开源的Llama从第一版开始&#xff0c;效果就很不错&#xff0c;有很多开源LLM都是基于它训练的&#xff0c;例如Vic…

GPT-5不叫GPT-5?下一代模型会有哪些新功能?

OpenAI首席执行官奥特曼在上周三达沃斯论坛接受媒体采访时表示&#xff0c;他现在的首要任务就是推出下一代大模型&#xff0c;这款模型不一定会命名GPT-5。虽然GPT-5的商标早已经注册。 如果GPT-4目前解决了人类任务的10%&#xff0c;GPT-5应该是15%或者20%。 OpenAI从去年开…