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

1,466 lines (1,340 loc) 87.2 kB
import React, { useEffect, useRef, useState } from "react"; import "./css/style.css"; import Styles from "./css/style.module.css"; import { AlignCenter, AlignJustify, AlignLeft, AlignRight, BoldIcon, ClearFormatting, CodeIcon, DecreaseIndentIcon, FullscreenExit, FullscreenIcon, HorizontalLineIcon, ImageIcon, IncreaseIndentIcon, ItalicIcon, LTRIcon, LinkIcon, OrderdList, RTLIcon, RedoIcon, SelectAll, SpecialCharIcon, SubscriptIcon, SuperscriptIcon, UnderlineIcon, UndoIcon, UnorderdList, VideoIcon, } from "./components"; import SpecialChars from "./components/SpecialChars"; import SelectFormat from "./components/SelectFormat"; import ButtonFunction from "./components/ButtonFunction"; import SelectInsert from "./components/SelectInsert"; import SelectView from "./components/SelectView"; import SelectFileOptions from "./components/SelectFileOptions"; import ViewSourceModel from "./components/ViewSourceModel"; import PreviewModel from "./components/PreviewModel"; import SelectFormations from "./components/SelectFormations"; import ManageColors from "./components/ManageColors"; import SimpleButton from "./components/SimpleButton"; import { NAVBAR_ITEMS, remove_resizer, TOOLBAR_ITEMS, transformHTML, } from "./components/constant"; import ViewLoadingModel from "./components/ViewLoadingModel"; import PasteIcon from "./components/SVGImages/PasteIcon"; import CutIcon from "./components/SVGImages/CutIcon"; import CopyIcon from "./components/SVGImages/CopyIcon"; import LinkModal from "./components/LinkModal"; import ImageModal from "./components/ImageModal"; import MediaModal from "./components/MediaModal"; import Modal from "./components/Model"; import RightClickLinkPopup from "./components/RightClickLinkPopup"; import SelectFontFamily from "./components/SelectFontFamily"; import AlignmentOptions from "./components/AlignmentOptions"; import FontSize from "./components/FontSize"; import LineHeight from "./components/LineHeight"; import "react-image-crop/dist/ReactCrop.css"; import { CheckAccessDataApi } from "./DAL/CheckAcces"; import { getBaseDomain } from "./utils/Constants"; import { use } from "react"; const show_final_options = (options, remove, all_options) => { if (!options) { options = all_options; } if (remove) { options = options.filter((item) => { if (typeof item === "string") { return !remove.includes(item); } else { return !remove.includes(item.name); } }); // remove | dublication options = options.filter((item, index) => { return item !== "|" || index === 0 || options[index - 1] !== "|"; }); } return options; }; const isValidURL = (str) => { try { // Try to create a URL object - most reliable method new URL(str); return true; } catch (e) { // If URL constructor fails, try regex pattern const pattern = new RegExp( "^(https?://)?" + // protocol "((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|" + // domain name "((\\d{1,3}\\.){3}\\d{1,3}))" + // OR ip (v4) address "(:\\d+)?(/[-a-z\\d%_.~+]*)*" + // port and path "(\\?[;&a-z\\d%_.~+=-]*)?" + // query string "(\\#[-a-z\\d_]*)?$", "i", ); return pattern.test(str); } }; export default function ReactEditorKit(props) { let { theme_config, toolbar, navbar, value, onChange, getEditorRef, mainProps, placeholder, image_handler, handleFullScreen, remove_from_toolbar, remove_from_navbar, style, apiKey, height, ...others } = props; const editorRef = useRef(null); const [viewSource, setViewSource] = useState(false); const [isLoading, setIsLoading] = useState(false); const [openPreview, setOpenPreview] = useState(false); const [init, setInit] = useState(false); const [sourceCode, setSourceCode] = useState(null); const [isFullScreen, setIsFullScreen] = useState(false); const [imageUrl, setImageUrl] = useState(""); const [isOpenModel, setIsOpenModel] = useState(""); const [targetElement, setTargetElement] = useState(null); const [targetElementType, setTargetElementType] = useState(null); const [previewContent, setPreviewContent] = useState(""); const [isDisable, setIsDisable] = useState(false); const [allowPaste, setAllowPaste] = useState(true); const [selectedData, setSelectedData] = useState({ link: "", height: "", width: "", type: "general", text: "", open_new_tab: false, }); const [selectedEvent, setSelectedEvent] = useState(null); const [isPlaceholder, setIsPlaceholder] = useState(true); const [selectedItem, setSelectedItem] = useState({}); const [selectedRange, setSelectedRange] = useState(null); const [showHR1, setShowHR1] = useState(false); const [showHR2, setShowHR2] = useState(false); const [showHR3, setShowHR3] = useState(false); const checkIfImageExists = () => { const editor = editorRef?.current; if (editor) { const imgTag = editor.querySelector("img"); if (imgTag) { return true; } else { return false; } } return false; }; const handleInput = () => { console.log("Input event triggered"); //if isDisable is true set editor content to empty and contentEditable to false if (isDisable && editorRef.current) { editorRef.current.innerHTML = ""; editorRef.current.setAttribute("contentEditable", "false"); return; } setInit(true); const editor = editorRef?.current; if (!editor) return; let isImageExists = checkIfImageExists(); if (!isImageExists) { let element = document.querySelector(".resizeImageWrapper"); if (element) { element.parentNode.removeChild(element); } setSelectedEvent(null); } // --- CAREFUL WRAPPING OF FIRST LINE --- const hasOnlyTextOrBR = [...editor.childNodes].every( (node) => node.nodeType === Node.TEXT_NODE || (node.nodeType === Node.ELEMENT_NODE && node.nodeName === "BR"), ); if (hasOnlyTextOrBR && editor.textContent.trim() !== "") { const selection = window.getSelection(); const range = selection?.getRangeAt(0); // Save caret position relative to start const preCaretRange = range?.cloneRange(); preCaretRange?.selectNodeContents(editor); preCaretRange?.setEnd(range.startContainer, range.startOffset); const caretOffset = preCaretRange?.toString().length || 0; // Wrap text in a <p> const wrapper = document.createElement("p"); while (editor.firstChild) { wrapper.appendChild(editor.firstChild); } editor.appendChild(wrapper); // Restore caret let newNode = wrapper.firstChild; let offset = caretOffset; // Traverse to the correct offset const setCaret = (node, offset) => { const range = document.createRange(); const selection = window.getSelection(); range.setStart(node, offset); range.collapse(true); selection.removeAllRanges(); selection.addRange(range); }; const traverse = (node, depth = 0) => { // Prevent infinite recursion if (depth > 100) { console.warn("Maximum traversal depth reached"); return; } if (node.nodeType === Node.TEXT_NODE) { if (offset <= node.length) { setCaret(node, offset); throw "done"; // stop traversal } else { offset -= node.length; } } else { for (let i = 0; i < node.childNodes.length; i++) { traverse(node.childNodes[i], depth + 1); } } }; try { traverse(wrapper); } catch (e) {} } function clearEditorFormatting(editor, tempDiv) { // Always clear to truly empty string editor.setAttribute("data-mlx-editor-empty", "true"); // Save current selection const selection = window.getSelection(); const range = document.createRange(); // Completely clear the editor range.selectNodeContents(editor); range.collapse(true); selection.removeAllRanges(); selection.addRange(range); editor.innerHTML = ""; document.execCommand("removeFormat", false, null); // Reset any formatting // Optional: Reset any CSS styles editor.style.cssText = ""; } // Usage: let content = editor.innerHTML; content = transformHTML(content); const tempDiv = document.createElement("div"); tempDiv.innerHTML = content; let checkEditorIsEmptyAndGetTagName = isEditorEmpty(content); // First check if the content is "effectively empty" // Allow spaces at the beginning - don't strip them when checking for empty content const hasOnlySpaces = tempDiv.textContent && tempDiv.textContent.replace(/[\s\u00A0]/g, "").length === 0 && tempDiv.textContent.length > 0; const isEffectivelyEmpty = (!tempDiv.textContent || tempDiv.textContent.length === 0) && checkEditorIsEmptyAndGetTagName.isEmpty; if (isEffectivelyEmpty) { clearEditorFormatting(editor, checkEditorIsEmptyAndGetTagName.tempDiv); onChange?.(""); } else { editor.removeAttribute("data-mlx-editor-empty"); onChange?.(tempDiv.innerHTML); } // Update placeholder state after content changes handlePlaceholder(); }; function isEditorEmpty(html) { // Create a temporary DOM element to parse and inspect the structure const temp = document.createElement("div"); temp.innerHTML = html; // Remove only completely empty text nodes, but preserve those with spaces temp.childNodes.forEach((node) => { if (node.nodeType === Node.TEXT_NODE && node.textContent === "") { temp.removeChild(node); } }); // Case 1: Only a single <br> or empty if ( temp.children.length === 0 || temp.innerHTML === "<br>" || temp.innerHTML === "" ) { let pTagDiv = document.createElement("P"); pTagDiv.innerHTML = "<br>"; return { tempDiv: pTagDiv, isEmpty: true }; } // Case 2: Only a <p> with a <br> inside (ignore styles) if ( temp.children.length === 1 && temp.children[0].children.length === 1 && temp.children[0].children[0].tagName === "BR" ) { return { tempDiv: temp.children[0], isEmpty: true }; } // Case 3: Only a <p>&nbsp;</p>, <p> </p> (unicode nbsp), or <p> </p> (treat as empty) if (temp.children.length === 1 && temp.children[0].tagName === "P") { const inner = temp.children[0].innerHTML; // Check for HTML &nbsp;, unicode nbsp, or only whitespace if (inner === "&nbsp;" || inner === "\u00A0") { let pTagDiv = document.createElement("P"); pTagDiv.innerHTML = "<br>"; return { tempDiv: pTagDiv, isEmpty: true }; } } return { isEmpty: false }; } // Helper function to clone formatting from a text node's parent elements const cloneFormatting = (textNode, textContent) => { const formattingElements = [ "B", "STRONG", "I", "EM", "U", "S", "STRIKE", "SUB", "SUP", "SPAN", ]; let currentNode = textNode.parentNode; let formattedElement = document.createTextNode(textContent); // Traverse up the DOM tree to find formatting elements while (currentNode && currentNode !== editorRef.current) { if (formattingElements.includes(currentNode.nodeName)) { const clonedElement = currentNode.cloneNode(false); // Clone without children clonedElement.appendChild(formattedElement); formattedElement = clonedElement; } currentNode = currentNode.parentNode; } return formattedElement; }; const handleEditorKeyDown = (event) => { if (event.key === "Enter" && !event.shiftKey) { event.preventDefault(); const editor = editorRef.current; const selection = window.getSelection(); if (!selection.rangeCount || !editor) return; const range = selection.getRangeAt(0); let currentNode = range.startContainer; let listItemNode = null; // Traverse up to find the nearest LI element while (currentNode && currentNode !== editor) { if (currentNode.nodeName === "LI") { listItemNode = currentNode; break; } currentNode = currentNode.parentNode; } if (listItemNode) { // Create new LI const newLI = document.createElement("li"); newLI.appendChild(document.createElement("br")); // Insert after current LI if (listItemNode.nextSibling) { listItemNode.parentNode.insertBefore(newLI, listItemNode.nextSibling); } else { listItemNode.parentNode.appendChild(newLI); } // Move cursor into new LI const selection = window.getSelection(); const range = document.createRange(); range.selectNodeContents(newLI); range.collapse(true); selection.removeAllRanges(); selection.addRange(range); return; } // Step 1: Find the containing block element (P, H1, H2, etc.) let currentBlock = null; let container = range.startContainer; // Traverse up to find existing block element while (container && container !== editor) { if ( container.nodeName === "P" || container.nodeName === "H1" || container.nodeName === "H2" || container.nodeName === "H3" || container.nodeName === "H4" || container.nodeName === "H5" || container.nodeName === "H6" || container.nodeName === "DIV" ) { currentBlock = container; break; } container = container.parentNode; } // If no block element found, create a paragraph if (!currentBlock) { currentBlock = document.createElement("p"); // Move all content into the paragraph while (editor.firstChild) { currentBlock.appendChild(editor.firstChild); } editor.appendChild(currentBlock); // Set cursor at the end of the new paragraph const newRange = document.createRange(); newRange.selectNodeContents(currentBlock); newRange.collapse(false); selection.removeAllRanges(); selection.addRange(newRange); return; } // Step 2: Determine what tag to create for the new line let newTagName = "p"; // Default to paragraph // Check if current block is a heading tag if ( currentBlock.nodeName === "H1" || currentBlock.nodeName === "H2" || currentBlock.nodeName === "H3" || currentBlock.nodeName === "H4" || currentBlock.nodeName === "H5" || currentBlock.nodeName === "H6" ) { // Check if there's text after the cursor position const offset = range.startOffset; const textContainer = range.startContainer; let hasTextAfterCursor = false; if (textContainer.nodeType === Node.TEXT_NODE) { const text = textContainer.nodeValue; const afterText = text.slice(offset); hasTextAfterCursor = afterText.trim() !== ""; } else { // If cursor is at element boundary, check if there are more text nodes after let nextNode = textContainer.nextSibling; while (nextNode) { if ( nextNode.nodeType === Node.TEXT_NODE && nextNode.textContent.trim() !== "" ) { hasTextAfterCursor = true; break; } nextNode = nextNode.nextSibling; } } if (hasTextAfterCursor) { // If there's text after cursor, create same type of heading newTagName = currentBlock.nodeName.toLowerCase(); } else { // If no text after cursor, always create paragraph newTagName = "p"; } } // Step 3: Split the block at cursor position const offset = range.startOffset; const textContainer = range.startContainer; // Create new block element for the split content const newBlock = document.createElement(newTagName); if (textContainer.nodeType === Node.TEXT_NODE) { const text = textContainer.nodeValue; const beforeText = text.slice(0, offset); const afterText = text.slice(offset); // Update current text node with text before cursor textContainer.nodeValue = beforeText; // Add text after cursor to new block with preserved formatting if (afterText.trim()) { // Clone the formatting from the current text node's parent elements const formattedText = cloneFormatting(textContainer, afterText); newBlock.appendChild(formattedText); } else { newBlock.appendChild(document.createElement("br")); } } else { // If cursor is at element boundary, just add a br newBlock.appendChild(document.createElement("br")); } // Step 4: Insert new block after currecnt one const parent = currentBlock.parentNode; if (parent && parent.contains(currentBlock)) { if (currentBlock.nextSibling) { parent.insertBefore(newBlock, currentBlock.nextSibling); } else { parent.appendChild(newBlock); } } // Step 5: Position cursor at start of new block const newRange = document.createRange(); if ( newBlock.firstChild && newBlock.firstChild.nodeType === Node.TEXT_NODE ) { newRange.setStart(newBlock.firstChild, 0); } else { newRange.setStart(newBlock, 0); } newRange.collapse(true); selection.removeAllRanges(); selection.addRange(newRange); } }; const handleOpenModel = (e, type, item) => { if (isDisable) { return; } e.preventDefault(); setIsOpenModel(type); setSelectedItem(item); }; const handleCloseModel = (e) => { if (e) { e.preventDefault(); } setImageUrl(""); setTargetElement(""); setIsOpenModel(""); setSelectedData({}); setSelectedEvent(null); }; const handleSaveSource = (e) => { e.preventDefault(); if (editorRef?.current) { const trimmedSourceCode = sourceCode .replace(/\n\s*\n/g, "\n") .replace(/\s+/g, " ") .trim(); editorRef.current.innerHTML = trimmedSourceCode; setViewSource(false); if (onChange) { onChange(trimmedSourceCode); } // Update placeholder after setting content (ensure DOM is updated first) setTimeout(() => { handlePlaceholder(); }, 1000); } }; const handleSelectAll = (e) => { if (isDisable) { return; } e.preventDefault(); if (!editorRef?.current) { console.warn("Editor not available for select all"); return; } try { const selection = window.getSelection(); if (!selection.toString()) { const range = document.createRange(); range.selectNodeContents(editorRef.current); selection.removeAllRanges(); selection.addRange(range); } else { selection.removeAllRanges(); } } catch (error) { console.warn("Select all failed:", error); } }; const handleInsertHRClick = () => { if (isDisable) { return; } handleFocusEditor(); document.execCommand("insertHorizontalRule"); }; const handleFocusEditor = () => { const editor = editorRef?.current; if (editor && selectedRange) { const selection = window.getSelection(); selection.removeAllRanges(); selection.addRange(selectedRange); editor.focus(); } }; const handleLinkInsert = (props) => { let { text, link, open_new_tab, link_type } = props; if (!text) { text = link; } let linkHTML = `<a href="${link}"`; if (open_new_tab && open_new_tab !== "false") { linkHTML += ' target="_blank"'; } if (link_type === "image" && imageUrl) { // Use selectedEvent if available; otherwise, fall back to default image HTML if (selectedEvent) { if (selectedEvent.tagName === "IMG") { let src = selectedEvent.src; if (src === imageUrl) { text = selectedEvent.outerHTML; } else { text = `<img src="${imageUrl}" alt="ImageLink" />`; } } else if (selectedEvent.tagName === "A") { let childNode = selectedEvent.firstChild; if ( childNode && childNode.nodeType === Node.ELEMENT_NODE && childNode.tagName === "IMG" && childNode.src === imageUrl ) { text = childNode.outerHTML; } else { text = `<img src="${imageUrl}" alt="ImageLink" />`; } } else { text = `<img src="${imageUrl}" alt="ImageLink" />`; } } else { text = `<img src="${imageUrl}" alt="ImageLink" />`; } } else if (link_type === "button") { text = `<button contentEditable=false>${text}</button>`; } linkHTML += `>${text}</a>`; if (selectedEvent && selectedEvent.parentElement) { selectedEvent.parentElement.removeChild(selectedEvent); } else { restoreSelection(); } handleFocusEditor(); document.execCommand("insertHTML", false, linkHTML); handleCloseModel(); }; const handleRemoveLink = () => { let linkHTML = `${selectedEvent.textContent.trim()}`; if (selectedEvent.tagName === "IMG") { linkHTML = `<img src="${selectedEvent.src}" alt="ImageLink" />`; } if (selectedEvent) { const parentElement = selectedEvent.parentElement; if (parentElement) { parentElement.removeChild(selectedEvent); } } handleFocusEditor(); document.execCommand("insertHTML", false, linkHTML); handleCloseModel(); }; const handleImageInsert = (data) => { let { link, height, width } = data; if (selectedEvent && selectedData) { selectedEvent.src = link; if (height) { selectedEvent.style.height = `${height}px`; } else { selectedEvent.style.height = null; } if (width) { selectedEvent.style.width = `${width}px`; } else { selectedEvent.style.width = null; } setTimeout(() => { setIsLoading(false); handleCloseModel(); // Update placeholder after image update handlePlaceholder(); }, 0); } else { handleFocusEditor(); let imgElement = `<img src="${link}" alt="Image" style="`; if (height) { imgElement += `height:${height}px;`; } if (width) { imgElement += `width:${width}px;`; } imgElement += `"/>`; document.execCommand("insertHTML", false, imgElement); // Ensure editor picks up the change const editorElement = document.querySelector('[contenteditable="true"]'); if (editorElement) { editorElement.dispatchEvent(new Event("input", { bubbles: true })); } setIsLoading(false); handleCloseModel(); // Update placeholder after image insertion handlePlaceholder(); } }; const handleMediaInsert = (data, targetElement) => { let { link, height, width, type, embed_code } = data; const editorNode = editorRef.current; let iframeHTML = ""; if (type === "general") { // Check if it's a direct video link if (link.match(/\.(mp4|mov|avi|wmv|webm|mkv|flv)$/i)) { iframeHTML = `<video width="${width || "640"}" height="${ height || "360" }" controls><source src="${link}" type="video/mp4"></video>`; } else { // Check for specific video platforms const youtubeRegex = /(?:https?:\/\/)?(?:www\.)?(?:youtube\.com\/(?:[^\/\n\s]+\/\S+\/|(?:v|e(?:mbed)?)\/|\S*?[?&]v=)|youtu\.be\/)([a-zA-Z0-9_-]{11})/; const vimeoRegex = /(?:https?:\/\/)?(?:www\.)?vimeo.com\/(\d+)/; const dailymotionRegex = /(?:https?:\/\/)?(?:www\.)?dailymotion\.com\/video\/([a-z0-9]+)/i; const twitchRegex = /(?:https?:\/\/)?(?:www\.)?twitch\.tv\/([a-zA-Z0-9_]+)/; if (link.match(youtubeRegex)) { const videoId = link.match(youtubeRegex)[1]; iframeHTML = `<iframe width="${width || "640"}" height="${ height || "360" }" src="https://www.youtube.com/embed/${videoId}" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>`; } else if (link.match(vimeoRegex)) { const videoId = link.match(vimeoRegex)[1]; iframeHTML = `<iframe src="https://player.vimeo.com/video/${videoId}" width="${ width || "640" }" height="${ height || "360" }" frameborder="0" allow="autoplay; fullscreen; picture-in-picture" allowfullscreen></iframe>`; } else if (link.match(dailymotionRegex)) { const videoId = link.match(dailymotionRegex)[1]; iframeHTML = `<iframe width="${width || "640"}" height="${ height || "360" }" src="https://www.dailymotion.com/embed/video/${videoId}" frameborder="0" allow="autoplay; fullscreen" allowfullscreen></iframe>`; } else if (link.match(twitchRegex)) { const channelName = link.match(twitchRegex)[1]; iframeHTML = `<iframe src="https://twitch.tv/embed/${channelName}/chat?parent=example.com" width="${width || "640"}" height="${ height || "360" }" frameborder="0" allow="autoplay; fullscreen" allowfullscreen></iframe>`; } else if (isValidURL(link)) { // For any other valid URL, create an iframe iframeHTML = `<iframe width="${width || "640"}" height="${ height || "360" }" src="${link}" frameborder="0" allow="autoplay; fullscreen; picture-in-picture" allowfullscreen></iframe>`; } else { // Insert custom embed code if available iframeHTML = embed_code || ""; } } } else if (type === "embed" && embed_code && editorNode) { iframeHTML = embed_code; } if (targetElement && editorNode && iframeHTML) { handleFocusEditor(); targetElement.parentNode.setAttribute("data-mtl-link-type", type); targetElement.outerHTML = iframeHTML; // Trigger change event after updating content setTimeout(() => { const tempDiv = document.createElement("div"); tempDiv.innerHTML = editorNode.innerHTML; onChange?.(tempDiv.innerHTML); handlePlaceholder(); }, 0); } else if (editorNode && iframeHTML) { const wrapper = createIframeWrapperWithSettings( iframeHTML, type, (wrapper) => { let iframe_element = wrapper.querySelector("iframe"); setTargetElement(iframe_element); setTargetElementType( wrapper.getAttribute("data-mtl-link-type") || "general", ); setIsOpenModel("video"); }, ); handleFocusEditor(); // document.execCommand("insertHTML", false, wrapper.outerHTML); const selection = window.getSelection(); if (!selection.rangeCount) return; const range = selection.getRangeAt(0); range.deleteContents(); range.insertNode(wrapper); // Move cursor after the inserted element range.setStartAfter(wrapper); range.setEndAfter(wrapper); selection.removeAllRanges(); selection.addRange(range); // Trigger change event after inserting content setTimeout(() => { const tempDiv = document.createElement("div"); tempDiv.innerHTML = editorNode.innerHTML; onChange?.(tempDiv.innerHTML); handlePlaceholder(); }, 0); } setTargetElement(null); setIsOpenModel(""); // Assuming this is setting some state related to the modal }; function createIframeWrapperWithSettings(iframeHTML, type, onSettingsClick) { const iframeElement = document.createElement("div"); iframeElement.innerHTML = iframeHTML; // Create wrapper const wrapper = document.createElement("div"); wrapper.className = "iframe-wrapper"; wrapper.contentEditable = "true"; wrapper.setAttribute("data-mtl-link-type", type); const overlay = document.createElement("div"); overlay.className = "iframe-overlay"; const settingsBtn = document.createElement("button"); settingsBtn.className = "iframe-settings-btn"; settingsBtn.contentEditable = "false"; settingsBtn.innerHTML = `<div style="display: flex; align-items: center;"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="currentColor" d="M10.275 22q-.425 0-.75-.275t-.375-.7l-.3-2.225q-.325-.125-.612-.3t-.563-.375l-1.55.65q-.625.275-1.25.05t-.975-.8l-1.175-2.05q-.35-.575-.2-1.225t.675-1.075l1.325-1Q4.5 12.5 4.5 12.337v-.675q0-.162.025-.337l-1.325-1Q2.675 9.9 2.525 9.25t.2-1.225L3.9 5.975q.35-.575.975-.8t1.25.05l1.55.65q.275-.2.575-.375t.6-.3l.2-1.65q.075-.675.575-1.113T10.8 2h2.4q.675 0 1.175.438t.575 1.112l.2 1.65q.325.125.613.3t.562.375l1.5-.65q.625-.275 1.263-.05t.987.8l1.175 2.05q.35.575.213 1.225t-.663 1.075L19.125 11.6q-.275.2-.562.3t-.638.1h-2.35q0-1.45-1.037-2.475T12.05 8.5q-1.475 0-2.488 1.013T8.55 12q0 1.2.688 2.1T11 15.35v5.8q0 .35-.2.6t-.525.25M20 22h-6q-.425 0-.712-.288T13 21v-6q0-.425.288-.712T14 14h6q.425 0 .713.288T21 15v2l1.575-1.575q.125-.125.275-.062t.15.237v4.8q0 .175-.15.238t-.275-.063L21 19v2q0 .425-.288.713T20 22"/></svg> <span style="margin-inline: 8px;">Settings</span></div>`; settingsBtn.addEventListener("click", (e) => { e.stopPropagation(); if (typeof onSettingsClick === "function") { onSettingsClick(wrapper); } }); // Build DOM structure wrapper.appendChild(overlay); wrapper.appendChild(settingsBtn); // Add null check for iframe element const iframeChild = iframeElement.childNodes[0]; if (iframeChild) { wrapper.appendChild(iframeChild); } else { console.warn("No iframe element found to append"); } return wrapper; } const handlePrint = () => { if (!editorRef.current) { console.warn("Editor not available for printing"); return; } let data = editorRef.current.innerHTML; const iframe = document.createElement("iframe"); iframe.style.display = "none"; document.body.appendChild(iframe); const iframeDoc = iframe.contentDocument || iframe.contentWindow.document; try { iframeDoc.write(data); iframeDoc.close(); iframe.contentWindow.print(); } catch (error) { console.error("Print failed:", error); } setTimeout(() => { if (document.body.contains(iframe)) { document.body.removeChild(iframe); } }, 100); }; const handleCharSelect = (e, char) => { e.preventDefault(); if (editorRef?.current !== null) { handleFocusEditor(); document.execCommand("insertHTML", false, char); setTargetElement(null); setIsOpenModel(""); // Update placeholder after character insertion handlePlaceholder(); } }; const cleanHTML = (html) => { // Create a temporary div to parse HTML const tempDiv = document.createElement("div"); tempDiv.innerHTML = html; // Function to clean styles from an element const cleanStyles = (element) => { if (element.style) { // Get computed styles const color = element.style.color; const fontWeight = element.style.fontWeight; const fontStyle = element.style.fontStyle; const textDecoration = element.style.textDecoration; const fontSize = element.style.fontSize; const fontFamily = element.style.fontFamily; // Clear all styles element.removeAttribute("style"); // Re-apply only the styles we want to keep if (color) element.style.color = color; if (fontWeight) element.style.fontWeight = fontWeight; if (fontStyle) element.style.fontStyle = fontStyle; if (textDecoration) element.style.textDecoration = textDecoration; if (fontSize) element.style.fontSize = fontSize; if (fontFamily) element.style.fontFamily = fontFamily; } // Recursively clean child elements Array.from(element.children).forEach((child) => cleanStyles(child)); }; // Clean all elements Array.from(tempDiv.children).forEach((element) => cleanStyles(element)); return tempDiv.innerHTML; }; const onPaste = (event) => { event.preventDefault(); // Check if modern clipboard API is available and in secure context if ( navigator.clipboard && navigator.clipboard.read && window.isSecureContext ) { navigator.clipboard .read() .then((clipboardItems) => { clipboardItems.forEach((item) => { if ( item.types.includes("image/png") || item.types.includes("image/jpeg") ) { item .getType(item.types[0]) .then((imageBlob) => { const imgElement = `<img src="${URL.createObjectURL( imageBlob, )}" alt="Image">`; document.execCommand("insertHTML", false, imgElement); }) .catch((error) => { console.error("Error reading image content:", error); }); } else if (item.types.includes("text/html")) { item .getType("text/html") .then((htmlBlob) => { htmlBlob .text() .then((htmlContent) => { const cleanedHTML = cleanHTML(htmlContent); const withoutComments = cleanedHTML.replace( /<!--[\s\S]*?-->/g, "", ); document.execCommand( "insertHTML", false, withoutComments, ); }) .catch((error) => { console.error("Error reading HTML content:", error); }); }) .catch((error) => { console.error( "Error getting HTML type from ClipboardItem:", error, ); }); } else if (item.types.includes("text/plain")) { item .getType("text/plain") .then((textBlob) => { textBlob .text() .then((text) => { if (isValidURL(text)) { // Insert the URL as a link const linkElement = `<a href="${text}" target="_blank">${text}</a>`; document.execCommand("insertHTML", false, linkElement); } else { // Insert plain text document.execCommand("insertText", false, text); } }) .catch((error) => { console.error("Error reading text content:", error); }); }) .catch((error) => { console.error( "Error getting text type from ClipboardItem:", error, ); }); } }); }) .catch((error) => { console.error("Error reading clipboard:", error); // Fallback to traditional paste handling fallbackPasteHandler(event); }); } else { // Fallback for non-secure contexts or when modern API is not available fallbackPasteHandler(event); } }; const fallbackPasteHandler = (event) => { try { // Use the traditional clipboardData API as fallback const clipboardData = event.clipboardData || window.clipboardData; if (clipboardData) { // Check for text data const text = clipboardData.getData("text/plain") || clipboardData.getData("text"); const html = clipboardData.getData("text/html"); if (html) { // Process HTML content const cleanedHTML = cleanHTML(html); const withoutComments = cleanedHTML.replace(/<!--[\s\S]*?-->/g, ""); document.execCommand("insertHTML", false, withoutComments); } else if (text) { // Process plain text if (isValidURL(text)) { // Insert the URL as a link const linkElement = `<a href="${text}" target="_blank">${text}</a>`; document.execCommand("insertHTML", false, linkElement); } else { // Insert plain text document.execCommand("insertText", false, text); } } // Check for files (images) const files = clipboardData.files; if (files && files.length > 0) { for (let i = 0; i < files.length; i++) { const file = files[i]; if (file.type.startsWith("image/")) { const imgElement = `<img src="${URL.createObjectURL( file, )}" alt="Image">`; document.execCommand("insertHTML", false, imgElement); } } } } else { console.warn( "No clipboard data available, trying navigator.clipboard.readText as fallback", ); // Try navigator.clipboard.readText() for text-only content if (navigator.clipboard && navigator.clipboard.readText) { navigator.clipboard .readText() .then((text) => { if (text) { if (isValidURL(text)) { const linkElement = `<a href="${text}" target="_blank">${text}</a>`; document.execCommand("insertHTML", false, linkElement); } else { document.execCommand("insertText", false, text); } } }) .catch((error) => { console.warn("navigator.clipboard.readText failed:", error); }); } } } catch (error) { console.warn("Fallback paste handler failed:", error); // Try one more fallback with readText if (navigator.clipboard && navigator.clipboard.readText) { navigator.clipboard .readText() .then((text) => { if (text) { if (isValidURL(text)) { const linkElement = `<a href="${text}" target="_blank">${text}</a>`; document.execCommand("insertHTML", false, linkElement); } else { document.execCommand("insertText", false, text); } } }) .catch((err) => { console.warn("All paste methods failed:", err); }); } } }; const handleBlur = () => { handleSelection(); }; const handleNewDocument = () => { if (editorRef.current) { editorRef.current.innerHTML = ""; // Update placeholder after clearing content handlePlaceholder(); } }; const handlePreview = () => { setPreviewContent(editorRef?.current.innerHTML); setOpenPreview(true); }; const handleViewSource = () => { if (isDisable) { return; } if (!viewSource && editorRef?.current) { const content = editorRef?.current.innerHTML; const formattedContent = transformHTML(content); setSourceCode(formattedContent); } else { setSourceCode(""); } setViewSource(!viewSource); }; const toggleFullScreen = () => { if (isDisable) { return; } setIsFullScreen(!isFullScreen); }; const handleKeyDown = (event) => { if (event.key === "Escape") { setIsFullScreen(false); } }; const handlePlaceholder = () => { const editor = editorRef?.current; if (!editor) { return; } // Check both innerText and innerHTML to handle all content types const hasTextContent = editor.innerText.trim() !== ""; const hasHTMLContent = editor.innerHTML.trim() !== "" && editor.innerHTML.trim() !== "<br>" && editor.innerHTML.trim() !== "<p><br></p>"; const hasContent = hasTextContent || hasHTMLContent; if (!hasContent) { editor.classList.add("empty"); setIsPlaceholder(true); } else { editor.classList.remove("empty"); setIsPlaceholder(false); } }; const handleSelection = () => { try { const selection = window.getSelection(); if (selection && selection.rangeCount > 0) { setSelectedRange(selection.getRangeAt(0)); } } catch (error) { console.warn("Failed to handle selection:", error); } }; const restoreSelection = () => { if (selectedRange) { try { const selection = window.getSelection(); selection.removeAllRanges(); selection.addRange(selectedRange); } catch (error) { console.warn("Failed to restore selection:", error); // Clear invalid selection setSelectedRange(null); } } }; useEffect(() => { const handleFullScreenChange = () => { setIsFullScreen(document.fullscreenElement !== null); }; handlePlaceholder(); document.addEventListener("keydown", handleKeyDown); document.addEventListener("input", handlePlaceholder); document.addEventListener("fullscreenchange", handleFullScreenChange); return () => { document.removeEventListener("keydown", handleKeyDown); document.removeEventListener("input", handlePlaceholder); document.removeEventListener("fullscreenchange", handleFullScreenChange); }; }, []); const model_component = () => { if (isOpenModel === "link") { return { component: ( <LinkModal onLinkInsert={handleLinkInsert} item={selectedItem} setIsOpenModel={setIsOpenModel} selectedData={selectedData} imageUrl={imageUrl} setImageUrl={setImageUrl} image_handler={image_handler} setIsLoading={setIsLoading} /> ), title: `${selectedData?.link ? "Update" : "Insert"} Link`, }; } else if (isOpenModel === "image") { return { component: ( <ImageModal onImageInsert={handleImageInsert} item={selectedItem} setIsLoading={setIsLoading} setIsOpenModel={setIsOpenModel} image_handler={image_handler} selectedData={selectedData} /> ), title: `${selectedData?.link ? "Update" : "Insert"} Image`, }; } else if (isOpenModel === "video") { return { component: ( <MediaModal onMediaInsert={handleMediaInsert} targetElement={targetElement} targetElementType={targetElementType} /> ), title: `${selectedData?.link ? "Update" : "Insert"} Video`, }; } else if (isOpenModel === "special_char") { return { component: <SpecialChars handleCharSelect={handleCharSelect} />, title: "Insert Special Characters", }; } }; if (theme_config && Object.keys(theme_config).length > 0) { Object.keys(theme_config).forEach(function (key, index) { document.documentElement.style.setProperty( `--editor-${key}`, theme_config[key], ); }); } toolbar = show_final_options(toolbar, remove_from_toolbar, TOOLBAR_ITEMS); navbar = show_final_options(navbar, remove_from_navbar, NAVBAR_ITEMS); useEffect(() => { if (!init) { if (editorRef.current && value) { editorRef.current.innerHTML = value; setInit(true); // Update placeholder after setting initial content setTimeout(() => handlePlaceholder(), 0); } } if (!value && editorRef.current) { editorRef.current.innerHTML = ""; // Update placeholder when content is cleared setTimeout(() => handlePlaceholder(), 0); } if (getEditorRef) { getEditorRef(editorRef); } }, [isFullScreen, editorRef, value]); const handlePaste = (e) => { if (isDisable) { return; } e.preventDefault(); if (!editorRef.current) { setTimeout(() => { if (editorRef.current) { editorRef.current.focus(); } }, 0); } else { restoreSelection(); } // Check if modern clipboard API is available and in secure context if ( navigator.clipboard && navigator.clipboard.read && window.isSecureContext ) { navigator.clipboard .read() .then((clipboardItems) => { clipboardItems.forEach((item) => { if ( item.types.includes("image/png") || item.types.includes("image/jpeg") ) { item .getType(item.types[0]) .then((imageBlob) => { const imgElement = `<img src="${URL.createObjectURL( imageBlob, )}" alt="Image">`; document.execCommand("insertHTML", false, imgElement); }) .catch((error) => { console.error("Error reading image content:", error); }); } else if (item.types.includes("text/html")) { item .getType("text/html") .then((htmlBlob) => { htmlBlob .text() .then((htmlContent) => { const cleanedHTML = cleanHTML(htmlContent); const withoutComments = cleanedHTML.replace( /<!--[\s\S]*?-->/g, "", ); document.execCommand( "insertHTML", false, withoutComments, ); }) .catch((error) => { console.error("Error reading HTML content:", error); }); }) .catch((error) => { console.error( "Error getting HTML type from ClipboardItem:", error, ); }); } else if (item.types.includes("text/plain")) { item .getType("text/plain") .then((textBlob) => { textBlob .text() .then((text) => { if (isValidURL(text)) { // Insert the URL as a link const linkElement = `<a href="${text}" target="_blank">${text}</a>`; document.execCommand("insertHTML", false, linkElement); } else { // Insert plain text document.execCommand("insertText", false, text); } }) .catch((error) => { console.error("Error reading text content:", error); }); }) .catch((error) => { console.error( "Error getting text type from ClipboardItem:", error, ); }); } }); }) .catch((error) => { console.error("Error reading clipboard:", error); // Fallback to traditional paste handling fallbackPasteHandler(e); }); } else { // Fallback for non-secure contexts or when modern API is not available fallbackPasteHandler(e)