Mercurial
diff rich_editor/rich_editor.js @ 219:8c9bb0b0759e hg-web
[hg-web] Merge current Zenbu baseline
| author | MrJuneJune <me@mrjunejune.com> |
|---|---|
| date | Sun, 02 Aug 2026 08:34:54 -0700 |
| parents | 5d3e116dd745 |
| children | 9c2eec61a152 |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/rich_editor/rich_editor.js Sun Aug 02 08:34:54 2026 -0700 @@ -0,0 +1,1062 @@ +/** + * Rich Editor + * ----------- + * A vanilla JavaScript rich text editor with: + * - Text formatting (h1-h6, paragraphs, lists) + * - Image/file upload via paste, drop, click, or /upload command + * - Debounced auto-save + * - Generic callback functions for uploads and saves + * + * Usage: + * const editor = RichEditor.init('editor-container', { + * uploadCallback: async (file) => { return { url: '...', key: '...' }; }, + * saveCallback: async (content) => { console.log('Saved:', content); }, + * debounceMs: 1000, + * placeholder: 'Start writing...' + * }); + */ + +const RichEditor = (function() { + 'use strict'; + + const DEFAULT_OPTIONS = { + uploadCallback: null, + saveCallback: null, + debounceMs: 1000, + placeholder: 'Start writing... (Use /upload to insert files)', + cloudFrontUrl: '' + }; + + function debounce(func, wait) { + let timeout; + return function executedFunction(...args) { + const later = () => { + clearTimeout(timeout); + func(...args); + }; + clearTimeout(timeout); + timeout = setTimeout(later, wait); + }; + } + + function createToolbar(editor) { + const toolbar = document.createElement('div'); + toolbar.className = 'rich-editor-toolbar'; + + const buttons = [ + { cmd: 'h1', label: '', title: 'Heading 1', icons: "/public/icons/h1.png" }, + { cmd: 'h2', label: '', title: 'Heading 2', icons: "/public/icons/h2.png" }, + { cmd: 'h3', label: '', title: 'Heading 3', icons: "/public/icons/h3.png" }, + { cmd: 'p', label: '¶', title: 'Paragraph' }, + { cmd: 'ul', label: '•', title: 'Bullet List' }, + { cmd: 'ol', label: '1.', title: 'Numbered List' }, + { cmd: 'bold', label: '', title: 'Bold', icons: "/public/icons/bold.png" }, + { cmd: 'separator' }, + { cmd: 'link', label: '', title: 'Insert Link', icons: "/public/icons/link.png" }, + { cmd: 'notelink', label: '', title: 'Link to Note', icons: "/public/icons/binder.png" }, + { cmd: 'upload', label: '', title: 'Upload File', icons: "/public/icons/clipy.png" }, + { cmd: 'separator' }, + { cmd: 'readonly', label: '', title: 'readonly', icons: "/public/icons/book.png" }, + ]; + + buttons.forEach(btn => { + if (btn.cmd === 'separator') { + const sep = document.createElement('span'); + sep.className = 'rich-editor-separator'; + toolbar.appendChild(sep); + return; + } + + const button = document.createElement('button'); + button.type = 'button'; + button.className = 'rich-editor-btn'; + if (btn.icons) + button.style.backgroundImage = `url("${btn.icons}")`; + button.textContent = btn.label; + button.title = btn.title; + button.dataset.cmd = btn.cmd; + + button.addEventListener('click', (e) => { + e.preventDefault(); + editor.execCommand(btn.cmd); + }); + + toolbar.appendChild(button); + }); + + return toolbar; + } + + function ensureViewportMeta() { + // Check if viewport meta tag exists + let viewport = document.querySelector('meta[name="viewport"]'); + if (!viewport) { + viewport = document.createElement('meta'); + viewport.name = 'viewport'; + viewport.content = 'width=device-width, initial-scale=1.0, maximum-scale=5.0'; + document.head.appendChild(viewport); + } + } + + function createStyles() { + if (document.getElementById('rich-editor-styles')) return; + + ensureViewportMeta(); + + const style = document.createElement('style'); + style.id = 'rich-editor-styles'; + style.textContent = ` + .rich-editor-container { + border: 1px solid #ccc; + border-radius: 4px; + overflow: hidden; + } + + .rich-editor-toolbar { + display: flex; + flex-wrap: wrap; + gap: 4px; + padding: 8px; + background: #f5f5f5; + border-bottom: 1px solid #ccc; + } + + .rich-editor-btn { + padding: 6px 12px; + border: 1px solid #ccc; + border-radius: 4px; + background: #fff; + cursor: pointer; + font-size: 14px; + min-width: 32px; + background-repeat: no-repeat; + background-position: center; + background-size: 24px 24px; + border: none; + } + + .rich-editor-btn:hover { + background: #e9e9e9; + background-repeat: no-repeat; + background-position: center; + background-size: 24px 24px; + } + + .rich-editor-btn:active { + background: #ddd; + background-repeat: no-repeat; + background-position: center; + background-size: 24px 24px; + } + + .rich-editor-separator { + width: 1px; + background: #ccc; + margin: 0 4px; + } + + .rich-editor-content { + min-height: 300px; + padding: 16px; + outline: none; + overflow-y: auto; + } + + .rich-editor-content:empty:before { + content: attr(data-placeholder); + color: #999; + pointer-events: none; + } + + .rich-editor-content img { + max-width: 100%; + height: auto; + border-radius: 4px; + margin: 8px 0; + cursor: pointer; + } + + .rich-editor-content img.selected { + outline: 2px solid #0078ff; + outline-offset: 2px; + } + + .rich-editor-resize-wrapper { + position: relative; + display: inline-block; + margin: 8px 0; + } + + .rich-editor-resize-handle { + position: absolute; + width: 12px; + height: 12px; + background: #0078ff; + border: 2px solid white; + border-radius: 50%; + cursor: nwse-resize; + z-index: 10; + } + + .rich-editor-resize-handle.nw { + top: -6px; + left: -6px; + cursor: nwse-resize; + } + + .rich-editor-resize-handle.ne { + top: -6px; + right: -6px; + cursor: nesw-resize; + } + + .rich-editor-resize-handle.sw { + bottom: -6px; + left: -6px; + cursor: nesw-resize; + } + + .rich-editor-resize-handle.se { + bottom: -6px; + right: -6px; + cursor: nwse-resize; + } + + .rich-editor-size-input { + position: absolute; + top: -35px; + left: 50%; + transform: translateX(-50%); + background: white; + border: 1px solid #ccc; + padding: 4px 8px; + border-radius: 4px; + font-size: 12px; + display: flex; + gap: 8px; + align-items: center; + box-shadow: 0 2px 8px rgba(0,0,0,0.1); + z-index: 11; + } + + .rich-editor-size-input input { + width: 60px; + padding: 2px 4px; + border: 1px solid #ccc; + border-radius: 2px; + font-size: 12px; + } + + .rich-editor-size-input label { + font-size: 11px; + color: #666; + } + + .rich-editor-content a { + color: #0078ff; + text-decoration: none; + } + + .rich-editor-content a:hover { + text-decoration: underline; + } + + .rich-editor-content a.note-link { + background: #e8f4ff; + padding: 2px 6px; + border-radius: 3px; + } + + .rich-editor-content a.note-link:hover { + background: #d0e8ff; + } + + .rich-editor-content .upload-placeholder { + display: inline-block; + padding: 8px 16px; + background: #f0f0f0; + border-radius: 4px; + color: #666; + font-style: italic; + } + + .rich-editor-content .upload-placeholder.loading { + padding: 8px 16px; + background: #fffacd; + border-radius: 4px; + animation: pulse 1.5s ease-in-out infinite; + } + + @keyframes pulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.6; } + } + + .rich-editor-upload-overlay { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 120, 255, 0.1); + border: 2px dashed #0078ff; + display: flex; + align-items: center; + justify-content: center; + font-size: 18px; + color: #0078ff; + pointer-events: none; + z-index: 10; + padding: 20px; + text-align: center; + } + + .rich-editor-status { + padding: 4px 8px; + font-size: 12px; + color: #666; + background: #f9f9f9; + border-top: 1px solid #eee; + } + + .rich-editor-file-input { + display: none; + } + + ol,ul { + padding: 16px; + } + + /* Mobile optimizations */ + @media (max-width: 720px) { + .rich-editor-toolbar { + padding: 6px; + gap: 3px; + overflow-x: auto; + -webkit-overflow-scrolling: touch; + } + + .rich-editor-btn { + min-width: 44px; + min-height: 44px; + padding: 8px; + flex-shrink: 0; + } + + .rich-editor-content { + min-height: 200px; + padding: 12px; + font-size: 16px; /* Prevents iOS zoom */ + line-height: 1.6; + } + + .rich-editor-content h1 { + font-size: 1.75em; + } + + .rich-editor-content h2 { + font-size: 1.5em; + } + + .rich-editor-content h3 { + font-size: 1.25em; + } + + .rich-editor-status { + font-size: 11px; + padding: 6px 8px; + } + + /* Mobile-friendly image resizing */ + .rich-editor-resize-handle { + width: 20px; + height: 20px; + border-width: 3px; + } + + .rich-editor-resize-handle.nw { + top: -10px; + left: -10px; + } + + .rich-editor-resize-handle.ne { + top: -10px; + right: -10px; + } + + .rich-editor-resize-handle.sw { + bottom: -10px; + left: -10px; + } + + .rich-editor-resize-handle.se { + bottom: -10px; + right: -10px; + } + + .rich-editor-size-input { + top: -40px; + padding: 6px 10px; + } + + .rich-editor-size-input input { + width: 70px; + padding: 4px 6px; + font-size: 16px; /* Prevents iOS zoom */ + } + + /* Better touch targets for images */ + .rich-editor-content img { + margin: 12px 0; + } + + /* Hide drop overlay text on small screens */ + .rich-editor-upload-overlay { + font-size: 16px; + } + + /* Make link/note dialogs mobile-friendly */ + .rich-editor-content a { + padding: 2px 0; + } + } + + /* Extra small devices */ + @media (max-width: 480px) { + .rich-editor-toolbar { + padding: 4px; + gap: 2px; + } + + .rich-editor-btn { + min-width: 40px; + min-height: 40px; + padding: 6px; + } + + .rich-editor-content { + padding: 10px; + font-size: 16px; + } + + .rich-editor-size-input { + font-size: 11px; + } + + .rich-editor-size-input input { + width: 60px; + font-size: 14px; + } + } + `; + document.head.appendChild(style); + } + + class Editor { + constructor(elementId, options) { + this.options = { ...DEFAULT_OPTIONS, ...options }; + this.container = document.getElementById(elementId); + this.state = { + readOnly: false, + selectedImage: null, + resizing: false + }; + + if (!this.container) { + throw new Error(`Element with id "${elementId}" not found`); + } + + this.init(); + } + + init() { + createStyles(); + + // Create wrapper + this.wrapper = document.createElement('div'); + this.wrapper.className = 'rich-editor-container'; + this.wrapper.style.position = 'relative'; + + // Create toolbar + this.toolbar = createToolbar(this); + this.wrapper.appendChild(this.toolbar); + + // Create content area + this.content = document.createElement('div'); + this.content.className = 'rich-editor-content'; + this.content.contentEditable = true; + this.content.dataset.placeholder = this.options.placeholder; + this.wrapper.appendChild(this.content); + + // Create status bar + this.status = document.createElement('div'); + this.status.className = 'rich-editor-status'; + this.status.textContent = 'Ready'; + this.wrapper.appendChild(this.status); + + // Create hidden file input + this.fileInput = document.createElement('input'); + this.fileInput.type = 'file'; + this.fileInput.className = 'rich-editor-file-input'; + this.fileInput.multiple = true; + this.fileInput.accept = 'image/*,application/pdf,.doc,.docx,.txt'; + this.wrapper.appendChild(this.fileInput); + + // Add to container + this.container.appendChild(this.wrapper); + + // Setup event listeners + this.setupEvents(); + + // Setup debounced save + if (this.options.saveCallback) { + this.debouncedSave = debounce(() => { + this.save(); + }, this.options.debounceMs); + } + } + + setupEvents() { + // Input events for auto-save + this.content.addEventListener('input', () => { + if (this.debouncedSave) { + this.setStatus('Editing...'); + this.debouncedSave(); + } + }); + + // Handle paste + this.content.addEventListener('paste', (e) => { + const items = e.clipboardData?.items; + if (!items) return; + + for (const item of items) { + if (item.type.startsWith('image/') || item.kind === 'file') { + e.preventDefault(); + const file = item.getAsFile(); + if (file) this.uploadFile(file); + return; + } + } + }); + + // Handle drop + this.content.addEventListener('dragover', (e) => { + e.preventDefault(); + this.showDropOverlay(); + }); + + this.content.addEventListener('dragleave', (e) => { + e.preventDefault(); + this.hideDropOverlay(); + }); + + this.content.addEventListener('drop', (e) => { + e.preventDefault(); + this.hideDropOverlay(); + + const files = e.dataTransfer?.files; + if (files && files.length > 0) { + for (const file of files) { + this.uploadFile(file); + } + } + }); + + // Handle /upload command + this.content.addEventListener('keydown', (e) => { + if (e.key === 'Enter') { + const selection = window.getSelection(); + const node = selection.anchorNode; + if (node && node.textContent) { + const text = node.textContent; + if (text.trim() === '/upload') { + e.preventDefault(); + node.textContent = ''; + this.triggerFileUpload(); + } + } + } + }); + + // File input change + this.fileInput.addEventListener('change', (e) => { + const files = e.target.files; + if (files && files.length > 0) { + for (const file of files) { + this.uploadFile(file); + } + } + this.fileInput.value = ''; + }); + + // Handle keyboard shortcuts + this.content.addEventListener('keydown', (e) => { + if (e.ctrlKey || e.metaKey) { + switch (e.key.toLowerCase()) { + case 'b': + e.preventDefault(); + this.execCommand('bold'); + break; + case 'i': + e.preventDefault(); + this.execCommand('italic'); + break; + case 's': + e.preventDefault(); + this.save(); + break; + } + } + }); + + // Handle image clicks for resizing + this.content.addEventListener('click', (e) => { + if (e.target.tagName === 'IMG') { + e.preventDefault(); + this.selectImage(e.target); + } else if (!e.target.closest('.rich-editor-resize-wrapper')) { + this.deselectImage(); + } + }); + + // Deselect image when clicking outside + document.addEventListener('click', (e) => { + if (!this.content.contains(e.target)) { + this.deselectImage(); + } + }); + } + + showDropOverlay() { + if (this.dropOverlay) return; + + this.dropOverlay = document.createElement('div'); + this.dropOverlay.className = 'rich-editor-upload-overlay'; + this.dropOverlay.textContent = 'Drop files here to upload'; + this.wrapper.appendChild(this.dropOverlay); + } + + hideDropOverlay() { + if (this.dropOverlay) { + this.dropOverlay.remove(); + this.dropOverlay = null; + } + } + + triggerFileUpload() { + this.fileInput.click(); + } + + readOnly() { + this.state.readOnly = !this.state.readOnly; + + if (this.state.readOnly) + { + this.content.contentEditable = false; + this.setStatus('Read-only mode'); + const buttons = this.toolbar.querySelectorAll('.rich-editor-btn'); + buttons.forEach(btn => { + if (btn.dataset.cmd !== 'readonly') { + btn.disabled = true; + } + }); + } + else + { + // Enable editing + this.content.contentEditable = true; + this.content.style.backgroundColor = ''; + this.setStatus('Ready'); + + // Enable toolbar buttons + const buttons = this.toolbar.querySelectorAll('.rich-editor-btn'); + buttons.forEach(btn => { + btn.disabled = false; + }); + } + } + + async uploadFile(file) { + if (!this.options.uploadCallback) { + console.warn('No upload callback configured'); + return; + } + + // Insert loading placeholder + const placeholder = document.createElement('div'); + placeholder.className = 'upload-placeholder loading'; + placeholder.textContent = file.type.startsWith('image/') + ? `Processing ${file.name}... ⏳` + : `Uploading ${file.name}...`; + + this.insertAtCursor(placeholder); + + this.setStatus(`Uploading ${file.name}...`); + + try { + const result = await this.options.uploadCallback(file); + + if (result && result.url) { + // Replace placeholder with actual content + if (file.type.startsWith('image/')) { + const img = document.createElement('img'); + img.src = result.url; + img.alt = file.name; + + // Store original dimensions when image loads + img.onload = () => { + img.dataset.originalWidth = img.naturalWidth; + img.dataset.originalHeight = img.naturalHeight; + }; + + placeholder.replaceWith(img); + } else { + const link = document.createElement('a'); + link.href = result.url; + link.textContent = file.name; + link.target = '_blank'; + placeholder.replaceWith(link); + } + + this.setStatus(`Uploaded ${file.name}`); + if (this.debouncedSave) this.debouncedSave(); + } else { + placeholder.textContent = `Failed to upload ${file.name}`; + placeholder.className = 'upload-placeholder'; + this.setStatus('Upload failed'); + } + } catch (error) { + console.error('Upload error:', error); + placeholder.textContent = `Error: ${error.message}`; + placeholder.className = 'upload-placeholder'; + this.setStatus('Upload error'); + } + } + + insertAtCursor(element) { + const selection = window.getSelection(); + if (selection.rangeCount > 0) { + const range = selection.getRangeAt(0); + range.deleteContents(); + range.insertNode(element); + range.setStartAfter(element); + range.collapse(true); + selection.removeAllRanges(); + selection.addRange(range); + } else { + this.content.appendChild(element); + } + this.content.focus(); + } + + execCommand(cmd) { + this.content.focus(); + + switch (cmd) { + case 'h1': + case 'h2': + case 'h3': + case 'h4': + case 'h5': + case 'h6': + document.execCommand('formatBlock', false, cmd); + break; + case 'p': + document.execCommand('formatBlock', false, 'p'); + break; + case 'ul': + document.execCommand('insertUnorderedList', false, null); + break; + case 'ol': + document.execCommand('insertOrderedList', false, null); + break; + case 'bold': + document.execCommand('bold', false, null); + break; + case 'italic': + document.execCommand('italic', false, null); + break; + case 'link': + this.insertLink(); + break; + case 'notelink': + this.insertNoteLink(); + break; + case 'upload': + this.triggerFileUpload(); + break; + case 'readonly': + this.readOnly(); + break; + } + + if (this.debouncedSave) this.debouncedSave(); + } + + insertLink() { + const url = prompt('Enter URL:'); + if (!url) return; + + const selection = window.getSelection(); + const selectedText = selection.toString() || url; + + const link = document.createElement('a'); + link.href = url; + link.textContent = selectedText; + link.target = '_blank'; + + if (selection.rangeCount > 0) { + const range = selection.getRangeAt(0); + range.deleteContents(); + range.insertNode(link); + range.setStartAfter(link); + range.collapse(true); + selection.removeAllRanges(); + selection.addRange(range); + } + } + + insertNoteLink() { + const noteId = prompt('Enter note ID (e.g., my-ideas):'); + if (!noteId) return; + + const sanitizedId = noteId.toLowerCase().replace(/[^a-z0-9-]/g, '-').replace(/-+/g, '-'); + const selection = window.getSelection(); + const selectedText = selection.toString() || sanitizedId; + + const link = document.createElement('a'); + link.href = '/notes/' + encodeURIComponent(sanitizedId); + link.textContent = selectedText; + link.className = 'note-link'; + + if (selection.rangeCount > 0) { + const range = selection.getRangeAt(0); + range.deleteContents(); + range.insertNode(link); + range.setStartAfter(link); + range.collapse(true); + selection.removeAllRanges(); + selection.addRange(range); + } + } + + setStatus(text) { + this.status.textContent = text; + } + + async save() { + if (!this.options.saveCallback) return; + + this.setStatus('Saving...'); + + try { + await this.options.saveCallback(this.getContent()); + this.setStatus('Saved'); + } catch (error) { + console.error('Save error:', error); + this.setStatus('Save failed'); + } + } + + getContent() { + return this.content.innerHTML; + } + + setContent(html) { + this.content.innerHTML = html; + } + + getText() { + return this.content.textContent; + } + + clear() { + this.content.innerHTML = ''; + } + + focus() { + this.content.focus(); + } + + selectImage(img) { + if (this.state.readOnly) return; + + // Deselect previous image + this.deselectImage(); + + this.state.selectedImage = img; + img.classList.add('selected'); + + // Wrap image in resize container if not already wrapped + if (!img.parentElement.classList.contains('rich-editor-resize-wrapper')) { + const wrapper = document.createElement('div'); + wrapper.className = 'rich-editor-resize-wrapper'; + wrapper.contentEditable = false; + + // Store original dimensions if not set + if (!img.dataset.originalWidth) { + img.dataset.originalWidth = img.naturalWidth; + img.dataset.originalHeight = img.naturalHeight; + } + + img.parentNode.insertBefore(wrapper, img); + wrapper.appendChild(img); + } + + const wrapper = img.parentElement; + + // Add resize handles + const corners = ['nw', 'ne', 'sw', 'se']; + corners.forEach(corner => { + const handle = document.createElement('div'); + handle.className = `rich-editor-resize-handle ${corner}`; + handle.dataset.corner = corner; + + // Mouse events + handle.addEventListener('mousedown', (e) => { + e.preventDefault(); + e.stopPropagation(); + this.startResize(e, img, corner); + }); + + // Touch events for mobile + handle.addEventListener('touchstart', (e) => { + e.preventDefault(); + e.stopPropagation(); + const touch = e.touches[0]; + this.startResize(touch, img, corner, true); + }); + + wrapper.appendChild(handle); + }); + + // Add size input + const sizeInput = document.createElement('div'); + sizeInput.className = 'rich-editor-size-input'; + sizeInput.innerHTML = ` + <label>Width:</label> + <input type="number" class="width-input" value="${img.width}" min="50" max="2000"> + <label>px</label> + `; + + const widthInput = sizeInput.querySelector('.width-input'); + widthInput.addEventListener('input', (e) => { + const newWidth = parseInt(e.target.value); + if (newWidth && newWidth > 0) { + this.resizeImage(img, newWidth); + } + }); + + wrapper.appendChild(sizeInput); + } + + deselectImage() { + if (!this.state.selectedImage) return; + + const img = this.state.selectedImage; + img.classList.remove('selected'); + + const wrapper = img.parentElement; + if (wrapper && wrapper.classList.contains('rich-editor-resize-wrapper')) { + // Remove resize handles and size input + wrapper.querySelectorAll('.rich-editor-resize-handle, .rich-editor-size-input').forEach(el => el.remove()); + + // Unwrap image + wrapper.parentNode.insertBefore(img, wrapper); + wrapper.remove(); + } + + this.state.selectedImage = null; + } + + startResize(e, img, corner, isTouch = false) { + this.state.resizing = true; + + const startX = e.clientX; + const startY = e.clientY; + const startWidth = img.width; + const startHeight = img.height; + const aspectRatio = startWidth / startHeight; + + const onMove = (e) => { + if (!this.state.resizing) return; + + const clientX = isTouch ? e.touches[0].clientX : e.clientX; + const clientY = isTouch ? e.touches[0].clientY : e.clientY; + + let deltaX = clientX - startX; + let deltaY = clientY - startY; + + // Adjust delta based on corner + if (corner === 'nw' || corner === 'sw') { + deltaX = -deltaX; + } + if (corner === 'nw' || corner === 'ne') { + deltaY = -deltaY; + } + + // Use the larger delta to maintain aspect ratio + const delta = Math.abs(deltaX) > Math.abs(deltaY) ? deltaX : deltaY; + const newWidth = Math.max(50, startWidth + delta); + + this.resizeImage(img, newWidth, aspectRatio); + }; + + const onEnd = () => { + this.state.resizing = false; + + if (isTouch) { + document.removeEventListener('touchmove', onMove); + document.removeEventListener('touchend', onEnd); + } else { + document.removeEventListener('mousemove', onMove); + document.removeEventListener('mouseup', onEnd); + } + + // Update width input + const wrapper = img.parentElement; + const widthInput = wrapper.querySelector('.width-input'); + if (widthInput) { + widthInput.value = img.width; + } + + if (this.debouncedSave) this.debouncedSave(); + }; + + if (isTouch) { + document.addEventListener('touchmove', onMove, { passive: false }); + document.addEventListener('touchend', onEnd); + } else { + document.addEventListener('mousemove', onMove); + document.addEventListener('mouseup', onEnd); + } + } + + resizeImage(img, newWidth, aspectRatio = null) { + if (!aspectRatio) { + const originalWidth = parseInt(img.dataset.originalWidth) || img.naturalWidth; + const originalHeight = parseInt(img.dataset.originalHeight) || img.naturalHeight; + aspectRatio = originalWidth / originalHeight; + } + + const newHeight = newWidth / aspectRatio; + + img.width = newWidth; + img.height = newHeight; + img.style.width = newWidth + 'px'; + img.style.height = newHeight + 'px'; + } + } + + return { + init: function(elementId, options) { + return new Editor(elementId, options); + } + }; +})(); + +// Export for module systems +if (typeof module !== 'undefined' && module.exports) { + module.exports = RichEditor; +}