react-quill-editors
Version:
A fully customizable React rich text editor with no external dependencies. Features a comprehensive toolbar similar to popular Markdown editors with file operations, media insertion, and advanced formatting capabilities.
237 lines (230 loc) • 12.3 kB
JavaScript
'use strict';
var jsxRuntime = require('react/jsx-runtime');
var react = require('react');
const ToolbarButton = ({ icon, title, onClick, isActive = false, disabled = false, }) => {
return (jsxRuntime.jsx("button", { type: "button", className: `toolbar-button ${isActive ? 'active' : ''}`, onClick: onClick, disabled: disabled, title: title, children: icon }));
};
const getSelection = () => {
if (typeof window !== 'undefined') {
return window.getSelection();
}
return null;
};
const getDocument = () => {
if (typeof window !== 'undefined') {
return window.document;
}
return null;
};
const isFormatActive = (command) => {
const document = getDocument();
if (!document)
return false;
try {
return document.queryCommandState(command);
}
catch (e) {
return false;
}
};
const TOOLBAR_ICONS = {
// Text formatting
bold: 'B',
italic: 'I',
underline: 'U',
strikethrough: 'S',
// Headings
heading: 'H',
// Font controls
fontSize: 'Aa',
fontFamily: 'F',
// Colors
color: '🎨',
bgColor: '🖌️',
// Alignment
align: '⫷',
// Lists
lists: '📋',
bulletList: '•',
numberedList: '1.',
// Links and media
link: '🔗',
image: '🖼️',
video: '▶️',
// Code and special formatting
code: '</>',
quote: '"',
horizontalRule: '—',
// Additional features
table: '⊞',
emoji: '😊',
attachment: '📎',
preview: '👁️',
// File operations
save: '💾',
delete: '🗑️',
close: '✕',
};
const RichTextEditor = ({ features = {}, value = '', onChange, placeholder = 'Start typing...', className = '', style = {}, readOnly = false, disabled = false, onSave, onDelete, onClose, onPreview, }) => {
const editorRef = react.useRef(null);
const [activeFormats, setActiveFormats] = react.useState({});
const [wordCount, setWordCount] = react.useState(0);
const updateFormatState = () => {
const document = getDocument();
if (!document)
return;
const newActiveFormats = {};
// Check active states
if (features.bold)
newActiveFormats.bold = isFormatActive('bold');
if (features.italic)
newActiveFormats.italic = isFormatActive('italic');
if (features.underline)
newActiveFormats.underline = isFormatActive('underline');
if (features.strikethrough)
newActiveFormats.strikethrough = isFormatActive('strikeThrough');
if (features.align) {
newActiveFormats.alignLeft = isFormatActive('justifyLeft');
newActiveFormats.alignCenter = isFormatActive('justifyCenter');
newActiveFormats.alignRight = isFormatActive('justifyRight');
newActiveFormats.alignJustify = isFormatActive('justifyFull');
}
setActiveFormats(newActiveFormats);
};
const handleFormat = (command, value) => {
const document = getDocument();
const selection = getSelection();
if (!document || !selection || readOnly || disabled)
return;
// Restore selection if needed
if (selection.rangeCount > 0) {
const range = selection.getRangeAt(0);
if (range) {
selection.removeAllRanges();
selection.addRange(range);
}
}
// Execute command
if (value) {
document.execCommand(command, false, value);
}
else {
document.execCommand(command, false);
}
// Update state
updateFormatState();
// Trigger onChange
if (onChange && editorRef.current) {
onChange(editorRef.current.innerHTML);
}
};
const handleInput = () => {
if (onChange && editorRef.current) {
onChange(editorRef.current.innerHTML);
}
updateFormatState();
updateWordCount();
};
const handleFocus = () => {
// Focus handling if needed
};
const handleBlur = () => {
// Blur handling if needed
};
const updateWordCount = () => {
if (editorRef.current) {
const text = editorRef.current.textContent || '';
const words = text.trim().split(/\s+/).filter(word => word.length > 0);
setWordCount(words.length);
}
};
react.useEffect(() => {
if (editorRef.current) {
editorRef.current.innerHTML = value;
updateWordCount();
}
}, [value]);
react.useEffect(() => {
updateFormatState();
}, [features]);
const renderToolbarButton = (feature, command, icon, title) => {
if (!features[feature])
return null;
return (jsxRuntime.jsx(ToolbarButton, { icon: icon, title: title, onClick: () => handleFormat(command), isActive: activeFormats[command] || false, disabled: readOnly || disabled }));
};
// Horizontal toolbar layout with proper grouping
const renderFileOperations = () => {
const hasFileOps = features.save || features.delete || features.close;
if (!hasFileOps)
return null;
return (jsxRuntime.jsxs("div", { className: "toolbar-group", children: [features.save && (jsxRuntime.jsx(ToolbarButton, { icon: TOOLBAR_ICONS.save, title: "Save", onClick: onSave || (() => { }), disabled: readOnly || disabled })), features.delete && (jsxRuntime.jsx(ToolbarButton, { icon: TOOLBAR_ICONS.delete, title: "Delete", onClick: onDelete || (() => { }), disabled: readOnly || disabled })), features.close && (jsxRuntime.jsx(ToolbarButton, { icon: TOOLBAR_ICONS.close, title: "Close", onClick: onClose || (() => { }), disabled: readOnly || disabled }))] }));
};
const renderFormattingTools = () => {
const hasFormatting = features.heading || features.bold || features.italic || features.underline || features.strikethrough;
if (!hasFormatting)
return null;
return (jsxRuntime.jsxs("div", { className: "toolbar-group", children: [renderToolbarButton('heading', 'formatBlock', TOOLBAR_ICONS.heading, 'Heading'), renderToolbarButton('bold', 'bold', TOOLBAR_ICONS.bold, 'Bold'), renderToolbarButton('italic', 'italic', TOOLBAR_ICONS.italic, 'Italic'), renderToolbarButton('underline', 'underline', TOOLBAR_ICONS.underline, 'Underline'), renderToolbarButton('strikethrough', 'strikeThrough', TOOLBAR_ICONS.strikethrough, 'Strikethrough')] }));
};
const renderListControls = () => {
if (!features.lists)
return null;
return (jsxRuntime.jsxs("div", { className: "toolbar-group", children: [renderToolbarButton('lists', 'insertUnorderedList', TOOLBAR_ICONS.bulletList, 'Bullet List'), renderToolbarButton('lists', 'insertOrderedList', TOOLBAR_ICONS.numberedList, 'Numbered List')] }));
};
const renderSpecialFormatting = () => {
const hasSpecialFormatting = features.quote || features.code || features.horizontalRule;
if (!hasSpecialFormatting)
return null;
return (jsxRuntime.jsxs("div", { className: "toolbar-group", children: [features.quote && (jsxRuntime.jsx(ToolbarButton, { icon: TOOLBAR_ICONS.quote, title: "Quote", onClick: () => handleFormat('formatBlock', 'blockquote'), disabled: readOnly || disabled })), features.code && (jsxRuntime.jsx(ToolbarButton, { icon: TOOLBAR_ICONS.code, title: "Code Block", onClick: () => handleFormat('formatBlock', 'pre'), disabled: readOnly || disabled })), features.horizontalRule && (jsxRuntime.jsx(ToolbarButton, { icon: TOOLBAR_ICONS.horizontalRule, title: "Horizontal Rule", onClick: () => handleFormat('insertHorizontalRule'), disabled: readOnly || disabled }))] }));
};
const renderMediaControls = () => {
const hasMediaControls = features.link || features.image || features.video;
if (!hasMediaControls)
return null;
return (jsxRuntime.jsxs("div", { className: "toolbar-group", children: [features.link && (jsxRuntime.jsx(ToolbarButton, { icon: TOOLBAR_ICONS.link, title: "Insert Link", onClick: () => {
const url = prompt('Enter URL:');
if (url)
handleFormat('createLink', url);
}, disabled: readOnly || disabled })), features.image && (jsxRuntime.jsx(ToolbarButton, { icon: TOOLBAR_ICONS.image, title: "Insert Image", onClick: () => {
const src = prompt('Enter image URL:');
if (src)
handleFormat('insertImage', src);
}, disabled: readOnly || disabled })), features.video && (jsxRuntime.jsx(ToolbarButton, { icon: TOOLBAR_ICONS.video, title: "Insert Video", onClick: () => {
const src = prompt('Enter video URL:');
if (src) {
const videoHtml = `<video src="${src}" controls></video>`;
document.execCommand('insertHTML', false, videoHtml);
}
}, disabled: readOnly || disabled }))] }));
};
const renderRightControls = () => {
const hasRightControls = features.attachment || features.preview;
if (!hasRightControls)
return null;
return (jsxRuntime.jsxs("div", { className: "toolbar-group", children: [features.attachment && (jsxRuntime.jsx(ToolbarButton, { icon: TOOLBAR_ICONS.attachment, title: "Attachment", onClick: () => {
const input = document.createElement('input');
input.type = 'file';
input.onchange = (e) => {
var _a;
const file = (_a = e.target.files) === null || _a === void 0 ? void 0 : _a[0];
if (file) {
const reader = new FileReader();
reader.onload = (e) => {
var _a;
const result = (_a = e.target) === null || _a === void 0 ? void 0 : _a.result;
const linkHtml = `<a href="${result}" target="_blank">${file.name}</a>`;
document.execCommand('insertHTML', false, linkHtml);
};
reader.readAsDataURL(file);
}
};
input.click();
}, disabled: readOnly || disabled })), features.preview && (jsxRuntime.jsx(ToolbarButton, { icon: TOOLBAR_ICONS.preview, title: "Preview", onClick: onPreview || (() => { }), disabled: readOnly || disabled }))] }));
};
return (jsxRuntime.jsxs("div", { className: `react-quill-editor ${className}`, style: style, children: [jsxRuntime.jsxs("div", { className: "toolbar", children: [renderFileOperations(), renderFileOperations() && (jsxRuntime.jsx("div", { className: "toolbar-divider" })), renderFormattingTools(), renderFormattingTools() && (jsxRuntime.jsx("div", { className: "toolbar-divider" })), renderListControls(), renderListControls() && (jsxRuntime.jsx("div", { className: "toolbar-divider" })), renderSpecialFormatting(), renderSpecialFormatting() && (jsxRuntime.jsx("div", { className: "toolbar-divider" })), renderMediaControls(), renderMediaControls() && renderRightControls() && (jsxRuntime.jsx("div", { className: "toolbar-divider" })), renderRightControls()] }), jsxRuntime.jsx("div", { ref: editorRef, className: "editor-content", contentEditable: !readOnly && !disabled, onInput: handleInput, onFocus: handleFocus, onBlur: handleBlur, "data-placeholder": placeholder, style: {
cursor: readOnly || disabled ? 'not-allowed' : 'text',
direction: 'ltr',
textAlign: 'left',
} }), jsxRuntime.jsxs("div", { className: "editor-footer", children: [jsxRuntime.jsxs("div", { className: "word-count", children: ["Words: ", wordCount] }), jsxRuntime.jsx("div", { className: "version-info", children: "v1.0.5" })] })] }));
};
exports.RichTextEditor = RichTextEditor;
//# sourceMappingURL=index.js.map