Yjs+quill多人协同富文本

419 阅读2分钟

在react中使用quill

  1. 安装所需依赖
yarn add quill yjs y-quill y-websocket quill-cursors
  1. 初始化配置
import Quill from 'quill';
import 'quill/dist/quill.snow.css'; // 使用了 snow 主题色

const toolbarOptions = {
   container: [
     [{ header: [1, 2, 3, 4, 5, 6, false] }],
     ['bold', 'italic', 'underline', 'strike'], // toggled buttons
     ['blockquote', 'code-block'],
     [{ list: 'ordered' }, { list: 'bullet' }, { list: 'check' }],
     [{ script: 'sub' }, { script: 'super' }], // superscript/subscript
     [{ align: [] }],
     [{ indent: '-1' }, { indent: '+1' }], // outdent/indent
     [{ direction: 'rtl' }], // text direction
     [{ color: [] }, { background: [] }], // dropdown with defaults from theme
     // [{ font: [] }],
     ['link', 'image', 'video', 'formula'],
     ['clean'], // remove formatting button
   ],
 };
 useEffect(() => {
   if (!container.current) return; // 挂载容器
   const quill = new Quill(container.current, {
     theme: 'snow',
     placeholder: '请输入内容',
     modules: {
       toolbar: toolbarOptions,
       // toolbar: {
       //   container: '#toolbar',
       // },
       history: { userOnly: true },
       cursors: true, // 光标开启
     },
   });
   quill.on('text-change', () => {
     console.log('text change', quill.root.innerHTML);
   });
  

    return () => {
    
    };
  }, []);
  1. 使用Yjs、y-quill 进行编辑器绑定(此时编辑器已经具有协同编辑的功能了)
  • package.json中配置一个y-websocket启动
  • "servers": "cross-env HOST=localhost PORT=1234 npx y-websocket"

image.png

import * as Yjs from 'yjs';
import { QuillBinding } from 'y-quill';
import { WebsocketProvider } from 'y-websocket';

const doc = new Yjs.Doc();
const text = doc.getText('quill');
// websocket url
// 房间名
// ydoc
const websocketProvider = new WebsocketProvider(wsUrl, 'my-roomname', doc);
// 创建quill绑定
const quillBinding = new QuillBinding(text, quill);

image.png

  1. 接下来使用quill-cursors 实现光标功能

使用之前必须要注册光标插件

Quill.register('modules/cursors', QuillCursors);
// Quill配置项中cursors一定要打开
// 两种方式都可以
 cursors: true,
 // cursors: {
 //   selectionChangeSource: 'selection-change',
 //   transformOnTextChange: false,
 // },
import { QuillBinding } from 'y-quill';
import { WebsocketProvider } from 'y-websocket';

const websocketProvider = new WebsocketProvider(wsUrl, 'my-roomname', doc);
const awareness = websocketProvider.awareness;
let color = '#' + Math.random().toString(16).split('.')[1].slice(0, 6);
awareness.setLocalStateField('user', {
  name: 'zhangsan',
  color,
});
// 创建quill绑定
const quillBinding = new QuillBinding(text, quill, awareness);
// 监听awareness变化
awareness.on('change', (changes: Yjs.Transaction) => {
  // 获取所有协同信息 用户列表
  const allUsers = Array.from(awareness.getStates().values()).map((item) => item.user);
  console.log('changes', changes, allUsers);
});

  1. 这样就已经实现协同编辑,并且带有光标的效果了。

image.png

  1. 完整代码如下
import Quill from 'quill';
import QuillCursors from 'quill-cursors';
import 'quill/dist/quill.snow.css'; // 使用了 snow 主题色
import React, { useEffect, useRef } from 'react';
import { QuillBinding } from 'y-quill';
import { WebsocketProvider } from 'y-websocket';
import * as Yjs from 'yjs';
import { wsUrl } from './config';

const Editor: React.FC = () => {
  const container = useRef<HTMLDivElement | null>(null);
  const fontSizeStyle = Quill.import('attributors/style/size'); // 引入这个后会把样式写在style上
  fontSizeStyle.whitelist = [
    '12px',
    '14px',
    '16px',
    '18px',
    '20px',
    '24px',
    '28px',
    '32px',
    '36px',
  ];
  Quill.register(fontSizeStyle, true);
  Quill.register('modules/cursors', QuillCursors);

  const toolbarOptions = {
    container: [
      [{ header: [1, 2, 3, 4, 5, 6, false] }],
      ['bold', 'italic', 'underline', 'strike'], // toggled buttons
      ['blockquote', 'code-block'],
      [{ list: 'ordered' }, { list: 'bullet' }, { list: 'check' }],
      [{ script: 'sub' }, { script: 'super' }], // superscript/subscript
      [{ align: [] }],
      [{ indent: '-1' }, { indent: '+1' }], // outdent/indent
      [{ direction: 'rtl' }], // text direction
      [{ color: [] }, { background: [] }], // dropdown with defaults from theme
      // [{ font: [] }],
      ['link', 'image', 'video', 'formula'],
      ['clean'], // remove formatting button
    ],
  };
  useEffect(() => {
    if (!container.current) return;
    const quill = new Quill(container.current, {
      theme: 'snow',
      placeholder: '请输入内容',
      modules: {
        toolbar: toolbarOptions,
        // toolbar: {
        //   container: '#toolbar',
        // },
        history: { userOnly: true },
        cursors: true,
        // cursors: {
        //   selectionChangeSource: 'selection-change',
        //   transformOnTextChange: false,
        // },
      },
    });

    quill.on('text-change', () => {
      console.log('text change', quill.root.innerHTML);
    });

    const doc = new Yjs.Doc();
    const text = doc.getText('quill');
    // websocket url
    // 房间名
    // ydoc
    const websocketProvider = new WebsocketProvider(wsUrl, 'my-roomname', doc);
    const awareness = websocketProvider.awareness;
    let color = '#' + Math.random().toString(16).split('.')[1].slice(0, 6);
    awareness.setLocalStateField('user', {
      name: 'zhangsan',
      color,
    });
    // 创建quill绑定
    const quillBinding = new QuillBinding(text, quill, awareness);
    // 监听awareness变化
    awareness.on('change', (changes: Yjs.Transaction) => {
      // 获取所有协同信息 显示光标位置和用户列表
      const allUsers = Array.from(awareness.getStates().values()).map((item) => item.user);
      console.log('changes', changes, allUsers);
    });
    // // 链接成功之后删除quill里面的内容
    // websocketProvider.on('synced', () => {
    //   //   text.delete(0, text.length);
    //   //   text.insert(0, '<p><br></p>');
    // });

    return () => {
      websocketProvider.destroy();
      quillBinding.destroy();
      doc.destroy();
    };
  }, []);
  return (
    <div>
      <div ref={container} />
    </div>
  );
};

export default Editor;