我正在开发一个涉及使用 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;
您的代码非常复杂,并且有很多内容超出了问题的范围,因此出于说明目的并使这个答案对任何人都更容易理解,这里是带有 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>