pm-react-text-editor
Version:
A customizable and lightweight rich-text editor for React, built with hooks and modern styling. Supports common formatting tools like bold, italic, lists, headings, links, and more.
554 lines (489 loc) • 22 kB
JavaScript
import { jsx, jsxs } from 'react/jsx-runtime';
import { forwardRef, createElement, createContext, useState, useRef, useEffect, useContext, useLayoutEffect } from 'react';
function ToolbarGroup({ children }) {
return (jsx("div", { className: "toolbar-group", children: children }));
}
function ToolbarButton({ icon, tooltip, onClick, active }) {
return (jsx("button", { type: "button", className: `toolbar-button ${active ? 'active' : ''}`, onClick: onClick, title: tooltip, children: icon }));
}
/**
* @license lucide-react v0.344.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/
var defaultAttributes = {
xmlns: "http://www.w3.org/2000/svg",
width: 24,
height: 24,
viewBox: "0 0 24 24",
fill: "none",
stroke: "currentColor",
strokeWidth: 2,
strokeLinecap: "round",
strokeLinejoin: "round"
};
/**
* @license lucide-react v0.344.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/
const toKebabCase = (string) => string.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase().trim();
const createLucideIcon = (iconName, iconNode) => {
const Component = forwardRef(
({
color = "currentColor",
size = 24,
strokeWidth = 2,
absoluteStrokeWidth,
className = "",
children,
...rest
}, ref) => {
return createElement(
"svg",
{
ref,
...defaultAttributes,
width: size,
height: size,
stroke: color,
strokeWidth: absoluteStrokeWidth ? Number(strokeWidth) * 24 / Number(size) : strokeWidth,
className: ["lucide", `lucide-${toKebabCase(iconName)}`, className].join(" "),
...rest
},
[
...iconNode.map(([tag, attrs]) => createElement(tag, attrs)),
...Array.isArray(children) ? children : [children]
]
);
}
);
Component.displayName = `${iconName}`;
return Component;
};
/**
* @license lucide-react v0.344.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/
const AlignCenter = createLucideIcon("AlignCenter", [
["line", { x1: "21", x2: "3", y1: "6", y2: "6", key: "1fp77t" }],
["line", { x1: "17", x2: "7", y1: "12", y2: "12", key: "rsh8ii" }],
["line", { x1: "19", x2: "5", y1: "18", y2: "18", key: "1t0tuv" }]
]);
/**
* @license lucide-react v0.344.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/
const AlignJustify = createLucideIcon("AlignJustify", [
["line", { x1: "3", x2: "21", y1: "6", y2: "6", key: "4m8b97" }],
["line", { x1: "3", x2: "21", y1: "12", y2: "12", key: "10d38w" }],
["line", { x1: "3", x2: "21", y1: "18", y2: "18", key: "kwyyxn" }]
]);
/**
* @license lucide-react v0.344.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/
const AlignLeft = createLucideIcon("AlignLeft", [
["line", { x1: "21", x2: "3", y1: "6", y2: "6", key: "1fp77t" }],
["line", { x1: "15", x2: "3", y1: "12", y2: "12", key: "v6grx8" }],
["line", { x1: "17", x2: "3", y1: "18", y2: "18", key: "1awlsn" }]
]);
/**
* @license lucide-react v0.344.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/
const AlignRight = createLucideIcon("AlignRight", [
["line", { x1: "21", x2: "3", y1: "6", y2: "6", key: "1fp77t" }],
["line", { x1: "21", x2: "9", y1: "12", y2: "12", key: "1uyos4" }],
["line", { x1: "21", x2: "7", y1: "18", y2: "18", key: "1g9eri" }]
]);
/**
* @license lucide-react v0.344.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/
const Bold = createLucideIcon("Bold", [
["path", { d: "M14 12a4 4 0 0 0 0-8H6v8", key: "v2sylx" }],
["path", { d: "M15 20a4 4 0 0 0 0-8H6v8Z", key: "1ef5ya" }]
]);
/**
* @license lucide-react v0.344.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/
const ChevronDown = createLucideIcon("ChevronDown", [
["path", { d: "m6 9 6 6 6-6", key: "qrunsl" }]
]);
/**
* @license lucide-react v0.344.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/
const Image = createLucideIcon("Image", [
["rect", { width: "18", height: "18", x: "3", y: "3", rx: "2", ry: "2", key: "1m3agn" }],
["circle", { cx: "9", cy: "9", r: "2", key: "af1f0g" }],
["path", { d: "m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21", key: "1xmnt7" }]
]);
/**
* @license lucide-react v0.344.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/
const Italic = createLucideIcon("Italic", [
["line", { x1: "19", x2: "10", y1: "4", y2: "4", key: "15jd3p" }],
["line", { x1: "14", x2: "5", y1: "20", y2: "20", key: "bu0au3" }],
["line", { x1: "15", x2: "9", y1: "4", y2: "20", key: "uljnxc" }]
]);
/**
* @license lucide-react v0.344.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/
const Link = createLucideIcon("Link", [
["path", { d: "M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71", key: "1cjeqo" }],
["path", { d: "M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71", key: "19qd67" }]
]);
/**
* @license lucide-react v0.344.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/
const ListOrdered = createLucideIcon("ListOrdered", [
["line", { x1: "10", x2: "21", y1: "6", y2: "6", key: "76qw6h" }],
["line", { x1: "10", x2: "21", y1: "12", y2: "12", key: "16nom4" }],
["line", { x1: "10", x2: "21", y1: "18", y2: "18", key: "u3jurt" }],
["path", { d: "M4 6h1v4", key: "cnovpq" }],
["path", { d: "M4 10h2", key: "16xx2s" }],
["path", { d: "M6 18H4c0-1 2-2 2-3s-1-1.5-2-1", key: "m9a95d" }]
]);
/**
* @license lucide-react v0.344.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/
const List = createLucideIcon("List", [
["line", { x1: "8", x2: "21", y1: "6", y2: "6", key: "7ey8pc" }],
["line", { x1: "8", x2: "21", y1: "12", y2: "12", key: "rjfblc" }],
["line", { x1: "8", x2: "21", y1: "18", y2: "18", key: "c3b1m8" }],
["line", { x1: "3", x2: "3.01", y1: "6", y2: "6", key: "1g7gq3" }],
["line", { x1: "3", x2: "3.01", y1: "12", y2: "12", key: "1pjlvk" }],
["line", { x1: "3", x2: "3.01", y1: "18", y2: "18", key: "28t2mc" }]
]);
/**
* @license lucide-react v0.344.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/
const Palette = createLucideIcon("Palette", [
["circle", { cx: "13.5", cy: "6.5", r: ".5", fill: "currentColor", key: "1okk4w" }],
["circle", { cx: "17.5", cy: "10.5", r: ".5", fill: "currentColor", key: "f64h9f" }],
["circle", { cx: "8.5", cy: "7.5", r: ".5", fill: "currentColor", key: "fotxhn" }],
["circle", { cx: "6.5", cy: "12.5", r: ".5", fill: "currentColor", key: "qy21gx" }],
[
"path",
{
d: "M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10c.926 0 1.648-.746 1.648-1.688 0-.437-.18-.835-.437-1.125-.29-.289-.438-.652-.438-1.125a1.64 1.64 0 0 1 1.668-1.668h1.996c3.051 0 5.555-2.503 5.555-5.554C21.965 6.012 17.461 2 12 2z",
key: "12rzf8"
}
]
]);
/**
* @license lucide-react v0.344.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/
const Redo = createLucideIcon("Redo", [
["path", { d: "M21 7v6h-6", key: "3ptur4" }],
["path", { d: "M3 17a9 9 0 0 1 9-9 9 9 0 0 1 6 2.3l3 2.7", key: "1kgawr" }]
]);
/**
* @license lucide-react v0.344.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/
const Type = createLucideIcon("Type", [
["polyline", { points: "4 7 4 4 20 4 20 7", key: "1nosan" }],
["line", { x1: "9", x2: "15", y1: "20", y2: "20", key: "swin9y" }],
["line", { x1: "12", x2: "12", y1: "4", y2: "20", key: "1tx1rr" }]
]);
/**
* @license lucide-react v0.344.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/
const Underline = createLucideIcon("Underline", [
["path", { d: "M6 4v6a6 6 0 0 0 12 0V4", key: "9kb039" }],
["line", { x1: "4", x2: "20", y1: "20", y2: "20", key: "nun2al" }]
]);
/**
* @license lucide-react v0.344.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/
const Undo = createLucideIcon("Undo", [
["path", { d: "M3 7v6h6", key: "1v2h90" }],
["path", { d: "M21 17a9 9 0 0 0-9-9 9 9 0 0 0-6 2.3L3 13", key: "1r6uu6" }]
]);
function Dropdown({ options, onChange }) {
return (jsxs("div", { className: "dropdown", children: [jsx("select", { onChange: (e) => onChange(e.target.value), children: options.map((option) => (jsx("option", { value: option.value, children: option.label }, option.value))) }), jsx(ChevronDown, { size: 16, className: "dropdown-icon" })] }));
}
const EditorContext = createContext(undefined);
function EditorProvider({ children, initialContent = '', onChange }) {
const [content, setContent] = useState(initialContent);
const previousContentRef = useRef(initialContent);
useEffect(() => {
if (onChange && previousContentRef.current !== content) {
onChange(content);
previousContentRef.current = content;
}
}, [content, onChange]);
const execCommand = (command, value) => {
document.execCommand(command, false, value);
};
const formatBlock = (blockType) => {
document.execCommand('formatBlock', false, blockType);
};
const wordCount = content.trim() ? content.trim().replace(/<[^>]*>/g, ' ').split(/\s+/).filter(Boolean).length : 0;
const characterCount = content.trim() ? content.trim().replace(/ /g, " ").replace(/<[^>]*>/g, ' ').length : 0;
return (jsx(EditorContext.Provider, { value: {
content,
setContent,
wordCount,
characterCount,
execCommand,
formatBlock,
}, children: children }));
}
function useEditorContext() {
const context = useContext(EditorContext);
if (context === undefined) {
throw new Error('useEditorContext must be used within an EditorProvider');
}
return context;
}
const predefinedColors = [
'#000000', '#333333', '#666666', '#999999', '#cccccc', '#ffffff',
'#ff0000', '#ff6600', '#ffcc00', '#00ff00', '#0066ff', '#6600ff',
'#ff3366', '#ff9933', '#ffff00', '#33ff33', '#3366ff', '#9933ff',
'#cc0000', '#cc6600', '#cccc00', '#00cc00', '#0066cc', '#6600cc',
'#990000', '#996600', '#999900', '#009900', '#006699', '#660099'
];
function ColorPicker({ onColorSelect, icon, tooltip }) {
const [isOpen, setIsOpen] = useState(false);
const [customColor, setCustomColor] = useState('#000000');
const dropdownRef = useRef(null);
useEffect(() => {
const handleClickOutside = (event) => {
if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
setIsOpen(false);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}, []);
const handleColorClick = (color) => {
onColorSelect(color);
setIsOpen(false);
};
const handleCustomColorChange = (e) => {
const color = e.target.value;
setCustomColor(color);
onColorSelect(color);
};
return (jsxs("div", { className: "color-picker", ref: dropdownRef, children: [jsxs("button", { type: "button", className: "toolbar-button color-picker-button", onClick: () => setIsOpen(!isOpen), title: tooltip, children: [icon, jsx(ChevronDown, { size: 12, className: "color-picker-arrow" })] }), isOpen && (jsxs("div", { className: "color-picker-dropdown", children: [jsx("div", { className: "color-grid", children: predefinedColors.map((color) => (jsx("button", { type: "button", className: "color-swatch", style: { backgroundColor: color }, onClick: () => handleColorClick(color), title: color }, color))) }), jsxs("div", { className: "custom-color-section", children: [jsx("label", { htmlFor: "custom-color", children: "Custom:" }), jsx("input", { id: "custom-color", type: "color", value: customColor, onChange: handleCustomColorChange, className: "custom-color-input" })] })] }))] }));
}
function Toolbar() {
const { execCommand, formatBlock } = useEditorContext();
const [activeFormats, setActiveFormats] = useState({
bold: false,
italic: false,
underline: false,
justifyLeft: false,
justifyCenter: false,
justifyRight: false,
justifyFull: false,
insertUnorderedList: false,
insertOrderedList: false,
});
function isFormatActive(tagName) {
const selection = window.getSelection();
if (!selection || selection.rangeCount === 0)
return false;
let node = selection.anchorNode;
if (node && node.nodeType === 3) {
node = node.parentElement;
}
while (node && node !== document.body) {
if (node.tagName?.toLowerCase() === tagName.toLowerCase()) {
return true;
}
node = node.parentElement;
}
return false;
}
function getCurrentAlignment() {
const selection = window.getSelection();
if (!selection || selection.rangeCount === 0)
return null;
let node = selection.anchorNode;
if (node?.nodeType === Node.TEXT_NODE) {
node = node.parentElement;
}
while (node && node !== document.body) {
const align = node.style?.textAlign || window.getComputedStyle(node).textAlign;
if (['left', 'center', 'right', 'justify'].includes(align)) {
return align;
}
node = node.parentElement;
}
return null;
}
const updateActiveFormats = () => {
const alignment = getCurrentAlignment();
setActiveFormats({
bold: isFormatActive('b') || isFormatActive('strong'),
italic: isFormatActive('i') || isFormatActive('em'),
underline: isFormatActive('u'),
justifyLeft: alignment === 'left',
justifyCenter: alignment === 'center',
justifyRight: alignment === 'right',
justifyFull: alignment === 'justify',
insertUnorderedList: isFormatActive('ul'),
insertOrderedList: isFormatActive('ol'),
});
};
useEffect(() => {
document.addEventListener('selectionchange', updateActiveFormats);
return () => {
document.removeEventListener('selectionchange', updateActiveFormats);
};
}, []);
const handleBlockChange = (value) => {
formatBlock(value);
};
const handleLink = () => {
const url = prompt('Enter URL:');
if (url) {
execCommand('createLink', url);
}
};
const handleImage = () => {
const url = prompt('Enter image URL:');
if (url) {
execCommand('insertImage', url);
}
};
const handleTextColor = (color) => {
execCommand('foreColor', color);
};
const handleBackgroundColor = (color) => {
execCommand('hiliteColor', color);
};
const handleExec = (command) => {
execCommand(command);
setActiveFormats(prev => ({
...prev,
[command]: !prev[command]
}));
};
return (jsxs("div", { className: "toolbar", children: [jsxs(ToolbarGroup, { children: [jsx(ToolbarButton, { icon: jsx(Undo, { size: 18 }), tooltip: "Undo", onClick: () => handleExec('undo') }), jsx(ToolbarButton, { icon: jsx(Redo, { size: 18 }), tooltip: "Redo", onClick: () => handleExec('redo') })] }), jsx(ToolbarGroup, { children: jsx(Dropdown, { options: [
{ label: 'Paragraph', value: 'p' },
{ label: 'Heading 1', value: 'h1' },
{ label: 'Heading 2', value: 'h2' },
{ label: 'Heading 3', value: 'h3' },
{ label: 'Heading 4', value: 'h4' },
{ label: 'Heading 5', value: 'h5' },
{ label: 'Heading 6', value: 'h6' },
], onChange: handleBlockChange }) }), jsxs(ToolbarGroup, { children: [jsx(ToolbarButton, { icon: jsx(Bold, { size: 18 }), tooltip: "Bold", onClick: () => handleExec('bold'), active: activeFormats.bold }), jsx(ToolbarButton, { icon: jsx(Italic, { size: 18 }), tooltip: "Italic", onClick: () => handleExec('italic'), active: activeFormats.italic }), jsx(ToolbarButton, { icon: jsx(Underline, { size: 18 }), tooltip: "Underline", onClick: () => handleExec('underline'), active: activeFormats.underline })] }), jsxs(ToolbarGroup, { children: [jsx(ColorPicker, { icon: jsx(Type, { size: 18 }), tooltip: "Text Color", onColorSelect: handleTextColor }), jsx(ColorPicker, { icon: jsx(Palette, { size: 18 }), tooltip: "Background Color", onColorSelect: handleBackgroundColor })] }), jsxs(ToolbarGroup, { children: [jsx(ToolbarButton, { icon: jsx(AlignLeft, { size: 18 }), tooltip: "Align Left", onClick: () => handleExec('justifyLeft'), active: activeFormats.justifyLeft }), jsx(ToolbarButton, { icon: jsx(AlignCenter, { size: 18 }), tooltip: "Align Center", onClick: () => handleExec('justifyCenter'), active: activeFormats.justifyCenter }), jsx(ToolbarButton, { icon: jsx(AlignRight, { size: 18 }), tooltip: "Align Right", onClick: () => handleExec('justifyRight'), active: activeFormats.justifyRight }), jsx(ToolbarButton, { icon: jsx(AlignJustify, { size: 18 }), tooltip: "Justify", onClick: () => handleExec('justifyFull'), active: activeFormats.justifyFull })] }), jsxs(ToolbarGroup, { children: [jsx(ToolbarButton, { icon: jsx(List, { size: 18 }), tooltip: "Bullet List", onClick: () => handleExec('insertUnorderedList'), active: activeFormats.insertUnorderedList }), jsx(ToolbarButton, { icon: jsx(ListOrdered, { size: 18 }), tooltip: "Numbered List", onClick: () => handleExec('insertOrderedList'), active: activeFormats.insertOrderedList })] }), jsxs(ToolbarGroup, { children: [jsx(ToolbarButton, { icon: jsx(Link, { size: 18 }), tooltip: "Insert Link", onClick: handleLink }), jsx(ToolbarButton, { icon: jsx(Image, { size: 18 }), tooltip: "Insert Image", onClick: handleImage })] })] }));
}
function EditorContent() {
const { content, setContent } = useEditorContext();
const editorRef = useRef(null);
const savedRange = useRef(null);
const [isInitial, setIsInitial] = useState(true);
const saveCaretPosition = () => {
const selection = window.getSelection();
if (selection && selection.rangeCount > 0) {
savedRange.current = selection.getRangeAt(0).cloneRange();
}
};
const restoreCaretPosition = () => {
const selection = window.getSelection();
if (savedRange.current && selection) {
try {
selection.removeAllRanges();
selection.addRange(savedRange.current);
}
catch (e) {
console.warn('Unable to restore caret:', e);
}
}
};
const handleInput = (e) => {
saveCaretPosition();
setContent(e.currentTarget.innerHTML);
};
useLayoutEffect(() => {
if (editorRef.current) {
editorRef.current.focus();
restoreCaretPosition();
}
}, [content]);
useEffect(() => {
if (editorRef.current && isInitial) {
editorRef.current.innerHTML = content;
setIsInitial(false);
}
}, [content, isInitial]);
const handleWrapperClick = () => {
if (editorRef.current) {
editorRef.current.focus();
}
};
return (jsx("div", { className: "editor-content", onClick: handleWrapperClick, children: jsx("div", { ref: editorRef, className: "editor-area", contentEditable: true, onInput: handleInput, onKeyUp: saveCaretPosition, onMouseUp: saveCaretPosition, suppressContentEditableWarning: true }) }));
}
function StatusBar() {
const { wordCount, characterCount } = useEditorContext();
return (jsxs("div", { className: "status-bar", children: [jsxs("div", { children: ["Words: ", wordCount] }), jsxs("div", { children: ["Characters: ", characterCount] })] }));
}
function Editor() {
return (jsxs("div", { className: "editor-container", children: [jsx(Toolbar, {}), jsx(EditorContent, {}), jsx(StatusBar, {})] }));
}
function PMEditorContent({ value }) {
const { content, setContent } = useEditorContext();
const isInitial = useRef(true);
useEffect(() => {
if (isInitial.current && value !== undefined) {
setContent(value);
isInitial.current = false;
}
}, [value]);
return jsx(Editor, {});
}
function PMEditorHook({ onChange, value }) {
return (jsx(EditorProvider, { initialContent: value, onChange: onChange, children: jsx(PMEditorContent, { onChange: onChange, value: value }) }));
}
export { PMEditorHook as default };
//# sourceMappingURL=index.esm.js.map