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) 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. 也会持续关注大家的问题与需求,大家可以提一些好的建议。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

数据结构<1>——树状数组

树状数组&#xff0c;也叫Fenwick Tree和BIT(Binary Indexed Tree)&#xff0c;是一种支持单点修改和区间查询的&#xff0c;代码量小的数据结构。 那神马是单点修改和区间查询&#xff1f;我们来看一道题。 洛谷P3374(模板): 在本题中&#xff0c;单点修改就是将某一个数加上…

(C++)简单计算器

文章目录 一、实验目的、内容二、实验程序设计及结构1.需求分析变量函数 2.设计结构或流程图 三、设计过程四、测试分析第一组第二组实验中出现的bug及解决方案 五、设计的特点和结果 一、实验目的、内容 输入是一个带有括号的四则运算表达式&#xff0c;输出是计算得出的正确…

canvas绘制美国国旗(USA Flag)

查看专栏目录 canvas实例应用100专栏&#xff0c;提供canvas的基础知识&#xff0c;高级动画&#xff0c;相关应用扩展等信息。canvas作为html的一部分&#xff0c;是图像图标地图可视化的一个重要的基础&#xff0c;学好了canvas&#xff0c;在其他的一些应用上将会起到非常重…

kubeadm部署k8s1.27.2版本高可用集群(外部etcd集群带TLS认证)

文章目录 环境软件版本服务器系统初始化etcd 证书生成etcd集群部署负载均衡器部署部署k8s集群部署网络组件FAQ 环境 控制平面节点主机的配置最少是2C2G,否则kubeadm init的时候会报错 主机名IP组件系统os128192.168.177.128etcd、kube-apiserver、kube-controller-manager、k…

Kubernetes/k8s之HPA,命名空间资源限制

Horizontal Pod Autoscaling:po的水平自动伸缩 这是k8s自带的模块 pod占用cpu比例达到一定的阀值&#xff0c;会触发伸缩机制。 根据cpu的阀值触发伸缩机制 replication controller 副本控制器 控制pod的副本数 deployment controller 节点控制器 部署pod hpa控制副本的数…

玩客云Armbian 23.8.1 Bullseye安装PrometheusGrafana

Welcome to Armbian 23.8.1 Bullseye with bleeding edge Linux 6.4.13-edge-meson prometheus 参考Monitoring – How to install Prometheus/Grafana on arm – Raspberry PI/Rock64 | Blogs (mytinydc.com) cd /usr/local/srcwget https://github.com/prometheus/prometh…

Studio One 6 mac 6.5.2 激活版 数字音乐编曲创作

PreSonus Studio One是PreSonus出品的一款功能强大的音乐创作软件。主要为用户提供音乐创作、录音、编辑、制作等功能。它可以让你创造音乐&#xff0c;无限的轨道&#xff0c;无限的MIDI和乐器轨道&#xff0c;虚拟乐器和效果通道&#xff0c;这些都是强大和完美的。 软件下载…

不合格机器人工程讲师再读《悉达多》-2024-

一次又一次失败的经历&#xff0c;让我对经典书籍的认同感越来越多&#xff0c;越来越觉得原来的自己是多么多么的无知和愚昧。 ----zhangrelay 唯物也好&#xff0c;唯心也罢&#xff0c;我们都要先热爱这个世界&#xff0c;然后才能在其中找到自己所热爱的事业。 ----zh…

神经网络的学习(Neural Networks: Learning)

1.代价函数 案例&#xff1a;假设神经网络的训练样本有&#x1d45a;个&#xff0c;每个包含一组输入&#x1d465;和一组输出信号&#x1d466;&#xff0c;&#x1d43f;表示神经网络层数&#xff0c;&#x1d446;&#x1d43c;表示每层的 neuron 个数(&#x1d446;&#…

web安全思维导图(白帽子)

web安全思维导图(白帽子) 客户端脚本安全 服务端应用安全 白帽子讲web安全 安全运营体系建设

‘cnpm‘ 不是内部或外部命令,也不是可运行的程序

一、问题 昨天用npm 安装环境&#xff0c;实在太慢了&#xff0c;就想用cnpm&#xff0c;然后发现提示‘cnpm 不是内部或外部命令,也不是可运行的程序。 看了很多方法&#xff0c;选择了下面这个&#xff0c;运气好到爆棚&#xff0c;就直接可以用了。其他的方法暂未去了解。先…

C++模板与STL【STL概述】

&#x1f308;个人主页&#xff1a;godspeed_lucip &#x1f525; 系列专栏&#xff1a;C从基础到进阶 &#x1f30f;1 STL概述&#x1f349;1.1 STL的诞生&#x1f349;1.2 STL基本概念&#x1f349;1.3 STL六大组件&#x1f349;1.4 STL中容器、算法、迭代器&#x1f349;1.5…

Typecho后台无法登录显示503 service unavailable问题及处理

一、Typecho 我的博客地址&#xff1a;https://www.aomanhao.top 使用老薛主机动态Typecho博客框架handsome主题的搭配&#xff0c;文章内容可以异地网页更新&#xff0c;可以听后台背景音乐&#xff0c;很好的满足我的痛点需求&#xff0c;博客部署在云端服务器访问响应较快…

微信小程序(十二)在线图标与字体的获取与引入

注释很详细&#xff0c;直接上代码 上一篇 新增内容&#xff1a; 1.从IconFont获取图标与文字的样式链接 2.将在线图标配置进页面中&#xff08;源码&#xff09; 3.将字体配置进页面文字中&#xff08;源码&#xff09; 4.css样式的多文件导入 获取链接 1.获取图标链接 登入…

百度大脑 使用

百度大脑&#xff1a; 官方网址&#xff1a;https://ai.baidu.com/ 文档中心&#xff1a;https://ai.baidu.com/ai-doc 体验中心&#xff1a;https://ai.baidu.com/experience 百度大脑则是百度AI核心技术引擎&#xff0c;它包括基础层、感知层、认知层和安全&#xff0c;是百…

理想架构的非对称高回退Doherty功率放大器理论与仿真

Doherty理论—理想架构的非对称高回退Doherty功率放大器理论与仿真 参考&#xff1a; 三路Doherty设计 01 射频基础知识–基础概念 Switchmode RF and Microwave Power Amplifiers、 理想架构的Doherty功率放大器&#xff08;等分经典款&#xff09;的理论与ADS电流源仿真参考…

python爬虫之JS逆向基础小案例:网抑云数据获取

嗨喽~大家好呀&#xff0c;这里是魔王呐 ❤ ~! python更多源码/资料/解答/教程等 点击此处跳转文末名片免费获取 所用软件 解释器: python 3.8 编辑器: pycharm 2022.3 使用的模块 第三方模块&#xff1a; requests >>> 数据请求 execjs >>> pip insta…

Structure-from-Motion Revisited(COLMAP) 流程介绍

Structure-from-Motion Revisited&#xff08;COLMAP&#xff09;流程介绍 主要贡献1 场景图增强2 下一最佳视图选择3 稳健高效的三角化4 BA5 冗余视图挖掘 Reference&#xff1a; Structure-from-Motion Revisited 原文COLMAP 使用教程Colmap论文——《Structure-from-Motion …

HarmonyOS鸿蒙应用开发( 四、重磅组件List列表组件使用详解)

List列表组件&#xff0c;是一个非常常用的组件。可以说在一个应用中&#xff0c;它的身影无处不在。它包含一系列相同宽度的列表项&#xff0c;适合连续、多行呈现同类数据&#xff0c;如商品列表、图片列表和和文本列表等。ArkUI 框架采用 List 容器组件创建列表&#xff08;…

红队打靶练习:W34KN3SS: 1

目录 信息收集 1、arp 2、nmap 3、nikto 4、gobuster 5、dirsearch WEB web信息收集 目录探测 漏洞利用 openssl密钥碰撞 SSH登录 提权 get user.txt get passwd 信息收集 1、arp ┌──(root㉿ru)-[~/kali] └─# arp-scan -l Interface: eth0, type: EN10MB…