rich-text-editor-lib
Version:
A reusable and responsive rich text editor React component.
286 lines (282 loc) • 10.2 kB
JavaScript
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;