0

I've been working on a project where I created a custom toolbar for EditorJS, aiming to replace the default toolbar. The toolbar is designed to add and remove styles such as bold, italic, etc. I've managed to achieve the desired behavior, but I'm not entirely satisfied with the code structure, performance, and approach. I would greatly appreciate any feedback or suggestions on how I can improve the implementation or if there's a better approach I could take.

import React, {
  createContext,
  useRef
} from 'react';
import EditorJS from '@editorjs/editorjs';
import Header from '@editorjs/header';
import Checklist from '@editorjs/checklist'
import RawTool from '@editorjs/raw';
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';
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({
      holder: "editorjs",
      placeholder: "Let's take a note!",
      tools: {
        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();

  };




  ////// 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 ////////





  /////////////////////// 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;
    console.log('Elapsed time:', elapsedTime, 'milliseconds');
  }

  /**
   * This function is responsible for modifying the styling of the selected text within the block content.
   * It considers multiple cases, taking bold styling as an example.
   * The process starts by splitting the text and iterating through the full HTML text to determine the left, middle, and right portions.
   * Then, it performs checks on the left, right, and middle to determine which case we are in.
   *
   * There are 5 cases, for example, when the bold button is pressed:
   * 1. If the text is "123456789" and "2345678" is selected, we are in the last case, which is simple addition.
   * 2. If the text is "1<b>2345678</b>9" and "2345678" is selected, we are in case 3, which is removing the `<b></b>` tags.
   * 3. If the text is "1<b>2345678</b>9" and "45678" is selected, we are in case 3, which is changing the position of the `</b>` tag.
   * 4. If the text is "1<b>2345678</b>9" and "23456" is selected, we are in case 4, which is changing the position of the `<b>` tag.
   * 5. If the text is "1<b>2345678</b>9" and "45" is selected, we are in case 4, which involves adding `</b>` before "45" and adding `<b>` after.
   *
   * These are the common cases, but sometimes other cases like case 1 and 2 occur.
   * Note that all cases except adding are not applied to 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 
         *     exemple 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 ////////






  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;

GitHub Repository Link: https://github.com/nejdbedoui/customEditorJsToolBar.git

2
  • Please provide enough code so others can better understand or reproduce the problem. Commented Mar 20, 2024 at 3:16
  • Try asking a question there: codereview.stackexchange.com Commented Dec 7, 2024 at 11:57

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.