UNPKG

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
'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