chore: update project state and prepare for image support development
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
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>
This commit is contained in:
523
src/dom_renderer.js
Normal file
523
src/dom_renderer.js
Normal file
@@ -0,0 +1,523 @@
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
Reference in New Issue
Block a user