diff --git a/src/RichTextEditor.tsx b/src/RichTextEditor.tsx index dc59bae..77e2120 100644 --- a/src/RichTextEditor.tsx +++ b/src/RichTextEditor.tsx @@ -1,13 +1,16 @@ import "./styles.css"; import { + FontSizeIcon, + FontSizeIconDisabled, HighlighterIcon, HighlighterIconDisabled, ListIcon, ListIconDisabled, NumberedListIcon, NumberedListIconDisabled, + TextIcon, } from "./assets"; -import React, { useRef, useEffect } from "react"; +import React, { useRef, useEffect, useState } from "react"; import { HTMLTag, RichTextEditorProps } from "./types"; import { getParentElement, @@ -56,6 +59,15 @@ export const RichTextEditor = (props: RichTextEditorProps) => { } }; + // State to manage popover visibility + const [showHeaderPopover, setShowHeaderPopover] = useState(false); + + // Function to handle header style change and close popover + const handleHeaderStyleChange = (size) => { + toggleHeaderStyle(size); + setShowHeaderPopover(false); // Hide popover + }; + const toggleStyle = (tag: HTMLTag, className?: string) => { const { selection, range } = getSelectionContext(); const styledParentElement = getParentElement({ @@ -117,6 +129,17 @@ export const RichTextEditor = (props: RichTextEditorProps) => { element.appendChild(range?.extractContents()); range?.insertNode(element); } + + // Remove any parent header sizes + if ( + tag.startsWith("H") && + ["H1", "H2", "H3"].includes( + getParentElement({ contentRef })?.nodeName + ) + ) { + getParentElement({ contentRef }).replaceWith(element); + range.setEndAfter(element); + } } // Update content @@ -129,12 +152,13 @@ export const RichTextEditor = (props: RichTextEditorProps) => { selection?.addRange(range); } } + // Maintain focus. contentRef.current?.focus(); }; - // Function to toggle the header (h3) style - const toggleHeaderStyle = () => { + // Function to toggle the header style + const toggleHeaderStyle = (size: "H1" | "H2" | "H3") => { const { currentNode, range } = getSelectionContext(); const selectedText = range?.toString() ?? ""; const currentElement = @@ -149,35 +173,35 @@ export const RichTextEditor = (props: RichTextEditorProps) => { currentElement.outerHTML === "


"; if (selectedText.trim() === "" && isEmptyOrBr) { - // Clear the line and replace with an h3 containing a zero-width space - const h3 = document.createElement("h3"); - h3.innerHTML = "\u200B"; // Zero-width space + // Clear the line and replace with a header containing a zero-width space + const header = document.createElement(size); + header.innerHTML = "\u200B"; // Zero-width space if (currentElement) { - // If the current element is a P that contains only a BR, replace it entirely with the H3 + // If the current element is a P that contains only a BR, replace it entirely with the header if (currentElement.tagName === "P") { - currentElement.replaceWith(h3); + currentElement.replaceWith(header); } else { - // For other cases, just clear the innerHTML and set it to the H3 + // For other cases, just clear the innerHTML and set it to the header currentElement.innerHTML = ""; - currentElement.appendChild(h3); + currentElement.appendChild(header); } } else if (contentRef.current) { - // If there's no current element, append the H3 to the contentRef - contentRef.current.appendChild(h3); + // If there's no current element, append the header to the contentRef + contentRef.current.appendChild(header); } - // Set the cursor inside the new h3 element - setCursorAtStartOfElement(h3); + // Set the cursor inside the new header element + setCursorAtStartOfElement(header); } else if (selectedText.trim() === "") { - // No selection and current line is not empty: insert a blank h3 at the end - const h3 = document.createElement("h3"); - h3.innerHTML = "\u200B"; // Zero-width space - contentRef.current?.appendChild(h3); - setCursorAtStartOfElement(h3); + // No selection and current line is not empty: insert a blank header at the end + const header = document.createElement(size); + header.innerHTML = "\u200B"; // Zero-width space + contentRef.current?.appendChild(header); + setCursorAtStartOfElement(header); } else { - // There is selected text: toggle the h3 tag on the selection - toggleStyle("H3"); + // There is selected text: toggle the header tag on the selection + toggleStyle(size); } // Update content @@ -239,6 +263,54 @@ export const RichTextEditor = (props: RichTextEditorProps) => { contentRef.current?.focus(); }; + // Replaces all headers in selection with p tags + const removeHeadersFromSelection = () => { + const { range } = getSelectionContext(); + if (!range) return; + + // Get the common ancestor container of the selection range + const commonAncestorContainer = range?.commonAncestorContainer; + + if ( + commonAncestorContainer && + commonAncestorContainer?.nodeType === Node.ELEMENT_NODE + ) { + // Get all header elements (h1, h2, h3, etc.) within the common ancestor container + const headers = (commonAncestorContainer as Element).querySelectorAll( + "h1, h2, h3, h4, h5, h6" + ); + + // Iterate over each header and replace it with a paragraph + headers.forEach((header) => { + // Check if the header is the only child of a paragraph + if ( + header.parentNode.nodeName === "P" && + header.parentNode.childNodes.length >= 1 + ) { + // Replace the entire paragraph with a new paragraph containing the header's content + const newP = document.createElement("p"); + newP.innerHTML = header.innerHTML; + header.parentNode.parentNode.replaceChild(newP, header.parentNode); + } else { + // If the header is not the only child of a paragraph, just replace the header with a paragraph + const p = document.createElement("p"); + p.innerHTML = header.innerHTML; + header.parentNode.replaceChild(p, header); + } + }); + } else { + // Single line selection where parent element is header + const parentElement = getParentElement({ contentRef }); + if (["H1", "H2", "H3"].includes(parentElement?.nodeName)) { + const rangeContents = range.extractContents(); + parentElement.replaceWith(rangeContents); + } + } + + // Update content after replacing headers + handleChange(); + }; + // Handle when a key is pressed. const handleKeyDown = (e: React.KeyboardEvent) => { const { currentNode } = getSelectionContext(); @@ -332,9 +404,49 @@ export const RichTextEditor = (props: RichTextEditorProps) => { + + + + + )}