UNPKG

react-text-editor-kit

Version:

React Text Editor Kit is a customizable rich text editor component for React applications. It provides a flexible and easy-to-use interface for users to create and edit content with various formatting options. And also easy to integrate in react applicati

317 lines (282 loc) 9.59 kB
import React, { useEffect, useRef, useState } from "react"; import Styles from "../css/style.module.css"; import ArrowDown from "./SVGImages/ArrowDown"; function LineHeight({ editorRef, isDisable }) { const [isOpen, setIsOpen] = useState(false); const [selectedOption, setSelectedOption] = useState("default"); const selectRef = useRef(null); const lineHeights = [ { label: "Default", value: "default" }, { label: "1.0", value: "1" }, { label: "1.1", value: "1.1" }, { label: "1.2", value: "1.2" }, { label: "1.3", value: "1.3" }, { label: "1.4", value: "1.4" }, { label: "1.5", value: "1.5" }, { label: "1.6", value: "1.6" }, { label: "1.8", value: "1.8" }, { label: "2.0", value: "2" }, { label: "2.5", value: "2.5" }, { label: "3.0", value: "3" }, ]; const toggleSelect = (e) => { if (isDisable) { return; } e.preventDefault(); setIsOpen(!isOpen); }; const handleOptionClick = (e, option) => { e.preventDefault(); editorRef.current.focus(); const selection = window.getSelection(); if (!selection.rangeCount) return; const range = selection.getRangeAt(0); const value = option.value; // Function to find the parent block element const findParentBlockElement = (node) => { while (node && node !== editorRef.current) { if (node.nodeType === Node.ELEMENT_NODE) { const tagName = node.tagName.toLowerCase(); if ( [ "p", "div", "h1", "h2", "h3", "h4", "h5", "h6", "li", "blockquote", ].includes(tagName) ) { return node; } } node = node.parentNode; } return null; }; // Function to get all block elements within the selection const getAllBlockElementsInSelection = () => { const blockElements = []; if (selection.isCollapsed) { // If no selection, just get the current block element const currentBlock = findParentBlockElement(range.startContainer); if (currentBlock) { blockElements.push(currentBlock); } return blockElements; } // Get all block elements in the editor const allBlocks = editorRef.current.querySelectorAll( "p, div, h1, h2, h3, h4, h5, h6, li, blockquote", ); // Check which blocks intersect with the selection allBlocks.forEach((block) => { if (rangeIntersectsNode(range, block)) { blockElements.push(block); } }); return blockElements; }; // Helper function to check if range intersects with a node const rangeIntersectsNode = (range, node) => { try { const nodeRange = document.createRange(); nodeRange.selectNodeContents(node); // Check if ranges overlap return ( range.compareBoundaryPoints(Range.START_TO_END, nodeRange) > 0 && nodeRange.compareBoundaryPoints(Range.START_TO_END, range) > 0 ); } catch (e) { return false; } }; // Get all block elements that should be affected const targetElements = getAllBlockElementsInSelection(); if (targetElements.length > 0) { // Apply line height to all found block elements targetElements.forEach((element) => { element.style.lineHeight = value; }); } else { // If no block element found, create a paragraph wrapper const selection = window.getSelection(); const range = selection.getRangeAt(0); // Get the content at cursor or selection let contentToWrap; if (selection.isCollapsed) { // If no selection, wrap the current text node or create new paragraph const currentNode = range.startContainer; if (currentNode.nodeType === Node.TEXT_NODE) { contentToWrap = currentNode; } else { // Create new paragraph with line height const p = document.createElement("p"); p.style.lineHeight = value; p.innerHTML = "<br>"; range.insertNode(p); // Place cursor inside the new paragraph const newRange = document.createRange(); newRange.setStart(p, 0); newRange.collapse(true); selection.removeAllRanges(); selection.addRange(newRange); setSelectedOption(option.label); setIsOpen(false); return; } } else { contentToWrap = range.extractContents(); } // Create paragraph wrapper with line height const p = document.createElement("p"); p.style.lineHeight = value; if (contentToWrap) { p.appendChild(contentToWrap); range.insertNode(p); // Restore selection within the new paragraph const newRange = document.createRange(); newRange.selectNodeContents(p); selection.removeAllRanges(); selection.addRange(newRange); } } setSelectedOption(option.label); setIsOpen(false); // Trigger input event to update editor state const inputEvent = new Event("input", { bubbles: true }); editorRef.current.dispatchEvent(inputEvent); }; const getCurrentLineHeight = () => { const selection = window.getSelection(); if (selection.rangeCount > 0) { const range = selection.getRangeAt(0); const startNode = range.startContainer; // Function to find the parent block element const findParentBlockElement = (node) => { while (node && node !== editorRef.current) { if (node.nodeType === Node.ELEMENT_NODE) { const tagName = node.tagName.toLowerCase(); if ( [ "p", "div", "h1", "h2", "h3", "h4", "h5", "h6", "li", "blockquote", ].includes(tagName) ) { return node; } } node = node.parentNode; } return null; }; // Look for line height in the block element first const blockElement = findParentBlockElement(startNode); if (blockElement && blockElement.style.lineHeight) { const lineHeight = blockElement.style.lineHeight; const foundHeight = lineHeights.find( (height) => height.value === lineHeight, ); if (foundHeight) { return foundHeight.label; } } // Fallback: check any parent element with line height let node = startNode; while (node && node !== editorRef.current) { if (node.nodeType === Node.ELEMENT_NODE && node.style.lineHeight) { const lineHeight = node.style.lineHeight; const foundHeight = lineHeights.find( (height) => height.value === lineHeight, ); if (foundHeight) { return foundHeight.label; } } node = node.parentNode; } } return null; }; const handleKeyDown = () => { const editor = editorRef.current; if (!editor) return; if (!editor.contains(window.getSelection().anchorNode)) { return; } editor.focus(); }; useEffect(() => { const handleClickOutside = (event) => { if (selectRef.current && !selectRef.current.contains(event.target)) { setIsOpen(false); } }; const handleSelectionChange = () => { if (editorRef?.current?.contains(window.getSelection().anchorNode)) { const currentHeight = getCurrentLineHeight(); if (currentHeight) { setSelectedOption(currentHeight); } else { setSelectedOption("default"); } } }; document.addEventListener("click", handleClickOutside); document.addEventListener("selectionchange", handleSelectionChange); document.addEventListener("keydown", handleKeyDown); return () => { document.removeEventListener("click", handleClickOutside); document.removeEventListener("selectionchange", handleSelectionChange); document.removeEventListener("keydown", handleKeyDown); }; }, [editorRef]); return ( <button type="button" className={`${Styles.customSelectFormat} ${ isDisable ? Styles.disabledButton : "" }`} onClick={toggleSelect} ref={selectRef} disabled={isDisable} title="Line Height" > <div className={Styles.selectSelected}> {selectedOption} <ArrowDown /> </div> {isOpen && ( <div className={Styles.selectItemsFormat} style={{ maxHeight: "200px", overflowY: "auto" }} > {lineHeights.map((option, index) => ( <div key={`key${index}`} onClick={(e) => handleOptionClick(e, option)} className={Styles.selectOption} style={{ fontSize: "14px" }} > {option.label} </div> ))} </div> )} </button> ); } export default LineHeight;