UNPKG

rich-text-editor-lib

Version:

A reusable and responsive rich text editor React component.

286 lines (282 loc) 10.2 kB
var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/index.ts var index_exports = {}; __export(index_exports, { default: () => index_default }); module.exports = __toCommonJS(index_exports); // src/RichTextEditor.tsx var import_react = require("react"); var import_lucide_react = require("lucide-react"); var import_jsx_runtime = require("react/jsx-runtime"); var toolbarButtons = [ { cmd: "bold", icon: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_lucide_react.Bold, { size: 18 }), label: "Bold" }, { cmd: "italic", icon: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_lucide_react.Italic, { size: 18 }), label: "Italic" }, { cmd: "underline", icon: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_lucide_react.Underline, { size: 18 }), label: "Underline" }, { cmd: "strikeThrough", icon: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_lucide_react.Strikethrough, { size: 18 }), label: "Strikethrough" }, { cmd: "insertUnorderedList", icon: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_lucide_react.List, { size: 18 }), // Bullet List label: "Bullet List" }, { cmd: "insertOrderedList", icon: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_lucide_react.ListOrdered, { size: 18 }), // Numbered List label: "Numbered List" }, { cmd: "createLink", icon: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_lucide_react.Link2, { size: 18 }), label: "Link" } // Image upload intentionally omitted ]; var RichTextEditor = ({ name, value, onChange, onBlur, placeholder, error, className, style }) => { const editorRef = (0, import_react.useRef)(null); const [activeStates, setActiveStates] = (0, import_react.useState)( {} ); const [showLinkInput, setShowLinkInput] = (0, import_react.useState)(false); const [linkUrl, setLinkUrl] = (0, import_react.useState)(""); const [linkText, setLinkText] = (0, import_react.useState)(""); const savedRangeRef = (0, import_react.useRef)(null); (0, import_react.useEffect)(() => { if (editorRef.current && editorRef.current.innerHTML !== value) { editorRef.current.innerHTML = value || ""; } }, [value]); (0, import_react.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__ */ (0, import_jsx_runtime.jsxs)( "div", { className: `rte-root${className ? ` ${className}` : ""}`, style, children: [ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "rte-editor-container", children: [ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "rte-toolbar", children: toolbarButtons.map((btn) => /* @__PURE__ */ (0, import_jsx_runtime.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__ */ (0, import_jsx_runtime.jsxs)("div", { style: { position: "relative" }, className: "w-full max-w-full", children: [ showLinkInput && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "rte-link-popup", children: [ /* @__PURE__ */ (0, import_jsx_runtime.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__ */ (0, import_jsx_runtime.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__ */ (0, import_jsx_runtime.jsx)( "button", { type: "button", className: "rte-insert-btn", onClick: handleInsertLink, disabled: !linkUrl || !linkText, children: "Insert" } ), /* @__PURE__ */ (0, import_jsx_runtime.jsx)( "button", { type: "button", className: "rte-cancel-btn", onClick: handleCancelLink, children: "Cancel" } ) ] }), /* @__PURE__ */ (0, import_jsx_runtime.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__ */ (0, import_jsx_runtime.jsx)("div", { className: "rte-placeholder", children: placeholder }) ] }) ] }), error && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "rte-error", children: error }) ] } ); }; var RichTextEditor_default = RichTextEditor; // src/index.ts var index_default = RichTextEditor_default;