Some checks failed
Test Suite / unit-tests (3.11) (push) Has been cancelled
Test Suite / unit-tests (3.12) (push) Has been cancelled
Test Suite / code-quality (push) Has been cancelled
Test Suite / security-scan (push) Has been cancelled
Test Suite / integration-tests (push) Has been cancelled
Test Suite / e2e-tests (push) Has been cancelled
Test Suite / performance-tests (push) Has been cancelled
Test Suite / test-summary (push) Has been cancelled
- Add comprehensive image test document with various image types - Update project structure with development artifacts - Prepare foundation for image support enhancement phase - Include test files for validating image editing workflows 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
523 lines
17 KiB
JavaScript
523 lines
17 KiB
JavaScript
/**
|
|
* DOM Renderer - Clean separation between business logic and DOM manipulation
|
|
*
|
|
* This class handles all DOM operations and UI events, while delegating
|
|
* business logic to the SectionManager.
|
|
*/
|
|
|
|
// Import EditState if available, otherwise define locally
|
|
const EditState = (typeof window !== 'undefined' && window.MarkitectEditor?.EditState)
|
|
? window.MarkitectEditor.EditState
|
|
: {
|
|
ORIGINAL: 'original',
|
|
EDITING: 'editing',
|
|
MODIFIED: 'modified',
|
|
SAVED: 'saved'
|
|
};
|
|
|
|
/**
|
|
* DOMRenderer class - Handles DOM rendering and UI interactions
|
|
*
|
|
* Responsibilities:
|
|
* - Render sections to DOM elements
|
|
* - Handle UI events (clicks, keyboard)
|
|
* - Manage textarea creation and styling
|
|
* - Sync DOM state with Section objects
|
|
* - Provide visual feedback for different states
|
|
*/
|
|
class DOMRenderer {
|
|
constructor(sectionManager, containerElement) {
|
|
this.sectionManager = sectionManager;
|
|
this.container = containerElement;
|
|
this.currentTextarea = null;
|
|
this.currentSection = null;
|
|
|
|
// Bind event handlers
|
|
this.handleSectionClick = this.handleSectionClick.bind(this);
|
|
this.handleAccept = this.handleAccept.bind(this);
|
|
this.handleCancel = this.handleCancel.bind(this);
|
|
this.handleReset = this.handleReset.bind(this);
|
|
this.handleKeydown = this.handleKeydown.bind(this);
|
|
|
|
// Listen to section manager events
|
|
this.setupEventListeners();
|
|
}
|
|
|
|
/**
|
|
* Setup event listeners for section manager events
|
|
*/
|
|
setupEventListeners() {
|
|
this.sectionManager.on('sections-created', (data) => {
|
|
this.renderAllSections(data.sections);
|
|
});
|
|
|
|
this.sectionManager.on('edit-started', (data) => {
|
|
this.showEditor(data.sectionId, data.content);
|
|
});
|
|
|
|
this.sectionManager.on('edit-stopped', (data) => {
|
|
this.hideEditor(data.sectionId);
|
|
this.updateSectionVisual(data.sectionId, data.section);
|
|
});
|
|
|
|
this.sectionManager.on('changes-accepted', (data) => {
|
|
this.hideEditor(data.sectionId);
|
|
this.updateSectionContent(data.sectionId, data.content);
|
|
this.updateSectionVisual(data.sectionId, data.section);
|
|
});
|
|
|
|
this.sectionManager.on('changes-cancelled', (data) => {
|
|
this.hideEditor(data.sectionId);
|
|
this.updateSectionContent(data.sectionId, data.content);
|
|
this.updateSectionVisual(data.sectionId, data.section);
|
|
});
|
|
|
|
this.sectionManager.on('section-reset', (data) => {
|
|
this.updateTextareaContent(data.content);
|
|
});
|
|
|
|
this.sectionManager.on('content-updated', (data) => {
|
|
this.updateSectionVisual(data.sectionId, data.section);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Render all sections to the DOM
|
|
* @param {Array<Section>} sections - Sections to render
|
|
*/
|
|
renderAllSections(sections) {
|
|
this.container.innerHTML = '';
|
|
|
|
sections.forEach(section => {
|
|
const element = this.createSectionElement(section);
|
|
section.domElement = element;
|
|
this.container.appendChild(element);
|
|
});
|
|
|
|
// Add click handlers to all sections
|
|
this.container.addEventListener('click', this.handleSectionClick);
|
|
}
|
|
|
|
/**
|
|
* Create a DOM element for a section
|
|
* @param {Section} section - The section to create element for
|
|
* @returns {HTMLElement} The created DOM element
|
|
*/
|
|
createSectionElement(section) {
|
|
const element = document.createElement('div');
|
|
element.className = 'markitect-section-editable';
|
|
element.setAttribute('data-section-id', section.id);
|
|
element.setAttribute('data-section-type', section.sectionType);
|
|
|
|
// Parse markdown to HTML - use pending changes if they exist and section isn't being edited
|
|
const contentToRender = (section.state === EditState.MODIFIED && section.pendingMarkdown)
|
|
? section.pendingMarkdown
|
|
: section.currentMarkdown;
|
|
|
|
if (typeof marked !== 'undefined') {
|
|
element.innerHTML = marked.parse(contentToRender);
|
|
} else {
|
|
// Fallback for testing environment
|
|
element.innerHTML = `<p>${contentToRender}</p>`;
|
|
}
|
|
|
|
this.updateSectionVisual(section.id, section.getStatus());
|
|
|
|
return element;
|
|
}
|
|
|
|
/**
|
|
* Update section visual state based on its status
|
|
* @param {string} sectionId - The section ID
|
|
* @param {Object} status - The section status object
|
|
*/
|
|
updateSectionVisual(sectionId, status) {
|
|
const element = this.findSectionElement(sectionId);
|
|
if (!element) return;
|
|
|
|
// Remove all state classes
|
|
element.classList.remove('section-original', 'section-editing', 'section-modified', 'section-saved');
|
|
|
|
// Add appropriate state class
|
|
element.classList.add(`section-${status.state}`);
|
|
|
|
// Add visual indicators for modified sections
|
|
if (status.isModified) {
|
|
element.style.backgroundColor = 'rgba(255, 235, 59, 0.1)'; // Light yellow
|
|
element.style.borderLeft = '3px solid #ffc107'; // Orange border
|
|
} else if (status.state === 'saved') {
|
|
element.style.backgroundColor = 'rgba(76, 175, 80, 0.1)'; // Light green
|
|
element.style.borderLeft = '3px solid #4caf50'; // Green border
|
|
} else {
|
|
element.style.backgroundColor = '';
|
|
element.style.borderLeft = '';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Find DOM element for a section
|
|
* @param {string} sectionId - The section ID
|
|
* @returns {HTMLElement|null} The DOM element or null
|
|
*/
|
|
findSectionElement(sectionId) {
|
|
return this.container.querySelector(`[data-section-id="${sectionId}"]`);
|
|
}
|
|
|
|
/**
|
|
* Handle section click events
|
|
* @param {Event} event - The click event
|
|
*/
|
|
handleSectionClick(event) {
|
|
const sectionElement = event.target.closest('.markitect-section-editable');
|
|
if (!sectionElement) return;
|
|
|
|
const sectionId = sectionElement.getAttribute('data-section-id');
|
|
if (!sectionId) return;
|
|
|
|
// Don't start editing if already editing this section
|
|
if (this.currentSection === sectionId) return;
|
|
|
|
try {
|
|
this.sectionManager.startEditing(sectionId);
|
|
} catch (error) {
|
|
console.error('Failed to start editing:', error);
|
|
this.showError(error.message);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Show editor for a section
|
|
* @param {string} sectionId - The section ID
|
|
* @param {string} content - The content to edit
|
|
*/
|
|
showEditor(sectionId, content) {
|
|
const element = this.findSectionElement(sectionId);
|
|
if (!element) return;
|
|
|
|
// Hide any existing editor first
|
|
this.hideCurrentEditor();
|
|
|
|
// Create editor container
|
|
const editorContainer = this.createEditorContainer(content, sectionId);
|
|
|
|
// Replace section content with editor
|
|
element.innerHTML = '';
|
|
element.appendChild(editorContainer);
|
|
|
|
// Focus the textarea
|
|
const textarea = editorContainer.querySelector('textarea');
|
|
if (textarea) {
|
|
textarea.focus();
|
|
textarea.setSelectionRange(0, 0);
|
|
this.currentTextarea = textarea;
|
|
}
|
|
|
|
this.currentSection = sectionId;
|
|
}
|
|
|
|
/**
|
|
* Create editor container with textarea and controls
|
|
* @param {string} content - The initial content
|
|
* @param {string} sectionId - The section ID
|
|
* @returns {HTMLElement} The editor container
|
|
*/
|
|
createEditorContainer(content, sectionId) {
|
|
const container = document.createElement('div');
|
|
container.className = 'markitect-edit-container';
|
|
|
|
// Create textarea wrapper
|
|
const textareaWrapper = document.createElement('div');
|
|
textareaWrapper.className = 'markitect-textarea-wrapper';
|
|
|
|
// Create textarea
|
|
const textarea = document.createElement('textarea');
|
|
textarea.className = 'edit-mode';
|
|
textarea.value = content;
|
|
textarea.addEventListener('input', () => {
|
|
try {
|
|
this.sectionManager.updateContent(sectionId, textarea.value);
|
|
} catch (error) {
|
|
console.error('Failed to update content:', error);
|
|
}
|
|
});
|
|
|
|
// Add keyboard shortcuts
|
|
textarea.addEventListener('keydown', this.handleKeydown);
|
|
|
|
// Auto-resize functionality
|
|
this.setupAutoResize(textarea);
|
|
|
|
textareaWrapper.appendChild(textarea);
|
|
|
|
// Create controls
|
|
const controls = this.createControlButtons(sectionId);
|
|
|
|
container.appendChild(textareaWrapper);
|
|
container.appendChild(controls);
|
|
|
|
return container;
|
|
}
|
|
|
|
/**
|
|
* Create control buttons for section
|
|
* @param {string} sectionId - The section ID
|
|
* @returns {HTMLElement} The controls container
|
|
*/
|
|
createControlButtons(sectionId) {
|
|
const controls = document.createElement('div');
|
|
controls.className = 'markitect-section-controls';
|
|
|
|
// Accept button
|
|
const acceptBtn = document.createElement('button');
|
|
acceptBtn.className = 'markitect-section-btn accept';
|
|
acceptBtn.innerHTML = '<span>✓</span> Accept';
|
|
acceptBtn.title = 'Accept changes and save this section';
|
|
acceptBtn.addEventListener('click', () => this.handleAccept(sectionId));
|
|
|
|
// Cancel button
|
|
const cancelBtn = document.createElement('button');
|
|
cancelBtn.className = 'markitect-section-btn cancel';
|
|
cancelBtn.innerHTML = '<span>✗</span> Cancel';
|
|
cancelBtn.title = 'Cancel editing and revert to state before editing started';
|
|
cancelBtn.addEventListener('click', () => this.handleCancel(sectionId));
|
|
|
|
// Reset button
|
|
const resetBtn = document.createElement('button');
|
|
resetBtn.className = 'markitect-section-btn reset';
|
|
resetBtn.innerHTML = '<span>🔄</span> Reset';
|
|
resetBtn.title = 'Reset to original content from render time (discards all changes)';
|
|
resetBtn.addEventListener('click', () => this.handleReset(sectionId));
|
|
|
|
controls.appendChild(acceptBtn);
|
|
controls.appendChild(cancelBtn);
|
|
controls.appendChild(resetBtn);
|
|
|
|
return controls;
|
|
}
|
|
|
|
/**
|
|
* Setup auto-resize for textarea
|
|
* @param {HTMLTextAreaElement} textarea - The textarea element
|
|
*/
|
|
setupAutoResize(textarea) {
|
|
const autoResize = () => {
|
|
const transition = textarea.style.transition;
|
|
textarea.style.transition = 'none';
|
|
|
|
textarea.style.height = 'auto';
|
|
const contentHeight = textarea.scrollHeight;
|
|
const padding = 24;
|
|
|
|
const lineCount = textarea.value.split('\n').length;
|
|
const minHeight = Math.max(60, lineCount * 24 + padding);
|
|
const maxHeight = 360;
|
|
|
|
const newHeight = Math.max(60, Math.min(maxHeight, Math.max(minHeight, contentHeight + 4)));
|
|
textarea.style.height = newHeight + 'px';
|
|
|
|
textarea.style.transition = transition;
|
|
};
|
|
|
|
textarea.addEventListener('input', autoResize);
|
|
textarea.addEventListener('paste', () => setTimeout(autoResize, 10));
|
|
|
|
// Initial sizing
|
|
setTimeout(autoResize, 20);
|
|
}
|
|
|
|
/**
|
|
* Hide current editor
|
|
*/
|
|
hideCurrentEditor() {
|
|
if (this.currentSection) {
|
|
this.hideEditor(this.currentSection);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Hide editor for specific section
|
|
* @param {string} sectionId - The section ID
|
|
*/
|
|
hideEditor(sectionId) {
|
|
// Section manager will trigger re-render through events
|
|
this.currentTextarea = null;
|
|
this.currentSection = null;
|
|
}
|
|
|
|
/**
|
|
* Update section content in DOM
|
|
* @param {string} sectionId - The section ID
|
|
* @param {string} content - The new content
|
|
*/
|
|
updateSectionContent(sectionId, content) {
|
|
const element = this.findSectionElement(sectionId);
|
|
if (!element) return;
|
|
|
|
// Parse markdown to HTML
|
|
if (typeof marked !== 'undefined') {
|
|
element.innerHTML = marked.parse(content);
|
|
} else {
|
|
element.innerHTML = `<p>${content}</p>`;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update textarea content
|
|
* @param {string} content - The new content
|
|
*/
|
|
updateTextareaContent(content) {
|
|
if (this.currentTextarea) {
|
|
this.currentTextarea.value = content;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle accept button click
|
|
* @param {string} sectionId - The section ID
|
|
*/
|
|
handleAccept(sectionId) {
|
|
try {
|
|
this.sectionManager.acceptChanges(sectionId);
|
|
} catch (error) {
|
|
console.error('Failed to accept changes:', error);
|
|
this.showError(error.message);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle cancel button click
|
|
* @param {string} sectionId - The section ID
|
|
*/
|
|
handleCancel(sectionId) {
|
|
try {
|
|
this.sectionManager.cancelChanges(sectionId);
|
|
} catch (error) {
|
|
console.error('Failed to cancel changes:', error);
|
|
this.showError(error.message);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle reset button click
|
|
* @param {string} sectionId - The section ID
|
|
*/
|
|
handleReset(sectionId) {
|
|
try {
|
|
this.sectionManager.resetToOriginal(sectionId);
|
|
} catch (error) {
|
|
console.error('Failed to reset section:', error);
|
|
this.showError(error.message);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle keyboard shortcuts
|
|
* @param {KeyboardEvent} event - The keyboard event
|
|
*/
|
|
handleKeydown(event) {
|
|
if (!this.currentSection) return;
|
|
|
|
if (event.ctrlKey || event.metaKey) {
|
|
switch (event.key) {
|
|
case 'Enter':
|
|
event.preventDefault();
|
|
this.handleAccept(this.currentSection);
|
|
break;
|
|
case 'Escape':
|
|
event.preventDefault();
|
|
this.handleCancel(this.currentSection);
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (event.key === 'Escape') {
|
|
event.preventDefault();
|
|
this.handleCancel(this.currentSection);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Show error message to user
|
|
* @param {string} message - The error message
|
|
*/
|
|
showError(message) {
|
|
// Simple error display - could be enhanced with better UI
|
|
console.error('Section Editor Error:', message);
|
|
|
|
// Create temporary error message
|
|
const errorDiv = document.createElement('div');
|
|
errorDiv.className = 'markitect-error';
|
|
errorDiv.textContent = message;
|
|
errorDiv.style.cssText = `
|
|
position: fixed;
|
|
top: 20px;
|
|
right: 20px;
|
|
background: #f44336;
|
|
color: white;
|
|
padding: 12px 20px;
|
|
border-radius: 4px;
|
|
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
|
|
z-index: 10000;
|
|
font-size: 14px;
|
|
max-width: 300px;
|
|
`;
|
|
|
|
document.body.appendChild(errorDiv);
|
|
|
|
// Remove after 5 seconds
|
|
setTimeout(() => {
|
|
if (errorDiv.parentNode) {
|
|
errorDiv.parentNode.removeChild(errorDiv);
|
|
}
|
|
}, 5000);
|
|
}
|
|
|
|
/**
|
|
* Reset all sections to original state
|
|
*/
|
|
resetAllSections() {
|
|
try {
|
|
this.sectionManager.resetAllToOriginal();
|
|
this.hideCurrentEditor();
|
|
// Re-render all sections
|
|
const sections = this.sectionManager.getAllSections();
|
|
this.renderAllSections(sections);
|
|
} catch (error) {
|
|
console.error('Failed to reset all sections:', error);
|
|
this.showError(error.message);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get document status for UI display
|
|
* @returns {Object} Document status
|
|
*/
|
|
getDocumentStatus() {
|
|
return this.sectionManager.getDocumentStatus();
|
|
}
|
|
|
|
/**
|
|
* Get complete document markdown
|
|
* @returns {string} The markdown document
|
|
*/
|
|
getDocumentMarkdown() {
|
|
return this.sectionManager.getDocumentMarkdown();
|
|
}
|
|
|
|
/**
|
|
* Destroy the renderer and clean up event listeners
|
|
*/
|
|
destroy() {
|
|
this.container.removeEventListener('click', this.handleSectionClick);
|
|
this.hideCurrentEditor();
|
|
this.container.innerHTML = '';
|
|
}
|
|
}
|
|
|
|
// Export for testing and usage
|
|
if (typeof module !== 'undefined' && module.exports) {
|
|
module.exports = { DOMRenderer };
|
|
} else {
|
|
window.MarkitectEditor = window.MarkitectEditor || {};
|
|
window.MarkitectEditor.DOMRenderer = DOMRenderer;
|
|
} |