使用editor.js进行实时协作编辑

问题描述 投票:0回答:1

我正在开发一个涉及使用 Editor.js 进行协作文本编辑的项目,并且需要一些帮助来解决以下几个问题: 我正在尝试将 Editor.js 与 Yjs 或 ShareDB 等库集成,以使用 CRDT 或 OT 解决冲突。 每当我更新 Editor.js 中的块时,它就会闪烁并重新渲染,导致用户失去对文本字段的焦点。 这是我当前的代码:

import React, { createContext, useEffect, useRef } from 'react';
import EditorJS from '@editorjs/editorjs';
import Header from '@editorjs/header';
import Checklist from '@editorjs/checklist'
import RawTool from '@editorjs/raw';
import './css.css';
import 'bootstrap/dist/css/bootstrap.min.css';
import './toolBarcss.css';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faStrikethrough, faBold, faItalic, faUnderline } from '@fortawesome/free-solid-svg-icons';
import SketchExample from './SketchExample';
import Underline from '@editorjs/underline';
import Strikethrough from 'editorjs-strikethrough';
import { SimpleImage } from './simpleimage';
import Video from '@weekwood/editorjs-video';


export const EditorContext = createContext()
let blockid = null;
let blockElement = null;
let data = null;
let selectedText = null;
let startPosition = null;
let endPosition = null;
function Editor(props) {

  const editorInstanceRef = useRef(null)
  const initEditor = () => {
    const editor = new EditorJS({
      readOnly: false,
      holder: "editorjs",
      placeholder: "Let's take a note!",
      tools: {
        image: {
          class: SimpleImage,
          config: {
            endpoints: {
              byFile: 'http://localhost:8008/uploadFile', // Your backend file uploader endpoint
              byUrl: 'http://localhost:8008/fetchUrl', // Your endpoint that provides uploading by Url
            }
          },

          actions: [
            {
              name: 'new_button',
              icon: '<svg>...</svg>',
              title: 'New Button',
              toggle: true,
              action: (name) => {
                alert(`${name} button clicked`);
              }
            }
          ]
        }, video: {
          class: Video,
          config: {
            endpoints: {

            }
          }
        },

        strikethrough: {
          class: Strikethrough,
          shortcut: 'CMD+SHIFT+X',
        },
        underline: Underline,
        header: {
          class: Header,
          config: {
            placeholder: 'Enter a header',
            levels: [1, 2, 3, 4],
            defaultLevel: 1,
            shortcut: 'CMD+SHIFT+H',
          }
        },
        raw: {
          class: RawTool,
          inlineToolbar: false,
        },
        checklist: {
          class: Checklist,
          inlineToolbar: false,
        }
      },
      onChange: async () => {
        data = await editor.save();
      }

    })
    editorInstanceRef.current = editor
    const editorContainer = document.getElementById('editorjs');
    editorContainer.addEventListener('click', handleBlockClick);
    editorContainer.addEventListener('mouseup', selectionevent);
  }


  /**
   * Handles the user selection event within an editor block.
   * This function determines the closest block element based on the event target,
   * retrieves the selected text and its start and end positions within the block,
   * and gets the index of the current block in the editor.
   * 
   * @param {Event} event - The event object triggered by the user action.
   */

  const selectionevent = async (event) => {
    const closestBlock = event.target.closest('.ce-block');
    let blockElement, blockId;
    if (closestBlock) {
      blockElement = closestBlock;
      blockId = blockElement.getAttribute('data-id');
    } else {
      blockElement = null;
      blockId = null;
    }
    const selection = window.getSelection();
    selectedText = selection.toString();
    const range = selection.getRangeAt(0);
    const preSelectionRange = range.cloneRange();
    preSelectionRange.selectNodeContents(blockElement);
    preSelectionRange.setEnd(range.startContainer, range.startOffset);
    startPosition = preSelectionRange.toString().length;

    // Adjust endPosition by excluding the length of the selectedText itself
    endPosition = startPosition + selectedText.length;
    index = editorInstanceRef.current.blocks.getCurrentBlockIndex();

  };





  ////// add bold italic underline or color to selected text ////////


  /**
   * Changes the color of a font element in the HTML content.
   * This function takes a color value as input and generates opening and closing tags
   * for a font element with the specified color style. It then calls the changeStyle function
   * to apply the color change to the HTML content.
   * 
   * @param {string} data - The color value to apply (e.g., "red", "#00ff00", "rgb(255, 0, 0)").
   */
  const changeColor = (data) => {
    const word = "font"
    const open = `<font style="color: ${data};">`
    const close = '</font>'
    changeStyle(word, open, close);
  };


  /**
   * Removes empty HTML tags from the given text content.
   * This function parses the HTML text using DOMParser, removes all empty tags,
   * and returns the cleaned HTML text without empty tags.
   * 
   * @param {string} text - The HTML text content to clean.
   * @returns {string} - The cleaned HTML text without empty tags.
   */
  const cleanHTMLTags = (text) => {
    const parser = new DOMParser();
    const doc = parser.parseFromString(text, 'text/html');
    const emptyTags = doc.querySelectorAll(':empty');
    emptyTags.forEach((tag) => tag.parentNode.removeChild(tag));
    const cleanedText = doc.body.innerHTML;
    return cleanedText;
  };



  /**
   * Adds style tags (bold italic underline and strike) around the selected text in the block content
   *  using the changestyle function.
   */
  const addstyle = (word) => {
    const startTime = performance.now();
    const open = `<${word}>`;
    const close = `</${word}>`;
    changeStyle(word, open, close);
    const endTime = performance.now();
    const elapsedTime = endTime - startTime;
  }

  /**
 * This function is responsible for modifying the styling of the selected text in the block content.
 * It take in consideration multiple cases lets take bold for example 
 * it will start by splitting the text and Iterate through the full html text to determine left, middle, and right portions 
 * after that it perform checks on the right left and middle to determine which case are we in 
 * I have 5 cases for example when I press the bold button:
 * if I have a text "123456789"       and I select "2345678"  we are in the last case which is simple adding 
 * if I have a text "1<b>2345678<b>9" and I select "2345678"  we are in case 3 which is removing the <b></b>
 * if I have a text "1<b>2345678<b>9" and I select "45678"    we are in case 3 which is changing the </b> position
 * if I have a text "1<b>2345678<b>9" and I select "23456"    we are in case 4 which is changing the <b> position
 * if I have a text "1<b>2345678<b>9" and I select "45"       we are in case 4 which is adding the </b> before "45" and adding <b> after
 * 
 * those are the common cases but sometimes I got other cases like case 1 and 2 
 * note all cases except the adding are not applied for the font tag
 * 
 */
  const changeStyle = async (word, open, close) => {
    let left = '';
    let midle = '';
    let right = '';
    let leftResult;
    let midleResult;
    let rightResult;

    // Start and end positions for text modification
    let a = startPosition;
    let b = endPosition;

    // Ensure a <= b because when I select a text from right to left they switch 
    if (a > b) {
      a = startPosition;
      b = startPosition;
    }

    // Check if blockid is provided and fetch updated data
    if (blockid) {
      const updatedData = data;
      const currentBlock = updatedData.blocks.find((block) => block.id === blockid);

      // If the current block is found
      if (currentBlock) {
        let currentText = currentBlock.data.text;

        /**  Split the full html text with the tags into an array for processing 
        *     example text:"123456789" html text: "123<b>456</b>789"
        */
        const textArray = currentText.split('');
        let skipMode = false;

        // Iterate through the full html text to determine left, middle, and right portions 
        for (let i = 0; i < textArray.length && i < b; i++) {
          if (i === b) {
            break;
          }

          if (textArray[i] === '<') {
            skipMode = true;
          }

          if (skipMode && i <= a) {
            a++;
            b++;
          }

          if (skipMode && i > a) {
            b++;
          }

          if (textArray[i] === '>') {
            skipMode = false;
          }
        }

        // Extract left, middle, and right portions of the text
        left = currentText.substring(0, a)
        midle = currentText.substring(a, b)
        right = currentText.substring(b)

        // Perform checks and modifications based on the left, middle, and right portions
        leftResult = checkLeft(left, word)
        midleResult = countAndSubtractTags(midle, word)
        rightResult = checkright(right, word)

        // Construct modifiedText based on the checks and results
        if (leftResult.check && rightResult.check && word !== "font") {
          console.log("case 1")
          const modifiedText = [
            leftResult.text,
            midleResult.text,
            rightResult.text
          ].join('');
          currentBlock.data.text = cleanHTMLTags(modifiedText);
        } else if (leftResult.check && !rightResult.check && word !== "font") {
          console.log("case 2")
          const modifiedText = [
            leftResult.text,
            midleResult.text,
            rightResult.storedOpenTags,
            rightResult.text
          ].join('');
          currentBlock.data.text = cleanHTMLTags(modifiedText);
        } else if (!leftResult.check && rightResult.check && word !== "font") {
          console.log("case 3")
          const modifiedText = [
            leftResult.text,
            rightResult.CloseTag,
            midleResult.text,
            rightResult.text
          ].join('');
          currentBlock.data.text = cleanHTMLTags(modifiedText);
        } else if (!leftResult.check && !rightResult.check && leftResult.CloseTag && rightResult.storedOpenTags && word !== "font") {
          console.log("case 4")
          if (word === "font") {
            midleResult.text = open + midleResult.text + close;
          }
          const modifiedText = [
            leftResult.text,
            leftResult.CloseTag,
            midleResult.text,
            rightResult.storedOpenTags,
            rightResult.text
          ].join('');
          currentBlock.data.text = cleanHTMLTags(modifiedText);
        } else {
          console.log("case adding")
          const modifiedText = [
            currentText.substring(0, a),
            midleResult.storedCloseTags,
            open,
            midleResult.text,
            close,
            midleResult.storedOpenTags,
            currentText.substring(b)
          ].join('');
          currentBlock.data.text = cleanHTMLTags(modifiedText);
        }
      }

      // Render the updated data in the editor
      editorInstanceRef.current.render(updatedData);
    }
  }

  /**
   * Analyzes the left part of a text string in HTML content for a specified word.
   * This function looks for the last occurrence of the opening tag and the corresponding
   * closing tag related to the specified word in the text. It handles tags and modifies
   * the text accordingly, providing information about found tags and any modifications made.
   * 
   * @param {string} text - The input text string to be analyzed.
   * @param {string} word - The specified word that the function focuses on in the text.
   * @returns {Object} An object containing storedOpenTags, CloseTag, modified text, and check flag.
   * - storedOpenTags: Any opening tags related to the specified word found in the text.
   * - CloseTag: The closing tag related to the specified word.
   * - text: The modified text after tag processing.
   * - check: A flag indicating whether modifications were made to the text (true/false).
   */
  function checkLeft(text, word) {
    let storedOpenTags = '';
    let CloseTag = '';
    let check = false;

    // Check if the input text is not empty
    if (text !== '') {
      let startIndex = text.length - 1;
      let endIndex = text.length;

      // Iterate backwards through the text to find the last occurrence of the opening tag of the word
      while ((startIndex = text.lastIndexOf('<' + word[0], startIndex)) !== -1) {
        // Find the corresponding closing tag for the word
        endIndex = text.indexOf(word[word.length - 1] + '>', startIndex);

        // If no closing tag is found, break the loop
        if (endIndex === -1) {
          break;
        }

        // Extract the tag from startIndex to endIndex
        const tag = text.substring(startIndex, endIndex + 1);

        // Check if the tag is a closing tag for the word
        if (tag.startsWith('</' + word)) {
          // If it's a closing tag, break the loop
          break;
        } else if (tag.startsWith('<' + word)) {
          // If it's an opening tag, store it as the storedOpenTags
          storedOpenTags = text.substring(startIndex, endIndex + 1);

          // Check if the opening tag is at the end of the text
          if (endIndex + 1 === text.length) {
            // If yes, remove the tag from the text and set check to true
            text = text.substring(0, startIndex);
            check = true;
            break;
          } else {
            // If not, set the CloseTag and break the loop
            CloseTag = '</' + word + '>';
            break;
          }
        }
        // Move to the previous character in the text
        startIndex--;
      }
    }

    // Return an object containing the storedOpenTags, CloseTag, modified text, and check flag
    return { storedOpenTags, CloseTag, text, check };
  }


  /**
   * Analyzes the right part of a text string in HTML content for a specified word.
   * This function looks for opening and closing tags related to the specified word
   * in the right part of the text. It handles tags and modifies the text accordingly,
   * providing information about found tags and any modifications made.
   * 
   * @param {string} text - The input text string to be analyzed.
   * @param {string} word - The specified word that the function focuses on in the text.
   * @returns {Object} An object containing storedOpenTags, CloseTag, modified text, and check flag.
   * - storedOpenTags: Any opening tags related to the specified word found in the text.
   * - CloseTag: The closing tag related to the specified word.
   * - text: The modified text after tag processing.
   * - check: A flag indicating whether modifications were made to the text (true/false).
   */
  function checkright(text, word) {
    // Initialize variables to store open tags, closing tags, and a check flag
    let storedOpenTags = '';
    let CloseTag = '';
    let startIndex = 0;
    let endIndex = 0;
    let check = false;

    // Iterate through the text to find opening and closing tags related to the specified word
    while ((startIndex = text.indexOf('<', endIndex)) !== -1 && endIndex != text.length) {
      // Find the index of the closing '>' character
      endIndex = text.indexOf('>', startIndex);

      // If no closing tag is found, break the loop
      if (endIndex === -1) {
        break;
      }

      // Extract the tag from startIndex to endIndex
      const tag = text.substring(startIndex, endIndex + 1);

      // Check if the tag is a closing tag for the specified word
      if (tag.startsWith('</' + word)) {
        // If it's a closing tag, check if it's at the begining of the text
        if (tag.length === endIndex + 1) {
          // If yes, remove the tag from the text and set CloseTag and check flags
          text = text.substring(endIndex + 1, text.length);
          CloseTag = tag;
          check = true;
        } else {
          // If it's not the entire tag, set CloseTag and storedOpenTags accordingly
          CloseTag = tag;
          storedOpenTags = '<' + word + '>';
        }
        // Break the loop after processing the closing tag
        break;
      } else if (tag.startsWith('<' + word)) {
        // If it's an opening tag, break the loop
        break;
      }
      endIndex++; // Move to the next '>' character
    }

    // Return an object containing the stored open tags, CloseTag, modified text, and check flag
    return { storedOpenTags, CloseTag, text, check };
  }



  /**
   * Counts and subtracts opening and closing tags related to a specified word in the text.
   * This function iterates through the text to find and process tags (opening and closing)
   * related to the specified word. It counts the occurrences of opening and closing tags,
   * subtracts them based on certain conditions, and handles tag removal and storage.
   * 
   * @param {string} text - The input text string to be analyzed for tags.
   * @param {string} word - The specified word that the function focuses on in the text.
   * @returns {Object} An object containing storedOpenTags, storedCloseTags, and modified text.
   * - storedOpenTags: Any opening tags related to the specified word found in the text.
   * - storedCloseTags: Any closing tags related to the specified word found in the text.
   * - text: The modified text after tag processing.
   */
  function countAndSubtractTags(text, word) {
    // Initialize variables to store open tags, closing tags, and flags
    let storedOpenTags = '';
    let storedCloseTags = '';
    let closedtag = false;
    let startIndex = 0;
    let endIndex = 0;
    let startopen = false;
    let countWord1 = (text.match(new RegExp('<' + word, 'g')) || []).length;
    let countWord2 = (text.match(new RegExp('</' + word + '>', 'g')) || []).length;

    // Iterate through the text to find and process tags related to the specified word
    while ((startIndex = text.indexOf('<', endIndex)) !== -1) {
      endIndex = text.indexOf('>', startIndex);
      if (endIndex === -1) {
        break; // Break the loop if no closing '>' character is found
      }

      const tag = text.substring(startIndex, endIndex + 1);

      if (tag.startsWith('</' + word)) {
        // Handle closing tags
        if (!startopen) {
          // If it's the first closing tag encountered, store it in storedCloseTags
          closedtag = true;
          storedCloseTags += text.substring(startIndex, endIndex + 1);
        }
        // Remove the tag from the text and update counters
        text = text.slice(0, startIndex) + text.slice(endIndex + 1);
        startIndex = 0;
        endIndex = 0;
        countWord2--;
      } else if (tag.startsWith('<' + word)) {
        // Handle opening tags
        startopen = true;
        if (countWord2 > 0) {
          // If closing tags are still present, decrement the opening tag counter
          countWord1--;
        } else if (countWord1 > 0 && countWord2 <= 0) {
          // If no closing tags are left but opening tags are present, store in storedOpenTags
          storedOpenTags += text.substring(startIndex, endIndex + 1);
          closedtag = false;
        }
        // Remove the tag from the text and reset indices
        text = text.slice(0, startIndex) + text.slice(endIndex + 1);
        startIndex = 0;
        endIndex = 0;
      }
    }

    // Return an object containing the stored open tags, stored close tags, and modified text
    return { storedOpenTags, storedCloseTags, text };
  }

  ////// end of add bold italic underline or color to selected text ////////





  ////// when block is clicked ////////
  const handleBlockClick = async (event) => {
    const closestBlock = event.target.closest('.ce-block');
    if (closestBlock) {
      blockElement = closestBlock;
      blockid = blockElement.getAttribute('data-id');
    } else {
      blockElement = null;
      blockid = null;
    }
  };
  ////// end of when block is clicked ////////


  return (
    <>

      <div className="container">
        <div className="btn-group" role="group" aria-label="Basic example">
          <button onClick={() => addstyle('b')} type="button" className="btn btn-light btn-sm">
            <FontAwesomeIcon icon={faBold} />
          </button>
          <button onClick={() => addstyle('i')} type="button" className="btn btn-light btn-sm">
            <FontAwesomeIcon icon={faItalic} />
          </button>
          <button onClick={() => addstyle('u')} type="button" className="btn btn-light btn-sm">
            <FontAwesomeIcon icon={faUnderline} />
          </button>
          <button onClick={() => addstyle('strike')} type="button" className="btn btn-light btn-sm">
            <FontAwesomeIcon icon={faStrikethrough} />
          </button>
          <SketchExample onData={changeColor} />
        </div>

      </div>


      <EditorContext.Provider value={{ initEditor, editorInstanceRef }}>
        {props.children}

      </EditorContext.Provider>

    </>

  )
}
export default Editor;
javascript reactjs editorjs yjs sharedb
1个回答
0
投票

您的代码非常复杂,并且有很多内容超出了问题的范围,因此出于说明目的并使这个答案对任何人都更容易理解,这里是带有 Ys (用于 CRDT)的 editor.js 的简单实现有效。

这是 CRDT 增强型实时 Editor.js 共享接口的实现。

服务器端代码(没有,使用y-websocket。更多详细信息请参阅y-websocket):

HOST=localhost PORT=1234 npx y-websocket

客户端代码:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Real-Time Collaborative Editor with CRDT</title>
  <script src="https://cdn.jsdelivr.net/npm/@editorjs/editorjs@latest"></script>
  <script src="https://unpkg.com/yjs@latest"></script>
  <script src="https://unpkg.com/y-websocket@latest"></script>
</head>
<body>
  <div id="editorjs"></div>
  <script>
    const ydoc = new Y.Doc();
    const provider = new WebSocketProvider('wss://localhost:1234', 'my-document-id', ydoc);

    // Create a shared Y.Map for storing editor content
    const yText = ydoc.getText('editor');

    // Listen for changes and update the editor
    const editor = new EditorJS({
      holder: 'editorjs',
      onChange: async () => {
        const content = await editor.save();
        yText.delete(0, yText.length);
        yText.insert(0, JSON.stringify(content));
      },
    });

    // Update the editor whenever the shared data changes
    yText.observe(async () => {
      const content = JSON.parse(yText.toString());
      await editor.render(content);
    });

    provider.on('status', (event) => {
      console.log('Connection status:', event.status); // Logs connection status
    });
  </script>
</body>
</html>

© www.soinside.com 2019 - 2024. All rights reserved.