UNPKG

rich-text-editor-lib

Version:

A reusable and responsive rich text editor React component.

272 lines (270 loc) 8.69 kB
// src/RichTextEditor.tsx import { useRef, useEffect, useState } from "react"; import { Bold as LucideBold, Italic as LucideItalic, Underline as LucideUnderline, Strikethrough as LucideStrikethrough, List, ListOrdered, Link2 } from "lucide-react"; import { jsx, jsxs } from "react/jsx-runtime"; var toolbarButtons = [ { cmd: "bold", icon: /* @__PURE__ */ jsx(LucideBold, { size: 18 }), label: "Bold" }, { cmd: "italic", icon: /* @__PURE__ */ jsx(LucideItalic, { size: 18 }), label: "Italic" }, { cmd: "underline", icon: /* @__PURE__ */ jsx(LucideUnderline, { size: 18 }), label: "Underline" }, { cmd: "strikeThrough", icon: /* @__PURE__ */ jsx(LucideStrikethrough, { size: 18 }), label: "Strikethrough" }, { cmd: "insertUnorderedList", icon: /* @__PURE__ */ jsx(List, { size: 18 }), // Bullet List label: "Bullet List" }, { cmd: "insertOrderedList", icon: /* @__PURE__ */ jsx(ListOrdered, { size: 18 }), // Numbered List label: "Numbered List" }, { cmd: "createLink", icon: /* @__PURE__ */ jsx(Link2, { size: 18 }), label: "Link" } // Image upload intentionally omitted ]; var RichTextEditor = ({ name, value, onChange, onBlur, placeholder, error, className, style }) => { const editorRef = useRef(null); const [activeStates, setActiveStates] = useState( {} ); const [showLinkInput, setShowLinkInput] = useState(false); const [linkUrl, setLinkUrl] = useState(""); const [linkText, setLinkText] = useState(""); const savedRangeRef = useRef(null); useEffect(() => { if (editorRef.current && editorRef.current.innerHTML !== value) { editorRef.current.innerHTML = value || ""; } }, [value]); useEffect(() => { const updateToolbarState = () => { const newStates = {}; toolbarButtons.forEach((btn) => { try { newStates[btn.cmd] = document.queryCommandState(btn.cmd); } catch { newStates[btn.cmd] = false; } }); setActiveStates(newStates); }; document.addEventListener("selectionchange", updateToolbarState); return () => document.removeEventListener("selectionchange", updateToolbarState); }, []); const handleCommand = (cmd) => { if (editorRef.current) { editorRef.current.focus(); } if (cmd === "createLink") { let selectedText = ""; const selection = window.getSelection(); if (selection && !selection.isCollapsed) { selectedText = selection.toString(); } if (selection && selection.rangeCount > 0) { savedRangeRef.current = selection.getRangeAt(0).cloneRange(); } else { savedRangeRef.current = null; } setLinkText(selectedText); setShowLinkInput(true); setLinkUrl(""); return; } else if (cmd === "insertUnorderedList" || cmd === "insertOrderedList") { document.execCommand(cmd, false, ""); } else { document.execCommand(cmd, false); } if (editorRef.current) { onChange(editorRef.current.innerHTML); } setTimeout(() => { const newStates = {}; toolbarButtons.forEach((btn) => { try { newStates[btn.cmd] = document.queryCommandState(btn.cmd); } catch { newStates[btn.cmd] = false; } }); setActiveStates(newStates); }, 0); }; const handleInsertLink = () => { if (editorRef.current) { editorRef.current.focus(); const selection = window.getSelection(); if (savedRangeRef.current && selection) { selection.removeAllRanges(); selection.addRange(savedRangeRef.current); } if (selection && !selection.isCollapsed) { document.execCommand("createLink", false, linkUrl); } else if (selection) { const a = document.createElement("a"); a.href = linkUrl; a.target = "_blank"; a.rel = "noopener noreferrer"; a.textContent = linkText || linkUrl; const range = selection.rangeCount > 0 ? selection.getRangeAt(0) : null; if (range) { range.insertNode(a); range.setStartAfter(a); range.collapse(true); selection.removeAllRanges(); selection.addRange(range); } } onChange(editorRef.current.innerHTML); } setShowLinkInput(false); setLinkUrl(""); setLinkText(""); savedRangeRef.current = null; }; const handleCancelLink = () => { setShowLinkInput(false); setLinkUrl(""); if (editorRef.current) { editorRef.current.focus(); } }; const handleInput = (e) => { onChange(e.currentTarget.innerHTML); }; const handleBlur = (e) => { if (onBlur) onBlur(e); onChange(e.currentTarget.innerHTML); }; const handleEditorClick = (e) => { const target = e.target; if (target.tagName === "A") { const anchor = target; if (anchor.href) { e.preventDefault(); window.open(anchor.href, "_blank", "noopener,noreferrer"); } } }; return /* @__PURE__ */ jsxs( "div", { className: `rte-root${className ? ` ${className}` : ""}`, style, children: [ /* @__PURE__ */ jsxs("div", { className: "rte-editor-container", children: [ /* @__PURE__ */ jsx("div", { className: "rte-toolbar", children: toolbarButtons.map((btn) => /* @__PURE__ */ jsx( "button", { type: "button", "aria-label": btn.label, className: `rte-toolbar-btn${activeStates[btn.cmd] ? " rte-active" : ""}`, onMouseDown: (e) => e.preventDefault(), onClick: () => handleCommand(btn.cmd), children: btn.icon }, btn.cmd )) }), /* @__PURE__ */ jsxs("div", { style: { position: "relative" }, className: "w-full max-w-full", children: [ showLinkInput && /* @__PURE__ */ jsxs("div", { className: "rte-link-popup", children: [ /* @__PURE__ */ jsx( "input", { type: "text", className: "", placeholder: "Text", value: linkText, onChange: (e) => setLinkText(e.target.value), autoFocus: true, onKeyDown: (e) => { if (e.key === "Enter") handleInsertLink(); if (e.key === "Escape") handleCancelLink(); } } ), /* @__PURE__ */ jsx( "input", { type: "url", className: "", placeholder: "Enter URL", value: linkUrl, onChange: (e) => setLinkUrl(e.target.value), onKeyDown: (e) => { if (e.key === "Enter") handleInsertLink(); if (e.key === "Escape") handleCancelLink(); } } ), /* @__PURE__ */ jsx( "button", { type: "button", className: "rte-insert-btn", onClick: handleInsertLink, disabled: !linkUrl || !linkText, children: "Insert" } ), /* @__PURE__ */ jsx( "button", { type: "button", className: "rte-cancel-btn", onClick: handleCancelLink, children: "Cancel" } ) ] }), /* @__PURE__ */ jsx( "div", { ref: editorRef, className: `rte-editor-box ${error ? " border-red-500" : ""}`, contentEditable: true, dir: "ltr", spellCheck: true, "data-placeholder": placeholder, onInput: handleInput, onBlur: handleBlur, "aria-label": name, "aria-invalid": !!error, style: { whiteSpace: "pre-wrap", textAlign: "left" }, onClick: handleEditorClick } ), (!value || value === "<br>") && /* @__PURE__ */ jsx("div", { className: "rte-placeholder", children: placeholder }) ] }) ] }), error && /* @__PURE__ */ jsx("div", { className: "rte-error", children: error }) ] } ); }; var RichTextEditor_default = RichTextEditor; // src/index.ts var index_default = RichTextEditor_default; export { index_default as default };