diff --git a/markitect/static/js/components/dom-renderer.js b/markitect/static/js/components/dom-renderer.js index d0e8bdcd..c302f80f 100644 --- a/markitect/static/js/components/dom-renderer.js +++ b/markitect/static/js/components/dom-renderer.js @@ -396,47 +396,391 @@ class DOMRenderer { } /** - * Show editor for image sections + * Show advanced image editor with drag & drop, file upload, and preview */ showImageEditor(sectionId, section) { debug('showImageEditor: called for image section: ' + sectionId, 'EDITOR'); + // Track staging state for this editor + const stagingState = { + originalMarkdown: section.currentMarkdown, + currentAltText: '', + currentImageSrc: '', + stagedImageSrc: null, + stagedAltText: null, + hasChanges: false + }; + + // Parse markdown to extract image info + const imageMatch = section.currentMarkdown.match(/!\[(.*?)\]\((.*?)\)/); + if (imageMatch) { + const [, altText, imageSrc] = imageMatch; + stagingState.currentAltText = altText; + stagingState.currentImageSrc = imageSrc; + } + + // Create image editor content area const editorContent = document.createElement('div'); - editorContent.innerHTML = ` -
- Image Editor
- Edit the markdown for this image: -
- -
- - -
+ editorContent.className = 'ui-edit-image-content'; + editorContent.style.cssText = ` + display: flex; + flex-direction: column; + gap: 15px; + flex: 1; + min-width: 0; `; + // Image preview with drop zone + const imagePreview = document.createElement('div'); + imagePreview.className = 'ui-edit-image-preview'; + imagePreview.style.cssText = ` + width: 100%; + height: 180px; + text-align: center; + background: white; + padding: 12px; + border-radius: 8px; + border: 2px dashed #007bff; + transition: all 0.3s ease; + cursor: pointer; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + position: relative; + box-sizing: border-box; + overflow: hidden; + `; + + // Function to update image preview + const updateImagePreview = (imageSrc, altText) => { + imagePreview.innerHTML = ''; + + if (imageSrc) { + const img = document.createElement('img'); + img.src = imageSrc; + img.alt = altText || ''; + img.style.cssText = ` + max-width: 100%; + max-height: 150px; + border-radius: 4px; + box-shadow: 0 2px 8px rgba(0,0,0,0.1); + `; + imagePreview.appendChild(img); + + // Add overlay for drop zone + const overlay = document.createElement('div'); + overlay.className = 'drop-overlay'; + overlay.style.cssText = ` + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 123, 255, 0.1); + border-radius: 6px; + display: none; + align-items: center; + justify-content: center; + color: #007bff; + font-weight: bold; + font-size: 16px; + `; + overlay.textContent = '📁 Drop new image here'; + imagePreview.appendChild(overlay); + } else { + // Show drop zone placeholder + const placeholder = document.createElement('div'); + placeholder.style.cssText = ` + text-align: center; + color: #6c757d; + font-size: 14px; + `; + placeholder.innerHTML = ` +
📁
+
Drop image here or click to select
+
Supports JPG, PNG, GIF, WebP
+ `; + imagePreview.appendChild(placeholder); + } + }; + + // Initialize preview + updateImagePreview(stagingState.currentImageSrc, stagingState.currentAltText); + + // File input for image selection + const fileInput = document.createElement('input'); + fileInput.type = 'file'; + fileInput.accept = 'image/*'; + fileInput.style.display = 'none'; + + // Function to handle image file selection + const handleImageFile = (file) => { + if (file && file.type.startsWith('image/')) { + const reader = new FileReader(); + reader.onload = (event) => { + stagingState.stagedImageSrc = event.target.result; + stagingState.hasChanges = true; + updateImagePreview(stagingState.stagedImageSrc, altTextInput.value); + updateChangeIndicator(); + }; + reader.readAsDataURL(file); + } + }; + + // Drag and drop functionality + imagePreview.addEventListener('dragover', (e) => { + e.preventDefault(); + imagePreview.style.borderColor = '#28a745'; + imagePreview.style.backgroundColor = '#f8fff8'; + const overlay = imagePreview.querySelector('.drop-overlay'); + if (overlay) overlay.style.display = 'flex'; + }); + + imagePreview.addEventListener('dragleave', (e) => { + e.preventDefault(); + imagePreview.style.borderColor = '#007bff'; + imagePreview.style.backgroundColor = 'white'; + const overlay = imagePreview.querySelector('.drop-overlay'); + if (overlay) overlay.style.display = 'none'; + }); + + imagePreview.addEventListener('drop', (e) => { + e.preventDefault(); + imagePreview.style.borderColor = '#007bff'; + imagePreview.style.backgroundColor = 'white'; + const overlay = imagePreview.querySelector('.drop-overlay'); + if (overlay) overlay.style.display = 'none'; + + const files = e.dataTransfer.files; + if (files.length > 0) { + handleImageFile(files[0]); + } + }); + + // Click to select file + imagePreview.addEventListener('click', () => { + fileInput.click(); + }); + + fileInput.addEventListener('change', (e) => { + if (e.target.files.length > 0) { + handleImageFile(e.target.files[0]); + } + }); + + // Alt text editor + const altTextContainer = document.createElement('div'); + altTextContainer.className = 'ui-edit-alt-text-container'; + altTextContainer.style.cssText = ` + display: flex; + flex-direction: column; + gap: 8px; + `; + + const altTextLabel = document.createElement('label'); + altTextLabel.textContent = 'Alt Text Description:'; + altTextLabel.style.cssText = ` + font-size: 13px; + font-weight: 600; + color: #333; + margin: 0; + `; + + const altTextInput = document.createElement('input'); + altTextInput.type = 'text'; + altTextInput.value = stagingState.currentAltText; + altTextInput.style.cssText = ` + width: 100%; + padding: 10px 12px; + border: 1px solid #ddd; + border-radius: 6px; + font-size: 14px; + box-sizing: border-box; + outline: none; + transition: border-color 0.2s ease; + `; + + altTextInput.addEventListener('focus', () => { + altTextInput.style.borderColor = '#007bff'; + }); + + altTextInput.addEventListener('blur', () => { + altTextInput.style.borderColor = '#ddd'; + }); + + // Track alt text changes + altTextInput.addEventListener('input', () => { + stagingState.stagedAltText = altTextInput.value; + stagingState.hasChanges = altTextInput.value !== stagingState.currentAltText || stagingState.stagedImageSrc !== null; + updateChangeIndicator(); + }); + + altTextContainer.appendChild(altTextLabel); + altTextContainer.appendChild(altTextInput); + + // Change indicator + const changeIndicator = document.createElement('div'); + changeIndicator.className = 'change-indicator'; + changeIndicator.style.cssText = ` + padding: 8px 12px; + background: #fff3cd; + border: 1px solid #ffeaa7; + border-radius: 6px; + color: #856404; + font-size: 12px; + text-align: center; + display: none; + font-weight: 500; + `; + changeIndicator.textContent = '⚠️ You have unsaved changes'; + + const updateChangeIndicator = () => { + if (stagingState.hasChanges) { + changeIndicator.style.display = 'block'; + } else { + changeIndicator.style.display = 'none'; + } + }; + + // Assemble content + editorContent.appendChild(imagePreview); + editorContent.appendChild(altTextContainer); + editorContent.appendChild(changeIndicator); + editorContent.appendChild(fileInput); + + // Create controls + const controls = document.createElement('div'); + controls.className = 'ui-edit-controls'; + controls.style.cssText = ` + display: flex; + flex-direction: column; + gap: 8px; + width: 100%; + `; + + const acceptBtn = document.createElement('button'); + acceptBtn.textContent = '✓ Accept'; + acceptBtn.style.cssText = ` + padding: 8px 12px; + font-size: 12px; + border-radius: 6px; + border: none; + color: white; + cursor: pointer; + font-weight: 600; + transition: all 0.2s ease; + width: 100%; + text-align: center; + background: #28a745; + `; + + const cancelBtn = document.createElement('button'); + cancelBtn.textContent = '✗ Cancel'; + cancelBtn.style.cssText = ` + padding: 8px 12px; + font-size: 12px; + border-radius: 6px; + border: none; + color: white; + cursor: pointer; + font-weight: 600; + transition: all 0.2s ease; + width: 100%; + text-align: center; + background: #dc3545; + `; + + const resetBtn = document.createElement('button'); + resetBtn.textContent = '↺ Reset'; + resetBtn.style.cssText = ` + padding: 8px 12px; + font-size: 12px; + border-radius: 6px; + border: none; + color: white; + cursor: pointer; + font-weight: 600; + transition: all 0.2s ease; + width: 100%; + text-align: center; + background: #fd7e14; + `; + + controls.appendChild(acceptBtn); + controls.appendChild(cancelBtn); + controls.appendChild(resetBtn); + + // Event handlers + acceptBtn.addEventListener('click', () => { + // Apply staged changes only when accept is clicked + if (stagingState.hasChanges) { + let newMarkdown = stagingState.originalMarkdown; + + // Apply image source change if staged + if (stagingState.stagedImageSrc !== null) { + const currentImageMatch = newMarkdown.match(/!\[(.*?)\]\((.*?)\)/); + if (currentImageMatch) { + newMarkdown = newMarkdown.replace( + /!\[(.*?)\]\((.*?)\)/, + `![${currentImageMatch[1]}](${stagingState.stagedImageSrc})` + ); + } + } + + // Apply alt text change if staged + if (stagingState.stagedAltText !== null) { + newMarkdown = newMarkdown.replace( + /!\[(.*?)\]/, + `![${stagingState.stagedAltText}]` + ); + } + + // Update section with final changes + this.sectionManager.updateContent(sectionId, newMarkdown); + } + + // Accept changes and hide editor + this.sectionManager.acceptChanges(sectionId); + this.currentFloatingMenu.hide(); + this.currentFloatingMenu = null; + }); + + cancelBtn.addEventListener('click', () => { + // Discard all staged changes and hide editor + this.sectionManager.cancelChanges(sectionId); + this.currentFloatingMenu.hide(); + this.currentFloatingMenu = null; + }); + + resetBtn.addEventListener('click', () => { + // Reset to original content + const originalImageMatch = stagingState.originalMarkdown.match(/!\[(.*?)\]\((.*?)\)/); + if (originalImageMatch) { + const [, originalAltText, originalImageSrc] = originalImageMatch; + stagingState.currentAltText = originalAltText; + stagingState.currentImageSrc = originalImageSrc; + } + + // Clear any staged changes + stagingState.stagedImageSrc = null; + stagingState.stagedAltText = null; + stagingState.hasChanges = false; + + // Reset alt text input to original + altTextInput.value = stagingState.currentAltText; + + // Reset preview to original + updateImagePreview(stagingState.currentImageSrc, stagingState.currentAltText); + updateChangeIndicator(); + }); + + // Create floating menu const floatingMenu = new FloatingMenu(sectionId, 'image', this); this.currentFloatingMenu = floatingMenu; this.editingSections.add(sectionId); - floatingMenu.show(editorContent); - - // Add event listeners - const textarea = editorContent.querySelector('textarea'); - const acceptBtn = editorContent.querySelector('#accept-image'); - const cancelBtn = editorContent.querySelector('#cancel-image'); - - acceptBtn.addEventListener('click', () => { - this.sectionManager.updateContent(sectionId, textarea.value); - this.sectionManager.acceptChanges(sectionId); - floatingMenu.hide(); - this.currentFloatingMenu = null; // Clear reference - }); - - cancelBtn.addEventListener('click', () => { - this.sectionManager.cancelChanges(sectionId); - floatingMenu.hide(); - this.currentFloatingMenu = null; // Clear reference - }); + floatingMenu.show(editorContent, controls); } /**