diff --git a/capabilities/testdrive-jsui b/capabilities/testdrive-jsui
index 891d7855..f5ce02cf 160000
--- a/capabilities/testdrive-jsui
+++ b/capabilities/testdrive-jsui
@@ -1 +1 @@
-Subproject commit 891d78553391928fac5230de2d6fae1e9a25af65
+Subproject commit f5ce02cf8d8ad38fce77fa70b266216340bc4489
diff --git a/markitect/static/editor.js b/markitect/static/editor.js
deleted file mode 100644
index a724c4c8..00000000
--- a/markitect/static/editor.js
+++ /dev/null
@@ -1,5189 +0,0 @@
-// Clean Editor Architecture
-/**
- * Test-Driven Section Editor Implementation
- *
- * A clean, object-oriented approach to handling section editing
- * that can be tested independently of the DOM.
- */
-
-// Debug system - choose one of: 'off', 'console', 'alerts'
-const DEBUG_MODE = 'console';
-
-// Advanced State Management
-const EditState = Object.freeze({
- ORIGINAL: 'original',
- EDITING: 'editing',
- MODIFIED: 'modified',
- SAVED: 'saved'
-});
-
-/**
- * Reusable FloatingMenu Component
- * Provides a consistent floating menu system for both text and image editing
- */
-class FloatingMenu {
- constructor(sectionId, type, renderer) {
- this.sectionId = sectionId;
- this.type = type; // 'text' or 'image'
- this.renderer = renderer;
- this.element = null;
- this.dragHandle = null;
- this.contentArea = null;
- this.controlsArea = null;
- this.isDragging = false;
- this.isVisible = false;
- }
-
- /**
- * Create and show the floating menu
- * @param {HTMLElement} contentElement - Content to display in the menu
- * @param {HTMLElement} controlsElement - Controls to display in the menu
- * @returns {HTMLElement} The floating menu element
- */
- show(contentElement, controlsElement) {
- if (this.isVisible) this.hide();
-
- const targetElement = this.renderer.findSectionElement(this.sectionId);
- if (!targetElement) return null;
-
- // Calculate positioning
- const positioning = this._calculatePositioning(targetElement);
-
- // Create menu container
- this.element = this._createMenuContainer(positioning);
-
- // Create drag handle
- this.dragHandle = this._createDragHandle();
-
- // Create content and controls areas
- this.contentArea = this._createContentArea();
- this.controlsArea = this._createControlsArea();
-
- // Create wrapper for content that will take most of the space
- const contentWrapper = document.createElement('div');
- contentWrapper.style.cssText = `
- flex: 1;
- display: flex;
- flex-direction: column;
- gap: 10px;
- min-width: 0;
- overflow: hidden;
- `;
- contentWrapper.appendChild(contentElement);
-
- // Add content and controls to content area (side by side)
- this.contentArea.appendChild(contentWrapper);
- this.controlsArea.appendChild(controlsElement);
- this.contentArea.appendChild(this.controlsArea);
-
- // Assemble menu
- this.element.appendChild(this.dragHandle);
- this.element.appendChild(this.contentArea);
-
- // Apply highlighting to target element
- this._highlightTargetElement(targetElement);
-
- // Make draggable
- this._makeDraggable();
-
- // Add to DOM
- document.body.appendChild(this.element);
- this.isVisible = true;
-
- return this.element;
- }
-
- /**
- * Hide and remove the floating menu
- */
- hide() {
- if (!this.isVisible) return;
-
- // Stop editing state in the section manager
- const section = this.renderer.sectionManager.sections.get(this.sectionId);
- if (section && section.isEditing()) {
- section.stopEditing();
- }
-
- // Remove highlighting from target element
- const targetElement = this.renderer.findSectionElement(this.sectionId);
- if (targetElement) {
- this._removeHighlighting(targetElement);
- }
-
- // Remove section from editing set
- this.renderer.editingSections.delete(this.sectionId);
-
- // Remove from DOM
- if (this.element && this.element.parentElement) {
- this.element.remove();
- }
-
- this.element = null;
- this.dragHandle = null;
- this.contentArea = null;
- this.controlsArea = null;
- this.isVisible = false;
- }
-
- /**
- * Calculate optimal positioning for the floating menu
- * @param {HTMLElement} targetElement - Element being edited
- * @returns {Object} Positioning information
- */
- _calculatePositioning(targetElement) {
- const elementRect = targetElement.getBoundingClientRect();
- const scrollX = window.pageXOffset || document.documentElement.scrollLeft;
- const scrollY = window.pageYOffset || document.documentElement.scrollTop;
- const viewportWidth = window.innerWidth;
-
- // Calculate content width and positioning
- const elementWidth = elementRect.width;
- const controlsWidth = 180; // Width needed for controls on the right
- const minMenuWidth = 400; // Increased minimum to accommodate side-by-side layout
- const maxMenuWidth = Math.min(800, viewportWidth - 40);
-
- // Always use integrated layout with controls on the right side of content
- let menuWidth, menuLeft;
-
- if (elementWidth + controlsWidth >= minMenuWidth && elementWidth + controlsWidth <= maxMenuWidth) {
- // Use element width + controls width
- menuWidth = elementWidth + controlsWidth;
- } else if (elementWidth + controlsWidth > maxMenuWidth) {
- // Use maximum width
- menuWidth = maxMenuWidth;
- } else {
- // Use minimum width
- menuWidth = minMenuWidth;
- }
-
- menuLeft = elementRect.left + scrollX;
-
- return {
- width: menuWidth,
- left: menuLeft,
- top: elementRect.top + scrollY,
- height: Math.max(200, elementRect.height),
- controlsWidth: controlsWidth
- };
- }
-
- /**
- * Create the main menu container
- * @param {Object} positioning - Positioning information
- * @returns {HTMLElement} Menu container element
- */
- _createMenuContainer(positioning) {
- const container = document.createElement('div');
- container.className = 'ui-edit-floating-menu';
- container.dataset.sectionId = this.sectionId;
- container.dataset.editType = this.type;
- container.style.cssText = `
- position: fixed;
- top: ${positioning.top}px;
- left: ${positioning.left}px;
- width: ${positioning.width}px;
- min-height: ${positioning.height}px;
- max-height: 600px;
- z-index: 10000;
- background: rgba(255, 255, 255, 0.98);
- border: 2px solid #007bff;
- border-radius: 12px;
- box-shadow: 0 8px 32px rgba(0, 123, 255, 0.25);
- display: flex;
- flex-direction: column;
- overflow: hidden;
- backdrop-filter: blur(10px);
- `;
-
- // Store positioning info for controls layout
- container._positioning = positioning;
-
- return container;
- }
-
- /**
- * Create the drag handle with X button
- * @returns {HTMLElement} Drag handle element
- */
- _createDragHandle() {
- const handle = document.createElement('div');
- handle.className = 'ui-edit-drag-handle';
- handle.style.cssText = `
- padding: 10px 15px;
- background: #007bff;
- color: white;
- font-size: 12px;
- font-weight: bold;
- cursor: move;
- user-select: none;
- border-radius: 10px 10px 0 0;
- display: flex;
- align-items: center;
- justify-content: space-between;
- position: relative;
- `;
-
- // Left side: icon and title
- const leftContent = document.createElement('div');
- leftContent.style.cssText = `
- display: flex;
- align-items: center;
- gap: 8px;
- `;
-
- const icon = this.type === 'image' ? '๐ผ๏ธ' : '๐';
- leftContent.innerHTML = `${icon} Drag to Move โข Editing ${this.type === 'image' ? 'Image' : 'Text'} `;
-
- // Right side: X button
- const closeButton = document.createElement('button');
- closeButton.className = 'ui-edit-close-button';
- closeButton.innerHTML = 'โ';
- closeButton.style.cssText = `
- background: none;
- border: none;
- color: white;
- font-size: 16px;
- font-weight: bold;
- cursor: pointer;
- padding: 0;
- width: 20px;
- height: 20px;
- display: flex;
- align-items: center;
- justify-content: center;
- border-radius: 3px;
- transition: background-color 0.2s ease;
- `;
-
- // Hover effect for close button
- closeButton.addEventListener('mouseenter', () => {
- closeButton.style.backgroundColor = 'rgba(255, 255, 255, 0.2)';
- });
- closeButton.addEventListener('mouseleave', () => {
- closeButton.style.backgroundColor = 'transparent';
- });
-
- // Close functionality
- closeButton.addEventListener('click', (e) => {
- e.stopPropagation(); // Prevent dragging when clicking close
- this.hide();
- });
-
- handle.appendChild(leftContent);
- handle.appendChild(closeButton);
-
- return handle;
- }
-
- /**
- * Create the content area with horizontal layout
- * @returns {HTMLElement} Content area element
- */
- _createContentArea() {
- const area = document.createElement('div');
- area.className = 'ui-edit-content-area';
- area.style.cssText = `
- padding: 15px;
- flex: 1;
- display: flex;
- flex-direction: row;
- gap: 15px;
- overflow: hidden;
- `;
- return area;
- }
-
- /**
- * Create the controls area for side-by-side layout
- * @returns {HTMLElement} Controls area element
- */
- _createControlsArea() {
- const area = document.createElement('div');
- area.className = 'ui-edit-controls-area';
- area.style.cssText = `
- width: 160px;
- display: flex;
- flex-direction: column;
- gap: 10px;
- justify-content: flex-start;
- align-items: stretch;
- flex-shrink: 0;
- `;
- return area;
- }
-
- /**
- * Apply highlighting to the target element
- * @param {HTMLElement} targetElement - Element to highlight
- */
- _highlightTargetElement(targetElement) {
- targetElement.style.outline = '3px solid #007bff';
- targetElement.style.outlineOffset = '2px';
- targetElement.style.backgroundColor = 'rgba(0, 123, 255, 0.08)';
- targetElement.style.transition = 'all 0.2s ease';
- }
-
- /**
- * Remove highlighting from the target element
- * @param {HTMLElement} targetElement - Element to remove highlighting from
- */
- _removeHighlighting(targetElement) {
- targetElement.style.outline = '';
- targetElement.style.outlineOffset = '';
- targetElement.style.backgroundColor = '';
- targetElement.style.transition = '';
- }
-
- /**
- * Make the floating menu draggable
- */
- _makeDraggable() {
- let startX, startY, initialX, initialY;
-
- this.dragHandle.addEventListener('mousedown', (e) => {
- // Don't start dragging if clicking on the close button
- if (e.target.closest('.ui-edit-close-button')) {
- return;
- }
-
- this.isDragging = true;
- startX = e.clientX;
- startY = e.clientY;
-
- const rect = this.element.getBoundingClientRect();
- initialX = rect.left;
- initialY = rect.top;
-
- this.element.style.cursor = 'grabbing';
- this.dragHandle.style.cursor = 'grabbing';
-
- e.preventDefault();
- });
-
- document.addEventListener('mousemove', (e) => {
- if (!this.isDragging) return;
-
- const deltaX = e.clientX - startX;
- const deltaY = e.clientY - startY;
-
- const newX = initialX + deltaX;
- const newY = initialY + deltaY;
-
- // Keep within viewport bounds
- const maxX = window.innerWidth - this.element.offsetWidth;
- const maxY = window.innerHeight - this.element.offsetHeight;
-
- this.element.style.left = Math.max(0, Math.min(newX, maxX)) + 'px';
- this.element.style.top = Math.max(0, Math.min(newY, maxY)) + 'px';
- });
-
- document.addEventListener('mouseup', () => {
- if (this.isDragging) {
- this.isDragging = false;
- this.element.style.cursor = 'move';
- this.dragHandle.style.cursor = 'move';
- }
- });
- }
-}
-
-// Global debug message storage - reset on page load
-let debugMessages = [];
-let debugPanelActive = false;
-
-// Clear any messages that might have been captured during rendering
-if (typeof window !== 'undefined') {
- debugMessages = [];
-}
-
-function debug(message, category = 'INFO') {
- const timestamp = new Date().toLocaleTimeString();
- const prefix = `DEBUG ${category}:`;
- const fullMessage = `[${timestamp}] ${prefix} ${message}`;
-
- // Always store messages for the debug panel
- debugMessages.push({
- timestamp,
- category,
- message,
- fullMessage
- });
-
- // Keep only last 100 messages to prevent memory issues
- if (debugMessages.length > 100) {
- debugMessages = debugMessages.slice(-100);
- }
-
- // Update debug panel if active
- if (debugPanelActive) {
- console.log('Debug panel is active, updating...'); // Debug log
- if (window.updateDebugPanel) {
- console.log('Calling window.updateDebugPanel'); // Debug log
- window.updateDebugPanel();
- } else {
- console.log('window.updateDebugPanel not available'); // Debug log
- }
- } else {
- console.log('Debug panel not active, debugPanelActive =', debugPanelActive); // Debug log
- }
-
- switch (DEBUG_MODE) {
- case 'off':
- // No debugging output to console
- break;
- case 'console':
- console.log(prefix, message);
- break;
- case 'alerts':
- alert(`${prefix} ${message}`);
- console.log(prefix, message); // Also log to console for reference
- break;
- default:
- console.warn('Invalid DEBUG_MODE. Use: off, console, or alerts');
- }
-}
-
-// Global functions for debug panel
-window.updateDebugPanel = function() {
- // Find the DOMRenderer instance and call its updateDebugPanel method
- if (window.currentDOMRenderer && typeof window.currentDOMRenderer.updateDebugPanel === 'function') {
- window.currentDOMRenderer.updateDebugPanel();
- }
-};
-
-window.clearDebugMessages = function() {
- console.log('clearDebugMessages called, current messages:', debugMessages.length);
- debugMessages = [];
- console.log('Messages cleared, new length:', debugMessages.length);
- if (debugPanelActive && window.currentDOMRenderer && typeof window.currentDOMRenderer.updateDebugPanel === 'function') {
- console.log('Updating debug panel after clear');
- window.currentDOMRenderer.updateDebugPanel();
- } else {
- console.log('Cannot update debug panel after clear:', {
- debugPanelActive,
- hasRenderer: !!window.currentDOMRenderer,
- hasUpdateMethod: !!(window.currentDOMRenderer && window.currentDOMRenderer.updateDebugPanel)
- });
- }
-};
-
-// Enums for clear state management (already defined above)
-
-const SectionType = Object.freeze({
- HEADING: 'heading',
- PARAGRAPH: 'paragraph',
- LIST: 'list',
- CODE: 'code',
- QUOTE: 'quote',
- IMAGE: 'image',
- TABLE: 'table',
- HR: 'hr',
- OTHER: 'other'
-});
-
-/**
- * Section class - Represents a single editable section
- */
-class Section {
- constructor(id, markdown, type) {
- this.id = id;
- this.originalMarkdown = markdown;
- this.currentMarkdown = markdown;
- this.editingMarkdown = markdown;
- this.pendingMarkdown = null;
- this.type = type;
- this.state = EditState.ORIGINAL;
- this.domElement = null;
- this.lastSaved = null;
- this.created = new Date();
- }
-
- /**
- * Generate sophisticated section ID with hash-based algorithm
- * @param {string} markdown - Section content
- * @param {number} position - Position in document
- * @param {string} strategy - ID generation strategy ('hash', 'timestamp', 'sequential', 'hierarchical')
- * @param {string} parentId - Parent section ID for hierarchical strategy
- * @returns {string} Generated section ID
- */
- static generateId(markdown, position, strategy = 'hash', parentId = null) {
- return this.generateIdWithStrategy(markdown, position, strategy, parentId);
- }
-
- /**
- * Generate ID with specific strategy
- * @param {string} markdown - Section content
- * @param {number} position - Position in document
- * @param {string} strategy - Generation strategy
- * @param {string} parentId - Parent ID for hierarchical
- * @returns {string} Generated ID
- */
- static generateIdWithStrategy(markdown, position, strategy = 'hash', parentId = null) {
- const sanitizedContent = this.sanitizeContentForId(markdown);
- const normalizedContent = this.normalizeContentForHashing(sanitizedContent);
- const sectionType = this.detectType(markdown);
-
- switch (strategy) {
- case 'timestamp':
- return this.generateTimestampId(normalizedContent, position, sectionType);
- case 'sequential':
- return this.generateSequentialId(normalizedContent, position, sectionType);
- case 'hierarchical':
- return this.generateHierarchicalId(normalizedContent, position, parentId);
- case 'hash':
- default:
- return this.generateAdvancedId(normalizedContent, position, sectionType);
- }
- }
-
- /**
- * Generate advanced hash-based ID with section type
- * @param {string} content - Normalized content
- * @param {number} position - Position
- * @param {string} sectionType - Detected section type
- * @returns {string} Advanced ID
- */
- static generateAdvancedId(content, position, sectionType) {
- const contentHash = this.generateCryptoHash(content);
- const safeType = sectionType || 'paragraph';
- const typePrefix = safeType.substring(0, 3); // First 3 chars of type
- const positionHex = position.toString(16).padStart(2, '0');
-
- return `section-${typePrefix}-${contentHash}-${positionHex}`;
- }
-
- /**
- * Generate cryptographic hash for content fingerprinting
- * @param {string} content - Content to hash
- * @returns {string} Hex hash string
- */
- static generateCryptoHash(content) {
- // Simple but effective hash function for browser compatibility
- let hash = 0;
- if (content.length === 0) return '00000000';
-
- for (let i = 0; i < content.length; i++) {
- const char = content.charCodeAt(i);
- hash = ((hash << 5) - hash) + char;
- hash = hash & hash; // Convert to 32-bit integer
- }
-
- // Convert to positive hex string
- const hexHash = Math.abs(hash).toString(16).padStart(8, '0');
- return hexHash.substring(0, 8);
- }
-
- /**
- * Normalize content for consistent hashing
- * @param {string} content - Raw content
- * @returns {string} Normalized content
- */
- static normalizeContentForHashing(content) {
- if (!content || typeof content !== 'string') {
- return '';
- }
-
- return content
- .trim() // Remove leading/trailing whitespace
- .replace(/\s+/g, ' ') // Normalize whitespace
- .replace(/\r\n/g, '\n') // Normalize line endings
- .toLowerCase(); // Case insensitive
- }
-
- /**
- * Sanitize content for safe ID generation
- * @param {string} content - Raw content
- * @returns {string} Sanitized content
- */
- static sanitizeContentForId(content) {
- if (!content || typeof content !== 'string') {
- return '';
- }
-
- // Remove potentially dangerous characters and HTML
- return content
- .replace(/<[^>]*>/g, '') // Remove HTML tags
- .replace(/javascript:/gi, '') // Remove javascript: protocol
- .replace(/[^\w\s\-_.#]/g, '') // Keep only safe characters
- .trim();
- }
-
- /**
- * Generate timestamp-based ID for temporal uniqueness
- * @param {string} content - Content
- * @param {number} position - Position
- * @param {string} sectionType - Section type
- * @returns {string} Timestamp-based ID
- */
- static generateTimestampId(content, position = 0, sectionType = 'paragraph') {
- const timestamp = Date.now().toString(36); // Base-36 timestamp
- const contentSnippet = this.generateCryptoHash(content || '').substring(0, 4);
- const safeType = sectionType || 'paragraph';
- const typePrefix = safeType.substring(0, 3);
-
- return `section-${typePrefix}-${contentSnippet}-${timestamp}`;
- }
-
- /**
- * Generate sequential ID
- * @param {string} content - Content
- * @param {number} position - Position
- * @param {string} sectionType - Section type
- * @returns {string} Sequential ID
- */
- static generateSequentialId(content, position, sectionType = 'paragraph') {
- const safeType = sectionType || 'paragraph';
- const typePrefix = safeType.substring(0, 3);
- const seqNumber = (position || 0).toString().padStart(3, '0');
- const contentHash = this.generateCryptoHash(content || '').substring(0, 4);
-
- return `section-${typePrefix}-seq${seqNumber}-${contentHash}`;
- }
-
- /**
- * Generate hierarchical ID for nested sections
- * @param {string} content - Content
- * @param {number} position - Position
- * @param {string} parentId - Parent section ID
- * @returns {string} Hierarchical ID
- */
- static generateHierarchicalId(content, position, parentId = null) {
- const contentHash = this.generateCryptoHash(content || '').substring(0, 6);
-
- if (parentId) {
- const childIndex = (position || 0).toString().padStart(2, '0');
- return `${parentId}-child-${childIndex}-${contentHash}`;
- } else {
- return `section-root-${position || 0}-${contentHash}`;
- }
- }
-
- /**
- * Detect ID collision
- * @param {string} id - Proposed ID
- * @param {Set} existingIds - Set of existing IDs
- * @returns {boolean} True if collision detected
- */
- static detectIdCollision(id, existingIds) {
- return existingIds && existingIds.has(id);
- }
-
- /**
- * Resolve ID collision by generating alternative
- * @param {string} id - Colliding ID
- * @param {Set} existingIds - Set of existing IDs
- * @returns {string} Resolved unique ID
- */
- static resolveIdCollision(id, existingIds) {
- let counter = 1;
- let resolvedId = `${id}-${counter}`;
-
- while (existingIds.has(resolvedId)) {
- counter++;
- resolvedId = `${id}-${counter}`;
- }
-
- return resolvedId;
- }
-
- /**
- * Analyze section ID and extract metadata
- * @param {string} id - Section ID to analyze
- * @returns {Object} ID metadata
- */
- static analyzeId(id) {
- if (!id || !id.startsWith('section-')) {
- return { id, valid: false };
- }
-
- const parts = id.split('-');
- const analysis = {
- id,
- valid: true,
- prefix: parts[0], // 'section'
- format: 'unknown'
- };
-
- if (parts.length >= 4) {
- // Advanced format: section-{type}-{hash}-{position}
- analysis.format = 'advanced';
- analysis.type = parts[1];
- analysis.hash = parts[2];
- analysis.position = parseInt(parts[3], 16) || 0;
- } else if (parts.length === 3 && parts[1] === 'root') {
- // Hierarchical root format
- analysis.format = 'hierarchical-root';
- analysis.level = 'root';
- analysis.position = parseInt(parts[2]) || 0;
- } else if (id.includes('child')) {
- // Hierarchical child format
- analysis.format = 'hierarchical-child';
- analysis.level = 'child';
- } else if (id.includes('seq')) {
- // Sequential format
- analysis.format = 'sequential';
- } else {
- // Basic or legacy format
- analysis.format = 'basic';
- analysis.hash = parts.slice(1).join('-');
- }
-
- return analysis;
- }
-
- static detectType(markdown) {
- if (!markdown || typeof markdown !== 'string') {
- return SectionType.PARAGRAPH;
- }
-
- // Don't trim the entire markdown - preserve leading indentation
- const content = markdown.replace(/^\n+|\n+$/g, ''); // Only remove leading/trailing newlines
- if (!content) {
- return SectionType.PARAGRAPH;
- }
-
- const rawLines = content.split('\n');
- const lines = rawLines.map(line => line.trim()).filter(line => line.length > 0);
- if (lines.length === 0) {
- return SectionType.PARAGRAPH;
- }
-
- // For other detection methods, use trimmed content
- const trimmed = content.trim();
-
- // Detection order matters - most specific first
-
- // 1. Heading detection (must start with # and have space)
- if (this.isHeading(trimmed)) {
- return SectionType.HEADING;
- }
-
- // 2. Code block detection
- if (this.isCodeBlock(trimmed, lines, rawLines)) {
- return SectionType.CODE;
- }
-
- // 3. Table detection
- if (this.isTable(trimmed, lines)) {
- return SectionType.TABLE;
- }
-
- // 4. Horizontal rule detection
- if (this.isHorizontalRule(trimmed, lines)) {
- return SectionType.HR;
- }
-
- // 5. List detection
- if (this.isList(trimmed, lines)) {
- return SectionType.LIST;
- }
-
- // 6. Quote detection
- if (this.isQuote(trimmed, lines)) {
- return SectionType.QUOTE;
- }
-
- // 7. Image detection
- if (this.isImage(trimmed)) {
- return SectionType.IMAGE;
- }
-
- // 8. Default to paragraph
- return SectionType.PARAGRAPH;
- }
-
- /**
- * Advanced type detection with confidence scores
- * @param {string} markdown - Markdown content
- * @returns {Object} Detection result with type, confidence, and alternatives
- */
- static detectTypeWithConfidence(markdown) {
- const scores = {
- [SectionType.HEADING]: 0,
- [SectionType.CODE]: 0,
- [SectionType.TABLE]: 0,
- [SectionType.HR]: 0,
- [SectionType.LIST]: 0,
- [SectionType.QUOTE]: 0,
- [SectionType.IMAGE]: 0,
- [SectionType.PARAGRAPH]: 0
- };
-
- if (!markdown || typeof markdown !== 'string') {
- scores[SectionType.PARAGRAPH] = 1.0;
- return this.formatDetectionResult(scores);
- }
-
- const content = markdown.replace(/^\n+|\n+$/g, '');
- const trimmed = content.trim();
- const rawLines = content.split('\n');
- const lines = rawLines.map(line => line.trim()).filter(line => line.length > 0);
-
- // Calculate confidence scores for each type
- scores[SectionType.HEADING] = this.calculateHeadingScore(trimmed);
- scores[SectionType.CODE] = this.calculateCodeScore(trimmed, lines, rawLines);
- scores[SectionType.TABLE] = this.calculateTableScore(trimmed, lines);
- scores[SectionType.HR] = this.calculateHRScore(trimmed, lines);
- scores[SectionType.LIST] = this.calculateListScore(trimmed, lines);
- scores[SectionType.QUOTE] = this.calculateQuoteScore(trimmed, lines);
- scores[SectionType.IMAGE] = this.calculateImageScore(trimmed);
-
- // Base paragraph score
- scores[SectionType.PARAGRAPH] = 0.1;
-
- return this.formatDetectionResult(scores);
- }
-
- /**
- * Format detection result with primary type and alternatives
- * @param {Object} scores - Confidence scores for each type
- * @returns {Object} Formatted result
- */
- static formatDetectionResult(scores) {
- const sortedTypes = Object.entries(scores)
- .sort(([,a], [,b]) => b - a)
- .map(([type, score]) => ({ type, confidence: score }));
-
- const primaryType = sortedTypes[0];
- const alternatives = sortedTypes.slice(1, 4).filter(alt => alt.confidence > 0.1);
-
- return {
- type: primaryType.type,
- confidence: primaryType.confidence,
- alternatives: alternatives
- };
- }
-
- // Specific detection methods
-
- static isHeading(trimmed) {
- const headingPattern = /^#{1,6}\s+.+/;
- return headingPattern.test(trimmed);
- }
-
- static calculateHeadingScore(trimmed) {
- if (/^#{1,6}\s+.+/.test(trimmed)) {
- const hashCount = (trimmed.match(/^#+/) || [''])[0].length;
- if (hashCount <= 6) {
- return 0.95 - (hashCount - 1) * 0.05; // Higher score for lower level headings
- }
- }
- return 0;
- }
-
- static isCodeBlock(trimmed, lines, rawLines) {
- // Fenced code blocks - check if starts with fence OR contains fence blocks
- if (trimmed.startsWith('```') || trimmed.startsWith('~~~')) {
- return true;
- }
-
- // Check for code blocks anywhere in the content for mixed content
- if (trimmed.includes('```') || trimmed.includes('~~~')) {
- const codeBlockPattern = /```[\s\S]*?```|~~~[\s\S]*?~~~/;
- if (codeBlockPattern.test(trimmed)) {
- return true;
- }
- }
-
- // Indented code blocks (4+ spaces or 1+ tabs at start of every line)
- if (rawLines && rawLines.length > 0) {
- const nonEmptyRawLines = rawLines.filter(line => line.trim().length > 0);
- const indentedLines = nonEmptyRawLines.filter(line =>
- line.startsWith(' ') || line.startsWith('\t')
- );
- return indentedLines.length === nonEmptyRawLines.length && nonEmptyRawLines.length > 0;
- }
-
- return false;
- }
-
- static calculateCodeScore(trimmed, lines, rawLines) {
- let score = 0;
-
- // Fenced code blocks at start
- if (trimmed.startsWith('```') || trimmed.startsWith('~~~')) {
- score = 0.95;
- }
- // Code blocks anywhere in content (mixed content)
- else if (trimmed.includes('```') || trimmed.includes('~~~')) {
- const codeBlockPattern = /```[\s\S]*?```|~~~[\s\S]*?~~~/;
- if (codeBlockPattern.test(trimmed)) {
- // Lower score for mixed content, but still significant
- score = 0.7;
- }
- }
- // Indented code blocks
- else if (rawLines && rawLines.length > 0) {
- const nonEmptyRawLines = rawLines.filter(line => line.trim().length > 0);
- const indentedLines = nonEmptyRawLines.filter(line =>
- line.startsWith(' ') || line.startsWith('\t')
- );
- if (indentedLines.length === nonEmptyRawLines.length && nonEmptyRawLines.length > 0) {
- score = 0.8;
- } else if (indentedLines.length > 0) {
- score = 0.3;
- }
- }
-
- // Boost score for code-like content
- if (/[{}();]/.test(trimmed)) {
- score += 0.1;
- }
-
- return Math.min(score, 1.0);
- }
-
- static isTable(trimmed, lines) {
- if (lines.length < 2) return false;
-
- // Look for table structure with pipes
- const hasTableSeparator = lines.some(line =>
- /^\s*\|?.*?\|.*?\|?\s*$/.test(line) && line.includes('|')
- );
-
- const hasSeparatorRow = lines.some(line =>
- /^\s*\|?\s*:?-+:?\s*(\|\s*:?-+:?\s*)*\|?\s*$/.test(line)
- );
-
- return hasTableSeparator && (hasSeparatorRow || this.looksLikeSimpleTable(lines));
- }
-
- static looksLikeSimpleTable(lines) {
- if (lines.length < 2) return false;
-
- const pipeLines = lines.filter(line => line.includes('|'));
- if (pipeLines.length < 2) return false;
-
- // Check if lines have similar number of pipes
- const pipeCounts = pipeLines.map(line => (line.match(/\|/g) || []).length);
- const avgPipes = pipeCounts.reduce((a, b) => a + b, 0) / pipeCounts.length;
-
- return pipeCounts.every(count => Math.abs(count - avgPipes) <= 1);
- }
-
- static calculateTableScore(trimmed, lines) {
- if (lines.length < 2) return 0;
-
- let score = 0;
- const pipeLines = lines.filter(line => line.includes('|'));
-
- if (pipeLines.length >= 2) {
- score = 0.3 + (pipeLines.length / lines.length) * 0.4;
-
- // Boost for separator row
- if (lines.some(line => /^\s*\|?\s*:?-+:?\s*(\|\s*:?-+:?\s*)*\|?\s*$/.test(line))) {
- score += 0.3;
- }
- }
-
- return Math.min(score, 1.0);
- }
-
- static isHorizontalRule(trimmed, lines) {
- if (lines.length !== 1) return false;
-
- const line = lines[0];
-
- // Three or more hyphens, asterisks, or underscores
- const hrPatterns = [
- /^-{3,}$/,
- /^\*{3,}$/,
- /^_{3,}$/,
- /^- -( -)*$/,
- /^\* \*( \*)*$/,
- /^_ _( _)*$/
- ];
-
- return hrPatterns.some(pattern => pattern.test(line));
- }
-
- static calculateHRScore(trimmed, lines) {
- if (lines.length === 1) {
- const line = lines[0];
- const hrPatterns = [
- /^-{3,}$/, /^\*{3,}$/, /^_{3,}$/,
- /^- -( -)*$/, /^\* \*( \*)*$/, /^_ _( _)*$/
- ];
-
- if (hrPatterns.some(pattern => pattern.test(line))) {
- return 0.9;
- }
- }
- return 0;
- }
-
- static isList(trimmed, lines) {
- if (lines.length === 0) return false;
-
- const listItemPatterns = [
- /^[-*+]\s+/, // Bullet lists
- /^\d+[\.)]\s+/, // Numbered lists
- /^[-*+]\s*\[([ x])\]\s+/ // Task lists
- ];
-
- const listLines = lines.filter(line =>
- listItemPatterns.some(pattern => pattern.test(line))
- );
-
- return listLines.length > 0 && listLines.length >= lines.length * 0.5;
- }
-
- static calculateListScore(trimmed, lines) {
- if (lines.length === 0) return 0;
-
- const listItemPatterns = [
- /^[-*+]\s+/,
- /^\d+[\.)]\s+/,
- /^[-*+]\s*\[([ x])\]\s+/
- ];
-
- const listLines = lines.filter(line =>
- listItemPatterns.some(pattern => pattern.test(line))
- );
-
- if (listLines.length === 0) return 0;
-
- const ratio = listLines.length / lines.length;
- return ratio * 0.9;
- }
-
- static isQuote(trimmed, lines) {
- if (lines.length === 0) return false;
-
- const quoteLines = lines.filter(line => line.startsWith('>'));
- return quoteLines.length > 0 && quoteLines.length >= lines.length * 0.5;
- }
-
- static calculateQuoteScore(trimmed, lines) {
- if (lines.length === 0) return 0;
-
- const quoteLines = lines.filter(line => line.startsWith('>'));
- if (quoteLines.length === 0) return 0;
-
- const ratio = quoteLines.length / lines.length;
- return ratio * 0.85;
- }
-
- static isImage(trimmed) {
- // Look for image syntax anywhere in the content
- const imagePattern = /!\[.*?\]\([^)]+\)/;
- return imagePattern.test(trimmed);
- }
-
- static calculateImageScore(trimmed) {
- const imagePattern = /!\[.*?\]\([^)]+\)/g;
- const matches = trimmed.match(imagePattern);
-
- if (matches) {
- // Higher score for standalone images
- const isStandalone = trimmed.replace(imagePattern, '').trim().length < 20;
- return isStandalone ? 0.9 : 0.6;
- }
-
- return 0;
- }
-
- startEdit() {
- if (this.state === EditState.EDITING) {
- throw new Error(`Section ${this.id} is already being edited`);
- }
- this.editingMarkdown = this.pendingMarkdown || this.currentMarkdown;
- this.state = EditState.EDITING;
- return this.editingMarkdown;
- }
-
- updateContent(markdown) {
- if (this.state !== EditState.EDITING) {
- throw new Error(`Section ${this.id} is not in editing state`);
- }
- this.editingMarkdown = markdown;
- }
-
- acceptChanges() {
- if (this.state !== EditState.EDITING) {
- throw new Error(`Section ${this.id} is not in editing state`);
- }
- this.currentMarkdown = this.editingMarkdown;
- this.editingMarkdown = null;
- this.pendingMarkdown = null;
- this.state = EditState.SAVED;
- this.lastSaved = new Date();
- return this.currentMarkdown;
- }
-
- cancelChanges() {
- if (this.state !== EditState.EDITING) {
- throw new Error(`Section ${this.id} is not in editing state`);
- }
- this.editingMarkdown = null;
- if (this.pendingMarkdown !== null) {
- this.state = EditState.MODIFIED;
- return this.pendingMarkdown;
- } else if (this.lastSaved !== null) {
- this.state = EditState.SAVED;
- return this.currentMarkdown;
- } else {
- this.state = this.hasChanges() ? EditState.MODIFIED : EditState.ORIGINAL;
- return this.currentMarkdown;
- }
- }
-
- stopEditing() {
- if (this.state !== EditState.EDITING) {
- return this.state;
- }
-
- // If we have editing changes that differ from current content, preserve them as pending
- if (this.editingMarkdown && this.editingMarkdown !== this.currentMarkdown) {
- this.pendingMarkdown = this.editingMarkdown;
- this.state = EditState.MODIFIED; // Has pending changes
- } else {
- // No changes made during this edit session
- this.pendingMarkdown = null;
- if (this.lastSaved !== null) {
- this.state = EditState.SAVED;
- } else {
- this.state = this.hasChanges() ? EditState.MODIFIED : EditState.ORIGINAL;
- }
- }
-
- this.editingMarkdown = null;
- return this.state;
- }
-
- resetToOriginal() {
- this.currentMarkdown = this.originalMarkdown;
- this.editingMarkdown = this.originalMarkdown;
- this.pendingMarkdown = null;
- this.state = EditState.ORIGINAL;
- return this.originalMarkdown;
- }
-
- isEditing() {
- return this.state === EditState.EDITING;
- }
-
- hasChanges() {
- return this.currentMarkdown !== this.originalMarkdown;
- }
-
- getStatus() {
- return {
- id: this.id,
- state: this.state,
- hasChanges: this.hasChanges(),
- isEditing: this.isEditing(),
- contentLength: this.currentMarkdown.length,
- lastSaved: this.lastSaved,
- sectionType: this.sectionType,
- // Legacy compatibility
- type: this.type,
- originalLength: this.originalMarkdown.length,
- currentLength: this.currentMarkdown.length
- };
- }
-
- isImage() {
- return this.type === SectionType.IMAGE;
- }
-
- isProtectedHeading() {
- // In insert mode, headings 1-3 are protected
- if (this.type === SectionType.HEADING) {
- const headingMatch = this.originalMarkdown.match(/^(#{1,3})\s/);
- if (headingMatch) {
- this.headingLevel = headingMatch[1].length;
- return this.headingLevel <= 3;
- }
- }
- return false;
- }
-
- getHeadingContent() {
- if (this.type === SectionType.HEADING) {
- const lines = this.currentMarkdown.split('\n');
- return lines.slice(1).join('\n').trim();
- }
- return this.currentMarkdown;
- }
-
- /**
- * Re-detect and update the section type based on current content
- * @param {string} content - Optional content to use for detection (defaults to currentMarkdown)
- * @returns {string} The detected section type
- */
- redetectType(content = null) {
- const markdown = content || this.currentMarkdown;
- const oldType = this.type;
- this.type = Section.detectType(markdown);
-
- // Emit type change event if type changed
- if (oldType !== this.type) {
- debug(`Section ${this.id} type changed from ${oldType} to ${this.type}`, 'TYPE_DETECTION');
- }
-
- return this.type;
- }
-
- /**
- * Get detailed type detection information
- * @param {string} content - Optional content to analyze
- * @returns {Object} Detection result with confidence and alternatives
- */
- getTypeAnalysis(content = null) {
- const markdown = content || this.currentMarkdown;
- return Section.detectTypeWithConfidence(markdown);
- }
-}
-
-/**
- * SectionManager class - Manages the collection of sections
- */
-class SectionManager {
- constructor() {
- this.sections = new Map();
- this.listeners = new Map();
- this.statusInterval = null;
- this.lastStatusUpdate = new Date().toISOString();
- }
-
- on(event, callback) {
- if (!this.listeners.has(event)) {
- this.listeners.set(event, []);
- }
- this.listeners.get(event).push(callback);
- }
-
- emit(event, data) {
- if (this.listeners.has(event)) {
- this.listeners.get(event).forEach(callback => callback(data));
- }
- }
-
- createSectionsFromMarkdown(markdownContent) {
- const lines = markdownContent.split('\n');
- const sections = [];
- let currentSection = '';
- let position = 0;
-
- for (let i = 0; i < lines.length; i++) {
- const line = lines[i];
- const isHeading = /^#{1,6}\s/.test(line);
- const isNewParagraph = line.trim() && i > 0 && !lines[i-1].trim();
- const isNewSection = isHeading || isNewParagraph;
-
- if (isNewSection && currentSection.trim()) {
- const sectionId = Section.generateId(currentSection, position);
- const sectionType = Section.detectType(currentSection);
- const section = new Section(sectionId, currentSection.trim(), sectionType);
- sections.push(section);
- this.sections.set(sectionId, section);
- position++;
- currentSection = line;
- } else {
- if (currentSection) currentSection += '\n';
- currentSection += line;
- }
- }
-
- if (currentSection.trim()) {
- const sectionId = Section.generateId(currentSection, position);
- const sectionType = Section.detectType(currentSection);
- const section = new Section(sectionId, currentSection.trim(), sectionType);
- sections.push(section);
- this.sections.set(sectionId, section);
- }
-
- this.emit('sections-created', { sections, count: sections.length });
- return sections;
- }
-
- startEditing(sectionId) {
- debug('MANAGER: startEditing called for: ' + sectionId, 'MANAGER');
-
- const section = this.sections.get(sectionId);
- if (!section) {
- throw new Error(`Section ${sectionId} not found`);
- }
-
- if (section.isEditing()) {
- debug('MANAGER: Section already in editing state: ' + sectionId, 'MANAGER');
- return section.editingMarkdown;
- }
-
- debug('MANAGER: Starting edit for section: ' + sectionId, 'MANAGER');
- const content = section.startEdit();
-
- debug('MANAGER: About to emit edit-started event for: ' + sectionId, 'MANAGER');
- this.emit('edit-started', { sectionId, content, section: section.getStatus() });
- debug('MANAGER: Emitted edit-started event for: ' + sectionId, 'MANAGER');
-
- return content;
- }
-
- updateContent(sectionId, markdown) {
- const section = this.sections.get(sectionId);
- if (!section) {
- throw new Error(`Section ${sectionId} not found`);
- }
-
- // Store old type for comparison
- const oldType = section.type;
-
- // Update content
- section.updateContent(markdown);
-
- // Automatically redetect type if content changed significantly
- const newType = section.redetectType(markdown);
-
- // Emit events
- const eventData = {
- sectionId,
- markdown,
- section: section.getStatus(),
- typeChanged: oldType !== newType,
- oldType,
- newType
- };
-
- this.emit('content-updated', eventData);
-
- if (oldType !== newType) {
- this.emit('section-type-changed', {
- sectionId,
- oldType,
- newType,
- section: section.getStatus()
- });
- }
- }
-
- acceptChanges(sectionId) {
- const section = this.sections.get(sectionId);
- if (!section) {
- throw new Error(`Section ${sectionId} not found`);
- }
-
- const content = section.acceptChanges();
- this.emit('changes-accepted', { sectionId, content, section: section.getStatus() });
- return content;
- }
-
- cancelChanges(sectionId) {
- const section = this.sections.get(sectionId);
- if (!section) {
- throw new Error(`Section ${sectionId} not found`);
- }
-
- const content = section.cancelChanges();
- this.emit('changes-cancelled', { sectionId, content, section: section.getStatus() });
- return content;
- }
-
- resetSection(sectionId) {
- const section = this.sections.get(sectionId);
- if (!section) {
- throw new Error(`Section ${sectionId} not found`);
- }
-
- const content = section.resetToOriginal();
- this.emit('section-reset', { sectionId, content, section: section.getStatus() });
- return content;
- }
-
- getDocumentMarkdown() {
- const sortedSections = Array.from(this.sections.values())
- .sort((a, b) => a.created - b.created);
-
- return sortedSections.map(section => section.currentMarkdown).join('\n\n');
- }
-
- getAllSections() {
- return Array.from(this.sections.values());
- }
-
- getSectionStatus() {
- return Array.from(this.sections.values()).map(section => section.getStatus());
- }
-
- getDocumentStatus() {
- const sections = Array.from(this.sections.values());
- const editingSections = sections.filter(section => section.isEditing).length;
-
- return {
- totalSections: sections.length,
- editingSections: editingSections
- };
- }
-
- escapeRegex(str) {
- return str.replace(/[.*+?^${}()|\[\]\\]/g, '\\\\$&');
- }
-
- /**
- * Check if new content contains new headings that would require section splitting
- * @param {string} newContent - The new content to check
- * @param {string} originalContent - The original content to compare against
- * @returns {boolean} True if section splitting is needed
- */
- checkForSectionSplits(newContent, originalContent) {
- const originalHeadings = this.extractHeadings(originalContent);
- const newHeadings = this.extractHeadings(newContent);
-
- // If new content has more headings than original, we need to split
- return newHeadings.length > originalHeadings.length;
- }
-
- /**
- * Extract heading lines from markdown content
- * @param {string} content - Markdown content
- * @returns {Array} Array of heading lines
- */
- extractHeadings(content) {
- if (!content) return [];
- const lines = content.split('\n');
- return lines.filter(line => /^#{1,6}\s/.test(line.trim()));
- }
-
- /**
- * Handle splitting a section when new headings are detected
- * @param {string} sectionId - ID of the section to split
- * @param {string} newContent - New content with multiple headings
- */
- handleSectionSplit(sectionId, newContent) {
- const section = this.sections.get(sectionId);
- if (!section) {
- throw new Error(`Section ${sectionId} not found`);
- }
-
- // Remove the original section
- this.sections.delete(sectionId);
-
- // Create new sections from the content
- const newSections = this.createSectionsFromContent(newContent);
-
- // Emit section-split event
- this.emit('section-split', {
- originalSectionId: sectionId,
- newSections: newSections,
- count: newSections.length
- });
-
- return newSections;
- }
-
- /**
- * Create sections from content (alias for createSectionsFromMarkdown)
- * @param {string} content - Markdown content
- * @returns {Array} Array of created sections
- */
- createSectionsFromContent(content) {
- return this.createSectionsFromMarkdown(content);
- }
-
- /**
- * Get global status information about all sections
- * @returns {Object} Status object with global information
- */
- getGlobalStatus() {
- const sections = this.getAllSections();
- const editingSections = sections.filter(s => s.isEditing()).map(s => s.id);
- const modifiedSections = sections.filter(s => s.hasChanges());
- const hasModifications = modifiedSections.length > 0 || editingSections.length > 0;
-
- let state = 'ready';
- if (editingSections.length > 0) {
- state = 'editing';
- } else if (hasModifications) {
- state = 'modified';
- }
-
- return {
- totalSections: sections.length,
- editingSections: editingSections,
- modifiedSections: modifiedSections.length,
- hasModifications: hasModifications,
- state: state,
- lastUpdate: this.lastStatusUpdate
- };
- }
-
- /**
- * Update global status and emit status-updated event
- */
- updateGlobalStatus() {
- this.lastStatusUpdate = new Date().toISOString();
- const status = this.getGlobalStatus();
- this.emit('status-updated', status);
- return status;
- }
-
- /**
- * Start periodic status tracking
- * @param {number} intervalMs - Update interval in milliseconds (default: 2000)
- */
- startStatusTracking(intervalMs = 2000) {
- if (this.statusInterval) {
- clearInterval(this.statusInterval);
- }
-
- this.statusInterval = setInterval(() => {
- this.updateGlobalStatus();
- }, intervalMs);
-
- // Emit initial status
- this.updateGlobalStatus();
- }
-
- /**
- * Stop periodic status tracking
- */
- stopStatusTracking() {
- if (this.statusInterval) {
- clearInterval(this.statusInterval);
- this.statusInterval = null;
- }
- }
-
- /**
- * Bulk Operations for Concurrent Editing Sessions
- */
-
- /**
- * Accept changes for all currently editing sessions
- * @returns {Array} Array of results from accept operations
- */
- acceptAllEditingSessions() {
- const editingSections = this.getAllSections().filter(section => section.isEditing());
- const results = [];
-
- editingSections.forEach(section => {
- try {
- const content = this.acceptChanges(section.id);
- results.push({ sectionId: section.id, success: true, content });
- } catch (error) {
- results.push({ sectionId: section.id, success: false, error: error.message });
- }
- });
-
- this.emit('bulk-accept-completed', { results, count: results.length });
- return results;
- }
-
- /**
- * Cancel changes for all currently editing sessions
- * @returns {Array} Array of results from cancel operations
- */
- cancelAllEditingSessions() {
- const editingSections = this.getAllSections().filter(section => section.isEditing());
- const results = [];
-
- editingSections.forEach(section => {
- try {
- const content = this.cancelChanges(section.id);
- results.push({ sectionId: section.id, success: true, content });
- } catch (error) {
- results.push({ sectionId: section.id, success: false, error: error.message });
- }
- });
-
- this.emit('bulk-cancel-completed', { results, count: results.length });
- return results;
- }
-
- /**
- * Stop editing for all currently editing sessions (preserve changes as pending)
- * @returns {Array} Array of results from stop operations
- */
- stopAllEditingSessions() {
- const editingSections = this.getAllSections().filter(section => section.isEditing());
- const results = [];
-
- editingSections.forEach(section => {
- try {
- const finalState = section.stopEditing();
- results.push({ sectionId: section.id, success: true, finalState });
- } catch (error) {
- results.push({ sectionId: section.id, success: false, error: error.message });
- }
- });
-
- this.emit('bulk-stop-completed', { results, count: results.length });
- return results;
- }
-
- /**
- * Get detailed status of all concurrent editing sessions
- * @returns {Object} Detailed concurrent session information
- */
- getConcurrentEditingStatus() {
- const sections = this.getAllSections();
- const editingSections = sections.filter(s => s.isEditing());
- const modifiedSections = sections.filter(s => s.hasChanges());
- const pendingSections = sections.filter(s => s.pendingMarkdown !== null);
-
- return {
- totalSections: sections.length,
- concurrentSessions: {
- editing: editingSections.map(s => ({
- id: s.id,
- type: s.type,
- hasUnsavedChanges: s.editingMarkdown !== s.currentMarkdown,
- editingLength: s.editingMarkdown ? s.editingMarkdown.length : 0,
- originalLength: s.originalMarkdown.length
- })),
- editingCount: editingSections.length,
- pendingCount: pendingSections.length,
- modifiedCount: modifiedSections.length
- },
- systemState: {
- allowsConcurrentEditing: true,
- maxConcurrentSessions: null, // No limit
- activeSessionCount: editingSections.length
- }
- };
- }
-
- /**
- * Handle conflicts when multiple sections are edited simultaneously
- * @param {string} sectionId - Primary section ID
- * @param {Array} conflictingSectionIds - IDs of sections with potential conflicts
- * @returns {Object} Conflict resolution result
- */
- resolveEditingConflicts(sectionId, conflictingSectionIds = []) {
- const primarySection = this.sections.get(sectionId);
- if (!primarySection) {
- throw new Error(`Primary section ${sectionId} not found`);
- }
-
- const conflicts = [];
- const resolutions = [];
-
- conflictingSectionIds.forEach(conflictId => {
- const conflictSection = this.sections.get(conflictId);
- if (conflictSection && conflictSection.isEditing()) {
- // Check for content overlap or dependency conflicts
- const hasConflict = this.detectContentConflict(primarySection, conflictSection);
-
- if (hasConflict) {
- conflicts.push({
- sectionId: conflictId,
- conflictType: 'content-overlap',
- description: 'Sections may have overlapping content changes'
- });
-
- // Auto-resolve by preserving both as separate sections
- resolutions.push({
- sectionId: conflictId,
- resolution: 'preserve-separate',
- action: 'maintain-current-state'
- });
- }
- }
- });
-
- this.emit('conflicts-resolved', { sectionId, conflicts, resolutions });
- return { conflicts, resolutions, resolved: true };
- }
-
- /**
- * Detect potential content conflicts between two sections
- * @param {Section} section1 - First section
- * @param {Section} section2 - Second section
- * @returns {boolean} True if conflict detected
- */
- detectContentConflict(section1, section2) {
- // Simple conflict detection - check for similar content or headings
- if (section1.type === 'heading' && section2.type === 'heading') {
- const heading1 = section1.editingMarkdown || section1.currentMarkdown;
- const heading2 = section2.editingMarkdown || section2.currentMarkdown;
-
- // Check if headings are similar (potential duplicate)
- const similarity = this.calculateContentSimilarity(heading1, heading2);
- return similarity > 0.8; // 80% similarity threshold
- }
-
- return false; // No conflicts detected for non-heading sections
- }
-
- /**
- * Calculate content similarity between two markdown strings
- * @param {string} content1 - First content
- * @param {string} content2 - Second content
- * @returns {number} Similarity score (0-1)
- */
- calculateContentSimilarity(content1, content2) {
- if (!content1 || !content2) return 0;
-
- const clean1 = content1.toLowerCase().replace(/[^a-z0-9\s]/g, '');
- const clean2 = content2.toLowerCase().replace(/[^a-z0-9\s]/g, '');
-
- const words1 = clean1.split(/\s+/).filter(w => w.length > 0);
- const words2 = clean2.split(/\s+/).filter(w => w.length > 0);
-
- if (words1.length === 0 && words2.length === 0) return 1;
- if (words1.length === 0 || words2.length === 0) return 0;
-
- const commonWords = words1.filter(word => words2.includes(word));
- const totalWords = Math.max(words1.length, words2.length);
-
- return commonWords.length / totalWords;
- }
-}
-
-/**
- * DOM Renderer - Handles DOM interactions with Enhanced Event System
- */
-class DOMRenderer {
- constructor(sectionManager, container) {
- this.sectionManager = sectionManager;
- this.container = container;
- this.editingSections = new Set();
- this.currentFloatingMenu = null;
- this.eventListenersAttached = false;
-
- // Enhanced Event System - Track 6 event types
- this.eventHistory = [];
- this.eventStats = {
- 'section-click': 0,
- 'section-hover-enter': 0,
- 'section-hover-leave': 0,
- 'keyboard-shortcut': 0,
- 'section-drag-start': 0,
- 'section-drag-over': 0,
- 'section-drop': 0,
- 'section-focus-in': 0,
- 'section-focus-out': 0,
- 'section-context-menu': 0
- };
-
- // Bind event handlers
- this.handleSectionClick = this.handleSectionClick.bind(this);
- this.handleSectionHover = this.handleSectionHover.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);
- this.handleDragStart = this.handleDragStart.bind(this);
- this.handleDragOver = this.handleDragOver.bind(this);
- this.handleDrop = this.handleDrop.bind(this);
- this.handleFocus = this.handleFocus.bind(this);
- this.handleContextMenu = this.handleContextMenu.bind(this);
-
- this.setupEventListeners();
- }
-
- setupEventListeners() {
- this.sectionManager.on('sections-created', (data) => {
- this.renderAllSections(data.sections);
- });
- this.sectionManager.on('edit-started', (data) => {
- debug('EVENT: edit-started event received for: ' + data.sectionId, 'EVENT');
- this.showEditor(data.sectionId, data.content);
- });
- this.sectionManager.on('edit-stopped', (data) => {
- this.hideEditor(data.sectionId);
- });
- this.sectionManager.on('changes-accepted', (data) => {
- this.hideEditor(data.sectionId);
- this.updateSectionContent(data.sectionId, data.content);
- });
- this.sectionManager.on('changes-cancelled', (data) => {
- this.hideEditor(data.sectionId);
- this.updateSectionContent(data.sectionId, data.content);
- });
- this.sectionManager.on('section-reset', (data) => {
- this.hideEditor(data.sectionId);
- this.updateSectionContent(data.sectionId, data.content);
- });
- }
-
- /**
- * Track and log DOM events for analytics and debugging
- * @param {string} eventType - Type of event
- * @param {Object} eventData - Event data
- */
- trackEvent(eventType, eventData) {
- const timestamp = new Date().toISOString();
- const eventRecord = {
- type: eventType,
- timestamp,
- data: eventData
- };
-
- // Add to history (keep last 1000 events)
- this.eventHistory.push(eventRecord);
- if (this.eventHistory.length > 1000) {
- this.eventHistory.shift();
- }
-
- // Update stats
- if (this.eventStats.hasOwnProperty(eventType)) {
- this.eventStats[eventType]++;
- }
-
- // Emit to section manager for broader handling
- this.sectionManager.emit(eventType, eventData);
- }
-
- /**
- * Get event statistics for debugging
- * @returns {Object} Event statistics
- */
- getEventStats() {
- return {
- stats: { ...this.eventStats },
- recentEvents: this.eventHistory.slice(-10),
- totalEvents: this.eventHistory.length
- };
- }
-
- renderAllSections(sections) {
- debug('21: renderAllSections called with ' + sections.length + ' sections', 'RENDER');
-
- this.container.innerHTML = '';
- debug('22: Container cleared', 'RENDER');
-
- sections.forEach((section, index) => {
- debug('23: Creating element for section ' + (index + 1) + ': ' + section.id, 'RENDER');
- const element = this.createSectionElement(section);
- section.domElement = element;
- this.container.appendChild(element);
- });
-
- debug('24: All section elements added to container', 'RENDER');
-
- // Enhanced DOM Event System - Setup all 6 event types with delegation (only once)
- if (!this.eventListenersAttached) {
- this.container.addEventListener('click', this.handleSectionClick);
- this.container.addEventListener('mouseenter', this.handleSectionHover, true);
- this.container.addEventListener('mouseleave', this.handleSectionHover, true);
- this.container.addEventListener('keydown', this.handleKeydown);
- this.container.addEventListener('dragstart', this.handleDragStart);
- this.container.addEventListener('dragover', this.handleDragOver);
- this.container.addEventListener('drop', this.handleDrop);
- this.container.addEventListener('focusin', this.handleFocus);
- this.container.addEventListener('focusout', this.handleFocus);
- this.container.addEventListener('contextmenu', this.handleContextMenu);
-
- this.eventListenersAttached = true;
- debug('25: Enhanced event listeners attached for the first time', 'RENDER');
- } else {
- debug('25: Event listeners already attached, skipping', 'RENDER');
- }
-
- debug('25: Container content length: ' + this.container.innerHTML.length, 'RENDER');
- }
-
- createSectionElement(section) {
- const element = document.createElement('div');
- element.setAttribute('data-section-id', section.id);
- debug('SECTION: Creating element for section ' + section.id + ' with content: ' + section.currentMarkdown.substring(0, 50) + '...', 'SECTION');
-
- if (typeof marked !== 'undefined') {
- const html = marked.parse(section.currentMarkdown);
- const htmlWithTargetBlank = html.replace(/]*)>/g, ' ');
- element.innerHTML = htmlWithTargetBlank;
- } else {
- element.innerHTML = `${section.currentMarkdown}
`;
- }
-
- this.setupSectionElement(element);
- debug('SECTION: Section element setup complete for ' + section.id, 'SECTION');
- return element;
- }
-
- handleSectionClick(event) {
- debug('handleSectionClick: Click detected on target: ' + event.target.tagName + ' ' + (event.target.className || ''), 'CLICK');
-
- // Don't handle clicks on form elements, buttons, or links
- if (event.target.closest('textarea, button, input, a')) {
- debug('handleSectionClick: Ignoring click on form element', 'CLICK');
- return;
- }
-
- const sectionElement = event.target.closest('.ui-edit-section');
- debug('handleSectionClick: Found section element: ' + (sectionElement ? sectionElement.getAttribute('data-section-id') : 'null'), 'CLICK');
- if (!sectionElement) return;
-
- const sectionId = sectionElement.getAttribute('data-section-id');
- debug('handleSectionClick: Section ID: ' + sectionId, 'CLICK');
- if (!sectionId) return;
-
- // Track the click event
- this.trackEvent('section-click', {
- sectionId,
- target: event.target.tagName.toLowerCase(),
- event,
- timestamp: Date.now()
- });
-
- // Check if this section is already being edited
- const section = this.sectionManager.sections.get(sectionId);
- debug('handleSectionClick: Found section object: ' + (section ? 'YES' : 'NO'), 'CLICK');
-
- if (section && section.isEditing()) {
- debug('handleSectionClick: Section already being edited: ' + sectionId, 'CLICK');
- return;
- }
-
- debug('handleSectionClick: About to start editing for section: ' + sectionId, 'CLICK');
-
- try {
- debug('handleSectionClick: Calling sectionManager.startEditing', 'CLICK');
- this.sectionManager.startEditing(sectionId);
- debug('handleSectionClick: Successfully called startEditing', 'CLICK');
- } catch (error) {
- debug('handleSectionClick: ERROR in startEditing: ' + error.message, 'ERROR');
- console.error('Failed to start editing:', error);
- }
- }
-
- /**
- * Handle hover events (mouseenter/mouseleave)
- * @param {Event} event - Mouse event
- */
- handleSectionHover(event) {
- const sectionElement = event.target.closest('.ui-edit-section');
- if (!sectionElement) return;
-
- const sectionId = sectionElement.getAttribute('data-section-id');
- if (!sectionId) return;
-
- const eventType = event.type === 'mouseenter' ? 'section-hover-enter' : 'section-hover-leave';
-
- // Track the hover event
- this.trackEvent(eventType, {
- sectionId,
- event,
- timestamp: Date.now()
- });
- }
-
- /**
- * Handle drag and drop events for section reordering
- * @param {Event} event - Drag event
- */
- handleDragStart(event) {
- const sectionElement = event.target.closest('.ui-edit-section');
- if (!sectionElement) return;
-
- const sectionId = sectionElement.getAttribute('data-section-id');
- if (!sectionId) return;
-
- // Store the section ID being dragged
- event.dataTransfer.setData('text/plain', sectionId);
- event.dataTransfer.effectAllowed = 'move';
-
- // Track the drag start event
- this.trackEvent('section-drag-start', {
- sectionId,
- event,
- timestamp: Date.now()
- });
- }
-
- handleDragOver(event) {
- const sectionElement = event.target.closest('.ui-edit-section');
- if (!sectionElement) return;
-
- const sectionId = sectionElement.getAttribute('data-section-id');
- if (!sectionId) return;
-
- event.preventDefault(); // Allow drop
- event.dataTransfer.dropEffect = 'move';
-
- // Track the drag over event
- this.trackEvent('section-drag-over', {
- sectionId,
- event,
- timestamp: Date.now()
- });
- }
-
- handleDrop(event) {
- const sectionElement = event.target.closest('.ui-edit-section');
- if (!sectionElement) return;
-
- const targetSectionId = sectionElement.getAttribute('data-section-id');
- const draggedSectionId = event.dataTransfer.getData('text/plain');
-
- if (!targetSectionId || !draggedSectionId || targetSectionId === draggedSectionId) return;
-
- event.preventDefault();
-
- // Track the drop event
- this.trackEvent('section-drop', {
- draggedSectionId,
- targetSectionId,
- event,
- timestamp: Date.now()
- });
-
- // Emit section reorder event
- this.sectionManager.emit('section-reorder', {
- draggedSectionId,
- targetSectionId,
- action: 'reorder'
- });
- }
-
- /**
- * Handle focus events for accessibility
- * @param {Event} event - Focus event
- */
- handleFocus(event) {
- const sectionElement = event.target.closest('.ui-edit-section');
- if (!sectionElement) return;
-
- const sectionId = sectionElement.getAttribute('data-section-id');
- if (!sectionId) return;
-
- const eventType = event.type === 'focusin' ? 'section-focus-in' : 'section-focus-out';
-
- // Track the focus event
- this.trackEvent(eventType, {
- sectionId,
- target: event.target.tagName.toLowerCase(),
- event,
- timestamp: Date.now()
- });
- }
-
- /**
- * Handle context menu events for right-click operations
- * @param {Event} event - Context menu event
- */
- handleContextMenu(event) {
- const sectionElement = event.target.closest('.ui-edit-section');
- if (!sectionElement) return;
-
- const sectionId = sectionElement.getAttribute('data-section-id');
- if (!sectionId) return;
-
- // Track the context menu event
- this.trackEvent('section-context-menu', {
- sectionId,
- x: event.clientX,
- y: event.clientY,
- event,
- timestamp: Date.now()
- });
-
- // Prevent default context menu for sections
- event.preventDefault();
-
- // Show custom context menu
- this.showSectionContextMenu(sectionId, event.clientX, event.clientY);
- }
-
- /**
- * Show custom context menu for section operations
- * @param {string} sectionId - Section ID
- * @param {number} x - X coordinate
- * @param {number} y - Y coordinate
- */
- showSectionContextMenu(sectionId, x, y) {
- // Remove existing context menu
- const existingMenu = document.querySelector('.ui-edit-context-menu');
- if (existingMenu) {
- existingMenu.remove();
- }
-
- const menu = document.createElement('div');
- menu.className = 'ui-edit-context-menu';
- menu.style.cssText = `
- position: fixed;
- left: ${x}px;
- top: ${y}px;
- background: white;
- border: 1px solid #ccc;
- border-radius: 6px;
- box-shadow: 0 4px 12px rgba(0,0,0,0.15);
- z-index: 10000;
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
- font-size: 14px;
- min-width: 150px;
- `;
-
- const section = this.sectionManager.sections.get(sectionId);
- const isEditing = section && section.isEditing();
-
- const menuItems = [
- { text: 'Edit Section', action: () => this.sectionManager.startEditing(sectionId), disabled: isEditing },
- { text: 'Copy Section', action: () => this.copySectionToClipboard(sectionId), disabled: false },
- { text: 'Delete Section', action: () => this.deleteSection(sectionId), disabled: isEditing },
- { text: 'โ', action: null, disabled: false }, // Separator
- { text: 'Move Up', action: () => this.moveSectionUp(sectionId), disabled: false },
- { text: 'Move Down', action: () => this.moveSectionDown(sectionId), disabled: false }
- ];
-
- menuItems.forEach(item => {
- if (item.text === 'โ') {
- const separator = document.createElement('div');
- separator.style.cssText = 'height: 1px; background: #eee; margin: 4px 0;';
- menu.appendChild(separator);
- } else {
- const menuItem = document.createElement('div');
- menuItem.textContent = item.text;
- menuItem.style.cssText = `
- padding: 8px 12px;
- cursor: ${item.disabled ? 'not-allowed' : 'pointer'};
- color: ${item.disabled ? '#999' : '#333'};
- background: ${item.disabled ? 'transparent' : 'white'};
- `;
-
- if (!item.disabled) {
- menuItem.addEventListener('mouseenter', () => {
- menuItem.style.background = '#f0f0f0';
- });
- menuItem.addEventListener('mouseleave', () => {
- menuItem.style.background = 'white';
- });
- menuItem.addEventListener('click', () => {
- item.action();
- menu.remove();
- });
- }
-
- menu.appendChild(menuItem);
- }
- });
-
- document.body.appendChild(menu);
-
- // Remove menu when clicking elsewhere
- const removeMenu = (e) => {
- if (!menu.contains(e.target)) {
- menu.remove();
- document.removeEventListener('click', removeMenu);
- }
- };
- setTimeout(() => document.addEventListener('click', removeMenu), 100);
- }
-
- /**
- * Context menu actions
- */
- copySectionToClipboard(sectionId) {
- const section = this.sectionManager.sections.get(sectionId);
- if (section) {
- navigator.clipboard.writeText(section.currentMarkdown).then(() => {
- console.log('Section copied to clipboard');
- });
- }
- }
-
- deleteSection(sectionId) {
- if (confirm('Are you sure you want to delete this section?')) {
- this.sectionManager.sections.delete(sectionId);
- const element = this.findSectionElement(sectionId);
- if (element) {
- element.remove();
- }
- }
- }
-
- moveSectionUp(sectionId) {
- const element = this.findSectionElement(sectionId);
- if (element && element.previousElementSibling) {
- element.parentNode.insertBefore(element, element.previousElementSibling);
- }
- }
-
- moveSectionDown(sectionId) {
- const element = this.findSectionElement(sectionId);
- if (element && element.nextElementSibling) {
- element.parentNode.insertBefore(element.nextElementSibling, element);
- }
- }
-
- showEditor(sectionId, content) {
- debug('showEditor: called for section: ' + sectionId, 'EDITOR');
-
- const element = this.findSectionElement(sectionId);
- debug('showEditor: Found element: ' + (element ? 'YES' : 'NO'), 'EDITOR');
- if (!element) return;
-
- debug('showEditor: About to hide current editor', 'EDITOR');
- this.hideCurrentEditor();
- debug('showEditor: Hidden current editor', 'EDITOR');
-
- const section = this.sectionManager.sections.get(sectionId);
- const isImageSection = section && section.isImage();
-
- if (isImageSection) {
- this.showImageEditor(sectionId, section);
- return;
- }
-
- // Create content area for text editing (optimized for side-by-side layout)
- const editorContent = document.createElement('div');
- editorContent.className = 'ui-edit-editor-content';
- editorContent.style.cssText = `
- display: flex;
- flex-direction: column;
- gap: 12px;
- flex: 1;
- min-width: 0;
- `;
-
- const textarea = document.createElement('textarea');
- textarea.className = 'ui-edit-textarea ui-edit-textarea-main';
- textarea.value = content;
- textarea.style.cssText = `
- flex: 1;
- min-height: 200px;
- width: 100%;
- font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace;
- font-size: 14px;
- line-height: 1.5;
- padding: 12px;
- border: 1px solid #ddd;
- border-radius: 8px;
- resize: none;
- outline: none;
- background: white;
- color: #333;
- box-sizing: border-box;
- `;
-
- editorContent.appendChild(textarea);
-
- // Setup auto-resize functionality
- this.setupAutoResize(textarea);
-
- // Create controls (optimized for side panel layout)
- const controls = document.createElement('div');
- controls.className = 'ui-edit-controls';
- controls.style.cssText = `
- display: flex;
- flex-direction: column;
- gap: 8px;
- width: 100%;
- `;
-
- const acceptBtn = this.createButton('Accept', 'ui-edit-button-accept', this.handleAccept);
- const cancelBtn = this.createButton('Cancel', 'ui-edit-button-cancel', this.handleCancel);
- const resetBtn = this.createButton('Reset', 'ui-edit-button-reset', this.handleReset);
-
- // Style buttons for side panel layout
- [acceptBtn, cancelBtn, resetBtn].forEach(btn => {
- btn.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;
- `;
- });
-
- acceptBtn.style.background = '#28a745';
- cancelBtn.style.background = '#dc3545';
- resetBtn.style.background = '#fd7e14';
-
- controls.appendChild(acceptBtn);
- controls.appendChild(cancelBtn);
- controls.appendChild(resetBtn);
-
- // Create unified floating menu using new component
- this.currentFloatingMenu = new FloatingMenu(sectionId, 'text', this);
- const floatingMenu = this.currentFloatingMenu.show(editorContent, controls);
- if (!floatingMenu) return;
-
- textarea.focus();
- this.editingSections.add(sectionId);
-
- textarea.addEventListener('input', () => {
- this.sectionManager.updateContent(sectionId, textarea.value);
- });
-
- // Add keyboard shortcuts
- textarea.addEventListener('keydown', this.handleKeydown);
- }
-
- /**
- * Create a unified floating edit menu
- * @param {string} sectionId - The section being edited
- * @param {string} type - 'text' or 'image'
- * @param {HTMLElement} contentElement - The content area to add to the menu
- * @param {HTMLElement} controlsElement - The controls/buttons to add to the menu
- * @returns {HTMLElement} The floating menu element
- */
-
- /**
- * Show image editor with manipulation controls
- * @param {string} sectionId - The section ID
- * @param {Section} section - The section object
- */
- showImageEditor(sectionId, section) {
- const element = this.findSectionElement(sectionId);
- if (!element) return;
-
- this.hideCurrentEditor();
-
- // Track staging state for this editor
- const stagingState = {
- originalMarkdown: section.currentMarkdown,
- currentAltText: '',
- currentImageSrc: '',
- stagedImageSrc: null,
- stagedAltText: null,
- hasChanges: false
- };
-
- // Create image editor content area (optimized for side-by-side layout)
- const editorContent = document.createElement('div');
- editorContent.className = 'ui-edit-image-content';
- editorContent.style.cssText = `
- display: flex;
- flex-direction: column;
- gap: 15px;
- flex: 1;
- min-width: 0;
- `;
-
- // Parse markdown to extract image info
- const imageMatch = section.currentMarkdown.match(/!\[(.*?)\]\((.*?)\)/);
- if (imageMatch) {
- const [, altText, imageSrc] = imageMatch;
- stagingState.currentAltText = altText;
- stagingState.currentImageSrc = imageSrc;
- }
-
- // Image preview with drop zone for floating menu
- 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: 250px;
- 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: 18px;
- `;
- 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: 16px;
- `;
- 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 for floating menu
- 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();
- });
-
- // Add keyboard shortcuts to alt text input
- altTextInput.addEventListener('keydown', this.handleKeydown);
-
- altTextContainer.appendChild(altTextLabel);
- altTextContainer.appendChild(altTextInput);
-
- // Change indicator for floating menu
- 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';
- }
- };
-
- // Responsive controls container with alt text
- const editorControls = document.createElement('div');
- editorControls.className = 'ui-edit-controls';
- editorControls.style.cssText = `
- display: flex;
- flex-direction: column;
- gap: 12px;
- min-width: 180px;
- padding: 12px;
- justify-content: space-between;
- align-items: stretch;
- `;
-
- const acceptBtn = this.createButton('โ Accept', 'ui-edit-accept', (e) => {
- // 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);
- this.updateSectionContent(sectionId, newMarkdown);
- }
-
- // Accept changes and hide editor
- this.sectionManager.acceptChanges(sectionId);
- this.hideEditor(sectionId);
- });
-
- const cancelBtn = this.createButton('โ Cancel', 'ui-edit-cancel', (e) => {
- // Discard all staged changes and hide editor
- this.sectionManager.cancelChanges(sectionId);
- this.hideEditor(sectionId);
- });
-
- const resetBtn = this.createButton('โบ Reset', 'ui-edit-reset', (e) => {
- // Reset section to original content (like reset all does)
- this.sectionManager.resetSection(sectionId);
-
- // Get the reset section to update staging state with original content
- const resetSection = this.sectionManager.sections.get(sectionId);
- if (resetSection) {
- // Update DOM immediately to show reset content
- this.updateSectionContent(sectionId, resetSection.currentMarkdown);
-
- // Parse original image info from reset content
- const originalImageMatch = resetSection.currentMarkdown.match(/!\[(.*?)\]\((.*?)\)/);
- if (originalImageMatch) {
- const [, originalAltText, originalImageSrc] = originalImageMatch;
-
- // Update staging state to reflect original content
- stagingState.originalMarkdown = resetSection.currentMarkdown;
- 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();
- });
-
- // Assemble content for floating menu
- editorContent.appendChild(imagePreview);
- editorContent.appendChild(altTextContainer);
- editorContent.appendChild(changeIndicator);
- editorContent.appendChild(fileInput);
-
- // Create controls for side panel layout
- const controls = document.createElement('div');
- controls.className = 'ui-edit-controls';
- controls.style.cssText = `
- display: flex;
- flex-direction: column;
- gap: 8px;
- width: 100%;
- `;
-
- // Style buttons for side panel layout
- [acceptBtn, cancelBtn, resetBtn].forEach(btn => {
- btn.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;
- `;
- });
-
- acceptBtn.style.background = '#28a745';
- cancelBtn.style.background = '#dc3545';
- resetBtn.style.background = '#fd7e14';
-
- controls.appendChild(acceptBtn);
- controls.appendChild(cancelBtn);
- controls.appendChild(resetBtn);
-
- // Create unified floating menu using new component
- this.currentFloatingMenu = new FloatingMenu(sectionId, 'image', this);
- const floatingMenu = this.currentFloatingMenu.show(editorContent, controls);
- if (!floatingMenu) return;
-
- altTextInput.focus();
- this.editingSections.add(sectionId);
- }
-
- /**
- * Image manipulation methods
- * Note: Image replacement is now integrated into the main image editor with drag & drop
- */
-
- resizeImage(sectionId) {
- const section = this.sectionManager.sections.get(sectionId);
- const size = prompt('Enter image width (e.g., 300px, 50%, or leave empty for auto):', '');
- if (size !== null) {
- const imageMatch = section.currentMarkdown.match(/!\[(.*?)\]\((.*?)\)/);
- if (imageMatch) {
- const style = size ? ` style="width: ${size};"` : '';
- const newMarkdown = section.currentMarkdown.replace(
- /!\[(.*?)\]\((.*?)\)/,
- ` `
- );
- this.sectionManager.updateContent(sectionId, newMarkdown);
- }
- }
- }
-
- addImageCaption(sectionId) {
- const section = this.sectionManager.sections.get(sectionId);
- const caption = prompt('Enter image caption:', '');
- if (caption) {
- const newMarkdown = section.currentMarkdown + `\n\n*${caption}*`;
- this.sectionManager.updateContent(sectionId, newMarkdown);
- }
- }
-
- removeImage(sectionId) {
- if (confirm('Are you sure you want to remove this image?')) {
- this.sectionManager.updateContent(sectionId, '');
- this.hideEditor(sectionId);
- }
- }
-
- createButton(text, className, handler) {
- const btn = document.createElement('button');
- btn.textContent = text;
- btn.className = className;
- btn.style.cssText = `
- background: #007bff;
- color: white;
- border: none;
- padding: 8px 16px;
- border-radius: 4px;
- cursor: pointer;
- font-size: 14px;
- `;
- btn.addEventListener('click', handler);
- return btn;
- }
-
- handleAccept(event) {
- event.preventDefault();
- event.stopPropagation();
-
- const sectionId = this.getCurrentEditingSectionId(event.target);
- if (sectionId) {
- this.sectionManager.acceptChanges(sectionId);
- this.hideEditor(sectionId);
- }
- }
-
- handleCancel(event) {
- event.preventDefault();
- event.stopPropagation();
-
- const sectionId = this.getCurrentEditingSectionId(event.target);
- if (sectionId) {
- this.sectionManager.cancelChanges(sectionId);
- this.hideEditor(sectionId);
- }
- }
-
- handleReset(event) {
- event.preventDefault();
- event.stopPropagation();
-
- const sectionId = this.getCurrentEditingSectionId(event.target);
- if (sectionId) {
- this.sectionManager.resetSection(sectionId);
-
- // Update DOM to show reset content
- const section = this.sectionManager.sections.get(sectionId);
- if (section) {
- this.updateSectionContent(sectionId, section.currentMarkdown);
- }
-
- this.hideEditor(sectionId);
- }
- }
-
- handleKeydown(event) {
- // Enhanced keyboard shortcuts for section editing
- const textarea = event.target.closest('textarea, input');
- if (!textarea) return;
-
- // Find the section being edited
- const editorContainer = textarea.closest('.ui-edit-editor-container, .ui-edit-image-editor-container');
- if (!editorContainer) return;
-
- const sectionElement = editorContainer.parentElement;
- const sectionId = sectionElement ? sectionElement.getAttribute('data-section-id') : null;
- if (!sectionId) return;
-
- let shortcutKey = '';
- let shortcutAction = '';
-
- // Handle keyboard shortcuts
- if (event.ctrlKey || event.metaKey) {
- switch (event.key) {
- case 'Enter':
- event.preventDefault();
- shortcutKey = (event.ctrlKey ? 'ctrl' : 'cmd') + '+enter';
- shortcutAction = 'accept';
- this.sectionManager.acceptChanges(sectionId);
- debug('Keyboard shortcut: Ctrl+Enter - accepted changes for section ' + sectionId, 'KEYBOARD');
- break;
- case 'Escape':
- event.preventDefault();
- shortcutKey = (event.ctrlKey ? 'ctrl' : 'cmd') + '+escape';
- shortcutAction = 'cancel';
- this.sectionManager.cancelChanges(sectionId);
- debug('Keyboard shortcut: Ctrl+Escape - cancelled changes for section ' + sectionId, 'KEYBOARD');
- break;
- case 's':
- event.preventDefault();
- shortcutKey = (event.ctrlKey ? 'ctrl' : 'cmd') + '+s';
- shortcutAction = 'save';
- this.sectionManager.acceptChanges(sectionId);
- debug('Keyboard shortcut: Ctrl+S - saved changes for section ' + sectionId, 'KEYBOARD');
- break;
- }
- }
-
- // Handle plain Escape (without Ctrl)
- if (event.key === 'Escape' && !event.ctrlKey && !event.metaKey) {
- event.preventDefault();
- shortcutKey = 'escape';
- shortcutAction = 'cancel';
- this.sectionManager.cancelChanges(sectionId);
- debug('Keyboard shortcut: Escape - cancelled changes for section ' + sectionId, 'KEYBOARD');
- }
-
- // Track keyboard shortcut events
- if (shortcutKey && shortcutAction) {
- this.trackEvent('keyboard-shortcut', {
- sectionId,
- shortcut: shortcutKey,
- action: shortcutAction,
- key: event.key,
- ctrlKey: event.ctrlKey,
- metaKey: event.metaKey,
- event,
- timestamp: Date.now()
- });
- }
- }
-
- getCurrentEditingSectionId(button) {
- // Check if button is in a floating menu
- const floatingMenu = button.closest('.ui-edit-floating-menu');
- if (floatingMenu) {
- return floatingMenu.dataset.sectionId;
- }
-
- // Fallback to old overlay container method
- const editorContainer = button.closest('.ui-edit-editor-container, .ui-edit-image-editor-container, .ui-edit-overlay-container');
- if (!editorContainer) return null;
-
- const sectionElement = editorContainer.parentElement;
- return sectionElement ? sectionElement.getAttribute('data-section-id') : null;
- }
-
- hideEditor(sectionId) {
- const element = this.findSectionElement(sectionId);
-
- // Remove floating menu if it exists
- const floatingMenu = document.querySelector(`.ui-edit-floating-menu[data-section-id="${sectionId}"]`);
- if (floatingMenu) {
- floatingMenu.remove();
- }
-
- // Remove element highlighting
- if (element) {
- element.style.outline = '';
- element.style.outlineOffset = '';
- element.style.backgroundColor = '';
-
- // Remove any old editor UI containers from the DOM
- const textEditorContainer = element.querySelector('.ui-edit-editor-container');
- if (textEditorContainer) {
- textEditorContainer.remove();
- }
-
- const imageEditorContainer = element.querySelector('.ui-edit-image-editor-container');
- if (imageEditorContainer) {
- imageEditorContainer.remove();
- }
-
- // Remove overlay containers and restore original content if needed
- const overlayContainer = element.querySelector('.ui-edit-overlay-container');
- if (overlayContainer) {
- const originalContent = overlayContainer.dataset.originalContent;
- overlayContainer.remove();
-
- // Restore element's original positioning
- element.style.position = '';
-
- // If original content was stored, restore it
- if (originalContent) {
- element.innerHTML = originalContent;
- } else {
- // Otherwise, refresh the section content
- const section = this.sectionManager.sections.get(sectionId);
- if (section) {
- this.updateSectionContent(sectionId, section.currentMarkdown);
- }
- }
- }
-
- const section = this.sectionManager.sections.get(sectionId);
- if (section) {
- this.updateSectionContent(sectionId, section.currentMarkdown);
- }
- }
-
- this.editingSections.delete(sectionId);
- }
-
- hideCurrentEditor() {
- debug('EDITOR: hideCurrentEditor called', 'EDITOR');
-
- // Hide FloatingMenu if it exists
- if (this.currentFloatingMenu) {
- debug('EDITOR: Hiding currentFloatingMenu', 'EDITOR');
- this.currentFloatingMenu.hide();
- this.currentFloatingMenu = null;
- }
-
- // Hide any legacy editors
- this.editingSections.forEach(sectionId => {
- const element = this.findSectionElement(sectionId);
- if (element && element.querySelector('.ui-edit-editor-container')) {
- this.hideEditor(sectionId);
- }
- });
-
- // CRUCIAL FIX: Ensure ALL sections are reset from editing state
- this.sectionManager.sections.forEach((section, sectionId) => {
- if (section.isEditing()) {
- debug('EDITOR: Force stopping editing for stuck section: ' + sectionId, 'EDITOR');
- section.stopEditing();
- }
- });
-
- // Clear the editing sections set
- this.editingSections.clear();
-
- // Clean up any floating menu elements
- const floatingMenus = document.querySelectorAll('.ui-edit-floating-menu');
- floatingMenus.forEach(menu => menu.remove());
-
- debug('EDITOR: hideCurrentEditor completed', 'EDITOR');
- }
-
- updateSectionContent(sectionId, content) {
- const element = this.findSectionElement(sectionId);
- if (!element) return;
-
- if (typeof marked !== 'undefined') {
- const html = marked.parse(content);
- const htmlWithTargetBlank = html.replace(/ ]*)>/g, ' ');
- element.innerHTML = htmlWithTargetBlank;
- } else {
- element.innerHTML = `${content}
`;
- }
-
- this.setupSectionElement(element);
- }
-
- findSectionElement(sectionId) {
- return this.container.querySelector(`[data-section-id="${sectionId}"]`);
- }
-
- setupSectionElement(element) {
- element.className = 'ui-edit-section';
- element.draggable = true; // Enable drag and drop
- element.tabIndex = 0; // Make focusable for accessibility
- element.style.cssText = `
- margin: 16px 0;
- padding: 12px;
- border-radius: 6px;
- cursor: pointer;
- transition: all 0.2s ease;
- border: 2px solid transparent;
- position: relative;
- `;
-
- // Add drag handle indicator
- const dragHandle = document.createElement('div');
- dragHandle.className = 'ui-edit-drag-handle';
- dragHandle.innerHTML = 'โฎโฎ';
- dragHandle.style.cssText = `
- position: absolute;
- left: -8px;
- top: 50%;
- transform: translateY(-50%);
- color: #ccc;
- font-size: 16px;
- line-height: 1;
- cursor: grab;
- opacity: 0;
- transition: opacity 0.2s ease;
- user-select: none;
- `;
- element.appendChild(dragHandle);
-
- element.removeEventListener('mouseenter', element._mouseenterHandler);
- element.removeEventListener('mouseleave', element._mouseleaveHandler);
-
- element._mouseenterHandler = () => {
- element.style.backgroundColor = 'rgba(0, 0, 0, 0.02)';
- element.style.borderColor = 'rgba(0, 0, 0, 0.1)';
- dragHandle.style.opacity = '1';
- };
-
- element._mouseleaveHandler = () => {
- element.style.backgroundColor = '';
- element.style.borderColor = 'transparent';
- dragHandle.style.opacity = '0';
- };
-
- element.addEventListener('mouseenter', element._mouseenterHandler);
- element.addEventListener('mouseleave', element._mouseleaveHandler);
-
- // Enhanced accessibility
- element.setAttribute('role', 'article');
- element.setAttribute('aria-label', 'Editable section');
- element.setAttribute('title', 'Click to edit, right-click for options, drag to reorder');
-
- // Apply comprehensive styling enhancements
- this.applyComprehensiveStyling(element);
- }
-
- /**
- * Apply comprehensive styling enhancements to section elements
- * @param {HTMLElement} element - The section element
- */
- applyComprehensiveStyling(element) {
- if (!element || !element.dataset.sectionId) return;
-
- const sectionId = element.dataset.sectionId;
- const section = this.sectionManager.sections.get(sectionId);
- if (!section) return;
-
- // Apply type-specific styling
- this.applyTypeSpecificStyling(element, section);
-
- // Apply state-based styling
- this.applyStateStyling(element, section);
-
- // Apply responsive design
- this.applyResponsiveStyling(element);
-
- // Apply accessibility enhancements
- this.enhanceAccessibility(element, section);
-
- // Apply length-based styling
- this.applyLengthBasedStyling(element, section);
-
- // Apply performance-optimized transitions
- this.applyOptimizedTransitions(element);
-
- // Apply CSS custom properties
- this.applyCSSCustomProperties(element, section);
-
- // Apply theme support
- this.applySectionTheme(element, 'light'); // Default theme
-
- // Apply content analysis styling
- this.analyzeContentForStyling(element, section);
-
- // Apply CSS reset and normalization
- this.applyCSSReset(element);
-
- // Setup animation support
- this.setupAnimationSupport(element);
-
- // Apply print-friendly styling
- this.applyPrintStyling(element);
-
- // Integrate with existing systems
- this.integrateWithMessageSystem(element);
- this.integrateWithControlPanel(element);
- }
-
- /**
- * Apply type-specific styling to section elements
- * @param {HTMLElement} element - Section element
- * @param {Section} section - Section object
- */
- applyTypeSpecificStyling(element, section) {
- // Add base class
- element.classList.add('markitect-section-editable');
-
- // Add type-specific class
- const typeClass = `markitect-section-${section.type || 'paragraph'}`;
- element.classList.add(typeClass);
-
- // Set data attributes
- element.dataset.sectionType = section.type || 'paragraph';
- element.dataset.sectionId = section.id;
-
- // Type-specific styling
- const typeStyles = {
- heading: {
- borderLeft: '4px solid #007acc',
- backgroundColor: 'rgba(0, 122, 204, 0.02)',
- fontWeight: '600'
- },
- code: {
- borderLeft: '4px solid #28a745',
- backgroundColor: 'rgba(40, 167, 69, 0.02)',
- fontFamily: 'monospace'
- },
- list: {
- borderLeft: '4px solid #ffc107',
- backgroundColor: 'rgba(255, 193, 7, 0.02)'
- },
- quote: {
- borderLeft: '4px solid #6f42c1',
- backgroundColor: 'rgba(111, 66, 193, 0.02)',
- fontStyle: 'italic'
- },
- image: {
- borderLeft: '4px solid #fd7e14',
- backgroundColor: 'rgba(253, 126, 20, 0.02)',
- textAlign: 'center'
- },
- table: {
- borderLeft: '4px solid #20c997',
- backgroundColor: 'rgba(32, 201, 151, 0.02)'
- },
- hr: {
- borderLeft: '4px solid #6c757d',
- backgroundColor: 'rgba(108, 117, 125, 0.02)',
- minHeight: '20px'
- }
- };
-
- const styles = typeStyles[section.type] || typeStyles.paragraph || {};
- Object.assign(element.style, styles);
- }
-
- /**
- * Apply state-based styling
- * @param {HTMLElement} element - Section element
- * @param {Section} section - Section object
- */
- applyStateStyling(element, section) {
- // Remove existing state classes
- element.classList.remove('section-original', 'section-editing', 'section-modified', 'section-saved');
-
- // Apply current state class
- if (section.isEditing()) {
- element.classList.add('section-editing');
- element.style.backgroundColor = 'rgba(0, 122, 204, 0.1)';
- element.style.borderColor = '#007acc';
- } else if (section.hasChanges()) {
- element.classList.add('section-modified');
- element.style.backgroundColor = 'rgba(255, 193, 7, 0.1)';
- element.style.borderColor = '#ffc107';
- } else if (section.state === 'saved') {
- element.classList.add('section-saved');
- element.style.backgroundColor = 'rgba(40, 167, 69, 0.1)';
- element.style.borderColor = '#28a745';
- } else {
- element.classList.add('section-original');
- }
- }
-
- /**
- * Apply responsive design styling
- * @param {HTMLElement} element - Section element
- */
- applyResponsiveStyling(element) {
- element.classList.add('section-responsive');
-
- // Responsive styles
- element.style.cssText += `
- max-width: 100%;
- min-width: 0;
- overflow-wrap: break-word;
- word-wrap: break-word;
- `;
-
- // Responsive behavior based on viewport
- const updateResponsiveStyles = () => {
- const width = window.innerWidth;
- if (width < 768) {
- // Mobile styles
- element.style.margin = '8px 0';
- element.style.padding = '8px';
- element.style.fontSize = '14px';
- } else if (width < 1024) {
- // Tablet styles
- element.style.margin = '12px 0';
- element.style.padding = '10px';
- element.style.fontSize = '15px';
- } else {
- // Desktop styles
- element.style.margin = '16px 0';
- element.style.padding = '12px';
- element.style.fontSize = '16px';
- }
- };
-
- updateResponsiveStyles();
- window.addEventListener('resize', updateResponsiveStyles);
- }
-
- /**
- * Enhance accessibility features
- * @param {HTMLElement} element - Section element
- * @param {Section} section - Section object
- */
- enhanceAccessibility(element, section) {
- // Enhanced ARIA attributes
- element.setAttribute('aria-describedby', `section-desc-${section.id}`);
- element.setAttribute('aria-labelledby', `section-label-${section.id}`);
-
- // Screen reader support
- const srOnly = document.createElement('span');
- srOnly.className = 'sr-only';
- srOnly.id = `section-desc-${section.id}`;
- srOnly.textContent = `${section.type} section with ${section.currentMarkdown.length} characters`;
- srOnly.style.cssText = `
- position: absolute;
- width: 1px;
- height: 1px;
- padding: 0;
- margin: -1px;
- overflow: hidden;
- clip: rect(0,0,0,0);
- white-space: nowrap;
- border: 0;
- `;
- element.appendChild(srOnly);
-
- // Enhanced keyboard navigation
- element.tabIndex = 0;
- element.setAttribute('aria-keyshortcuts', 'Enter Space');
-
- // Focus enhancement
- element.addEventListener('focus', () => {
- element.style.outline = '2px solid #007acc';
- element.style.outlineOffset = '2px';
- });
-
- element.addEventListener('blur', () => {
- element.style.outline = '';
- element.style.outlineOffset = '';
- });
- }
-
- /**
- * Apply length-based visual indicators
- * @param {HTMLElement} element - Section element
- * @param {Section} section - Section object
- */
- applyLengthBasedStyling(element, section) {
- const length = section.currentMarkdown.length;
-
- // Remove existing length classes
- element.classList.remove('section-short', 'section-medium', 'section-long');
-
- if (length < 100) {
- element.classList.add('section-short');
- element.style.minHeight = '40px';
- } else if (length < 500) {
- element.classList.add('section-medium');
- element.style.minHeight = '60px';
- } else {
- element.classList.add('section-long');
- element.style.minHeight = '80px';
- element.style.maxHeight = '400px';
- element.style.overflowY = 'auto';
- }
-
- // Word count indicator
- const wordCount = section.currentMarkdown.split(/\s+/).length;
- element.dataset.wordCount = wordCount.toString();
- element.setAttribute('title', element.getAttribute('title') + ` (${wordCount} words)`);
- }
-
- /**
- * Apply performance-optimized CSS transitions
- * @param {HTMLElement} element - Section element
- */
- applyOptimizedTransitions(element) {
- // GPU-accelerated transitions
- element.style.willChange = 'transform, opacity';
- element.style.transform = 'translateZ(0)'; // Force GPU layer
- element.style.transition = 'all 0.2s cubic-bezier(0.4, 0, 0.2, 1)';
-
- // Hover enhancements
- element.classList.add('section-hoverable');
-
- const optimizedMouseEnter = () => {
- element.style.transform = 'translateZ(0) scale(1.01)';
- element.style.boxShadow = '0 4px 12px rgba(0, 0, 0, 0.1)';
- };
-
- const optimizedMouseLeave = () => {
- element.style.transform = 'translateZ(0) scale(1)';
- element.style.boxShadow = '';
- };
-
- element.addEventListener('mouseenter', optimizedMouseEnter);
- element.addEventListener('mouseleave', optimizedMouseLeave);
- }
-
- /**
- * Apply CSS custom properties for advanced styling
- * @param {HTMLElement} element - Section element
- * @param {Section} section - Section object
- */
- applyCSSCustomProperties(element, section) {
- // CSS variables for dynamic theming
- element.style.setProperty('--section-primary-color', '#007acc');
- element.style.setProperty('--section-background', 'rgba(248, 249, 250, 0.5)');
- element.style.setProperty('--section-border-radius', '6px');
- element.style.setProperty('--section-padding', '12px');
- element.style.setProperty('--section-margin', '16px 0');
- element.style.setProperty('--section-transition', 'all 0.2s ease');
-
- // Type-specific CSS variables
- const typeColors = {
- heading: '#007acc',
- code: '#28a745',
- list: '#ffc107',
- quote: '#6f42c1',
- image: '#fd7e14',
- table: '#20c997',
- hr: '#6c757d'
- };
-
- element.style.setProperty('--section-type-color', typeColors[section.type] || '#6c757d');
- element.dataset.cssVariables = 'true';
- }
-
- /**
- * Apply theme-based styling
- * @param {HTMLElement} element - Section element
- * @param {string} theme - Theme name ('light', 'dark', 'high-contrast')
- */
- applySectionTheme(element, theme) {
- element.dataset.theme = theme;
- element.classList.remove('theme-light', 'theme-dark', 'theme-high-contrast');
- element.classList.add(`theme-${theme}`);
-
- const themes = {
- light: {
- '--section-background': 'rgba(248, 249, 250, 0.5)',
- '--section-text-color': '#212529',
- '--section-border-color': 'rgba(0, 0, 0, 0.1)',
- '--section-hover-bg': 'rgba(0, 0, 0, 0.02)'
- },
- dark: {
- '--section-background': 'rgba(33, 37, 41, 0.8)',
- '--section-text-color': '#f8f9fa',
- '--section-border-color': 'rgba(255, 255, 255, 0.2)',
- '--section-hover-bg': 'rgba(255, 255, 255, 0.05)'
- },
- 'high-contrast': {
- '--section-background': '#ffffff',
- '--section-text-color': '#000000',
- '--section-border-color': '#000000',
- '--section-hover-bg': '#f0f0f0'
- }
- };
-
- const themeStyles = themes[theme] || themes.light;
- Object.entries(themeStyles).forEach(([property, value]) => {
- element.style.setProperty(property, value);
- });
- }
-
- /**
- * Analyze content for styling purposes
- * @param {HTMLElement} element - Section element
- * @param {Section} section - Section object
- */
- analyzeContentForStyling(element, section) {
- const content = section.currentMarkdown.toLowerCase();
-
- // Content-based classes
- if (content.includes('```') || content.includes('`')) {
- element.classList.add('contains-code');
- }
- if (content.includes('$$') || content.includes('\\(') || content.includes('\\[')) {
- element.classList.add('contains-math');
- }
- if (content.includes('http') || content.includes('[') && content.includes(']')) {
- element.classList.add('contains-links');
- }
- if (content.includes('![')) {
- element.classList.add('contains-images');
- }
- if (content.includes('|') && content.includes('-')) {
- element.classList.add('contains-tables');
- }
-
- // Priority content indicators
- if (content.includes('important') || content.includes('warning') || content.includes('!')) {
- element.classList.add('priority-high');
- element.style.borderLeftWidth = '6px';
- }
- }
-
- /**
- * Apply CSS reset and normalization
- * @param {HTMLElement} element - Section element
- */
- applyCSSReset(element) {
- element.classList.add('css-reset');
-
- // Normalize box model
- element.style.boxSizing = 'border-box';
- element.style.margin = '16px 0';
- element.style.padding = '12px';
-
- // Reset browser defaults
- const allChildren = element.querySelectorAll('*');
- allChildren.forEach(child => {
- child.style.boxSizing = 'border-box';
- });
- }
-
- /**
- * Setup animation support for state transitions
- * @param {HTMLElement} element - Section element
- */
- setupAnimationSupport(element) {
- element.classList.add('animation-ready');
-
- // Animation utility method
- element._animate = (animationType) => {
- this.animateSectionTransition(element, animationType);
- };
- }
-
- /**
- * Animate section transitions
- * @param {HTMLElement} element - Section element
- * @param {string} animationType - Type of animation
- */
- animateSectionTransition(element, animationType) {
- element.classList.add('section-animating');
-
- switch (animationType) {
- case 'enter':
- element.classList.add('transition-entering');
- element.style.opacity = '0';
- element.style.transform = 'translateY(-10px)';
- setTimeout(() => {
- element.style.opacity = '1';
- element.style.transform = 'translateY(0)';
- }, 50);
- break;
- case 'leave':
- element.classList.add('transition-leaving');
- element.style.opacity = '0';
- element.style.transform = 'translateY(10px)';
- break;
- case 'highlight':
- element.style.backgroundColor = 'rgba(255, 193, 7, 0.3)';
- setTimeout(() => {
- element.style.backgroundColor = '';
- }, 1000);
- break;
- }
-
- setTimeout(() => {
- element.classList.remove('section-animating', 'transition-entering', 'transition-leaving');
- }, 300);
- }
-
- /**
- * Apply print-friendly styling
- * @param {HTMLElement} element - Section element
- */
- applyPrintStyling(element) {
- element.classList.add('print-friendly');
- element.dataset.printOptimized = 'true';
-
- // Print-specific styles via CSS
- const printStyles = document.createElement('style');
- printStyles.textContent = `
- @media print {
- .print-friendly {
- break-inside: avoid;
- margin: 8px 0;
- padding: 8px;
- border: 1px solid #000;
- background: white !important;
- color: black !important;
- box-shadow: none !important;
- transform: none !important;
- }
- .ui-edit-drag-handle,
- .sr-only {
- display: none !important;
- }
- }
- `;
- if (!document.querySelector('#section-print-styles')) {
- printStyles.id = 'section-print-styles';
- document.head.appendChild(printStyles);
- }
- }
-
- /**
- * Update dynamic styles based on section state
- * @param {HTMLElement} element - Section element
- * @param {Object} newStyles - Style updates
- */
- updateSectionDynamicStyles(element, newStyles) {
- Object.entries(newStyles).forEach(([property, value]) => {
- if (property.startsWith('--')) {
- element.style.setProperty(property, value);
- } else {
- element.style[property] = value;
- }
- });
- }
-
- /**
- * Integrate with message system styling
- * @param {HTMLElement} element - Section element
- */
- integrateWithMessageSystem(element) {
- // Add message system integration class
- element.classList.add('message-system-ready');
-
- // Store reference for message positioning
- element.dataset.messageAnchor = 'true';
- }
-
- /**
- * Integrate with control panel styling
- * @param {HTMLElement} element - Section element
- */
- integrateWithControlPanel(element) {
- // Add control panel integration class
- element.classList.add('control-panel-aware');
-
- // Adjust positioning to avoid overlap with control panel
- const adjustForControlPanel = () => {
- const controlPanel = document.getElementById('markitect-global-controls');
- if (controlPanel) {
- const rect = controlPanel.getBoundingClientRect();
- const elementRect = element.getBoundingClientRect();
-
- // Avoid overlap with control panel
- if (elementRect.right > rect.left && elementRect.top < rect.bottom) {
- element.style.marginRight = `${rect.width + 20}px`;
- }
- }
- };
-
- // Check for overlap periodically
- setTimeout(adjustForControlPanel, 100);
- }
-
- /**
- * 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);
- }
-
- /**
- * Create status panel for real-time status display
- * @returns {HTMLElement} Status panel element
- */
- // createStatusPanel method removed - floating status panel no longer needed
- // Status information is displayed in the control panel menu instead
-
- /**
- * Update status display with current status information
- * @param {Object} status - Status object from SectionManager
- */
- updateStatusDisplay(status) {
- // Status information is now only displayed in the control panel menu
- // No floating status panel needed since status is integrated into the menu
- return;
- }
-}
-
-/**
- * Main Editor Integration
- */
-class MarkitectCleanEditor {
- constructor(markdownContent, containerElement, options = {}) {
- debug('10: MarkitectCleanEditor constructor called', 'EDITOR');
-
- this.options = {
- theme: 'github',
- keyboardShortcuts: true,
- autosave: false,
- originalFilename: null,
- ...options
- };
-
- debug('11: Creating SectionManager', 'EDITOR');
- this.sectionManager = new SectionManager();
-
- debug('12: Creating DOMRenderer', 'EDITOR');
- this.domRenderer = new DOMRenderer(this.sectionManager, containerElement);
-
- // Set global reference for debug panel
- window.currentDOMRenderer = this.domRenderer;
-
- // Initialize debug system for this page load
- this.initializeDebugSystem();
-
- this.originalMarkdown = markdownContent;
-
- this.cleanMarkdownContent = options.cleanMarkdownContent || markdownContent;
- this.dogtagContent = options.dogtagContent || '';
-
- debug('13: About to call initialize()', 'EDITOR');
- this.initialize();
- debug('14: initialize() completed', 'EDITOR');
- }
-
- initialize() {
- try {
- debug('15: Starting initialize() - markdown length: ' + this.originalMarkdown.length, 'INIT');
-
- const sections = this.sectionManager.createSectionsFromMarkdown(this.originalMarkdown);
- debug('16: Created ' + sections.length + ' sections', 'INIT');
-
- // Mark the dogtag section as protected if we have a dogtag
- if (this.dogtagContent) {
- debug('17: Marking dogtag section', 'INIT');
- this.markDogtagSection(sections);
- }
-
- // Mark base64 reference sections as protected
- debug('18: Marking base64 reference sections', 'INIT');
- this.markBase64ReferenceSections(sections);
-
- console.log(`โ Initialized clean editor with ${sections.length} sections`);
-
- // Add global control panel
- debug('19: Adding global controls', 'INIT');
- this.addGlobalControls();
-
- // Setup status tracking
- debug('19.5: Setting up status tracking', 'INIT');
- this.setupStatusTracking();
-
- debug('20: Initialize completed successfully', 'INIT');
- return true;
- } catch (error) {
- debug('ERROR in initialize: ' + error.message, 'ERROR');
- console.error('Failed to initialize clean editor:', error);
- return false;
- }
- }
-
- markDogtagSection(sections) {
- // Find the section that contains the dogtag content
- const dogtagText = this.dogtagContent.trim();
- if (!dogtagText) return;
-
- for (const section of sections) {
- if (section.currentMarkdown.includes(dogtagText)) {
- section.isDogtagSection = true;
- console.log('Marked dogtag section as protected:', section.id);
- break;
- }
- }
- }
-
- markBase64ReferenceSections(sections) {
- // Find sections that contain base64 image references
- if (!window.markitectBase64References) return;
-
- const refIds = Object.keys(window.markitectBase64References);
- if (refIds.length === 0) return;
-
- for (const section of sections) {
- const markdown = section.currentMarkdown;
-
- // Check if this section contains base64 reference syntax
- for (const refId of refIds) {
- if (markdown.includes(`[${refId}]:`)) {
- section.isBase64RefSection = true;
- console.log('Marked base64 reference section as protected:', section.id);
- break;
- }
- }
- }
- }
-
- addGlobalControls() {
- // Create a floating control panel
- const controlPanel = document.createElement('div');
- controlPanel.id = 'markitect-global-controls';
- controlPanel.style.cssText = `
- position: fixed;
- top: 20px;
- right: 20px;
- background: rgba(248, 249, 250, 0.95);
- border: 1px solid #dee2e6;
- border-radius: 8px;
- padding: 12px;
- box-shadow: 0 4px 12px rgba(0,0,0,0.15);
- z-index: 1000;
- backdrop-filter: blur(8px);
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
- font-size: 14px;
- min-width: 200px;
- `;
-
- const title = document.createElement('div');
- title.style.cssText = `
- font-weight: 600;
- margin-bottom: 8px;
- color: #495057;
- border-bottom: 1px solid #dee2e6;
- padding-bottom: 4px;
- `;
- title.textContent = 'Document Controls';
-
- const buttonContainer = document.createElement('div');
- buttonContainer.style.cssText = `
- display: flex;
- flex-direction: column;
- gap: 6px;
- `;
-
- // Save Document button
- const saveButton = document.createElement('button');
- saveButton.id = 'save-document';
- saveButton.textContent = '๐พ Save Document';
- saveButton.style.cssText = `
- background: #28a745;
- color: white;
- border: none;
- padding: 8px 12px;
- border-radius: 4px;
- cursor: pointer;
- font-size: 13px;
- font-weight: 500;
- transition: background-color 0.2s;
- `;
-
- // Reset All button
- const resetButton = document.createElement('button');
- resetButton.id = 'reset-all';
- resetButton.textContent = '๐ Reset All';
- resetButton.style.cssText = `
- background: #ffc107;
- color: #212529;
- border: none;
- padding: 8px 12px;
- border-radius: 4px;
- cursor: pointer;
- font-size: 13px;
- font-weight: 500;
- transition: background-color 0.2s;
- `;
-
- // Show Status button
- const statusButton = document.createElement('button');
- statusButton.id = 'show-status';
- statusButton.textContent = '๐ Show Status';
- statusButton.style.cssText = `
- background: #17a2b8;
- color: white;
- border: none;
- padding: 8px 12px;
- border-radius: 4px;
- cursor: pointer;
- font-size: 13px;
- font-weight: 500;
- transition: background-color 0.2s;
- `;
-
- // Debug button
- const debugButton = document.createElement('button');
- debugButton.id = 'toggle-debug';
- debugButton.textContent = '๐ Debug';
- debugButton.style.cssText = `
- background: #6c757d;
- color: white;
- border: none;
- padding: 8px 12px;
- border-radius: 4px;
- margin-left: 8px;
- cursor: pointer;
- font-size: 13px;
- font-weight: 500;
- transition: background-color 0.2s;
- `;
-
- buttonContainer.appendChild(saveButton);
- buttonContainer.appendChild(resetButton);
- buttonContainer.appendChild(statusButton);
- buttonContainer.appendChild(debugButton);
-
- // Debug messages container
- const debugContainer = document.createElement('div');
- debugContainer.id = 'debug-messages-container';
- debugContainer.style.cssText = `
- margin-top: 12px;
- max-height: 300px;
- overflow-y: auto;
- border: 1px solid #dee2e6;
- border-radius: 4px;
- background: #f8f9fa;
- padding: 8px;
- font-family: 'Courier New', monospace;
- font-size: 12px;
- line-height: 1.4;
- display: none;
- `;
-
- controlPanel.appendChild(title);
- controlPanel.appendChild(buttonContainer);
- controlPanel.appendChild(debugContainer);
-
- document.body.appendChild(controlPanel);
-
- // Add event listeners
- document.getElementById('save-document').addEventListener('click', () => this.saveDocument());
- document.getElementById('reset-all').addEventListener('click', () => this.resetAllSections());
- document.getElementById('show-status').addEventListener('click', () => this.showDocumentStatus());
- document.getElementById('toggle-debug').addEventListener('click', () => this.toggleDebugPanel());
-
- // Store reference for enhanced control panel methods
- this.controlPanel = controlPanel;
-
- // Initialize enhanced features
- this.setupControlPanelEnhancements();
- }
-
- /**
- * Enhanced method to create floating control panel (for TDD compatibility)
- * @returns {HTMLElement} The control panel element
- */
- createFloatingControlPanel() {
- // If control panel already exists, return it
- if (this.controlPanel) {
- return this.controlPanel;
- }
-
- // Create enhanced control panel
- this.addGlobalControls();
- return this.controlPanel;
- }
-
- /**
- * Setup enhanced control panel features
- */
- setupControlPanelEnhancements() {
- if (!this.controlPanel) return;
-
- // Add draggable functionality
- this.makeControlPanelDraggable();
-
- // Add collapsible functionality
- this.addCollapsibleFeature();
-
- // Add statistics display
- this.addStatisticsDisplay();
-
- // Setup keyboard shortcuts
- this.setupControlPanelKeyboard();
-
- // Apply responsive design
- this.setupResponsiveControlPanel();
-
- // Load user preferences
- this.loadControlPanelPreferences();
-
- // Setup animations
- this.setupControlPanelAnimations();
-
- // Apply default theme
- this.setControlPanelTheme('light');
- }
-
- /**
- * Make control panel draggable
- */
- makeControlPanelDraggable() {
- if (!this.controlPanel) return;
-
- const title = this.controlPanel.querySelector('div');
- if (title) {
- title.style.cursor = 'move';
- title.draggable = true;
-
- let isDragging = false;
- let startX, startY, initialX, initialY;
-
- title.addEventListener('mousedown', (e) => {
- isDragging = true;
- startX = e.clientX;
- startY = e.clientY;
- initialX = this.controlPanel.offsetLeft;
- initialY = this.controlPanel.offsetTop;
- title.style.userSelect = 'none';
- });
-
- document.addEventListener('mousemove', (e) => {
- if (!isDragging) return;
-
- const deltaX = e.clientX - startX;
- const deltaY = e.clientY - startY;
-
- this.controlPanel.style.left = `${initialX + deltaX}px`;
- this.controlPanel.style.top = `${initialY + deltaY}px`;
- this.controlPanel.style.right = 'auto'; // Override right positioning
- });
-
- document.addEventListener('mouseup', () => {
- if (isDragging) {
- isDragging = false;
- title.style.userSelect = '';
- this.saveControlPanelPreferences();
- }
- });
- }
- }
-
- /**
- * Add collapsible/expandable functionality
- */
- addCollapsibleFeature() {
- if (!this.controlPanel) return;
-
- const title = this.controlPanel.querySelector('div');
- const buttonContainer = this.controlPanel.querySelector('div:last-child');
-
- if (title && buttonContainer) {
- // Add toggle button
- const toggleBtn = document.createElement('span');
- toggleBtn.textContent = 'โผ';
- toggleBtn.className = 'panel-toggle';
- toggleBtn.style.cssText = `
- float: right;
- cursor: pointer;
- font-size: 12px;
- transition: transform 0.3s ease;
- `;
-
- title.appendChild(toggleBtn);
-
- // Toggle functionality
- let isCollapsed = false;
- toggleBtn.addEventListener('click', () => {
- this.toggleControlPanel();
- });
- }
- }
-
- /**
- * Toggle control panel collapsed state
- */
- toggleControlPanel() {
- if (!this.controlPanel) return;
-
- const buttonContainer = this.controlPanel.querySelector('div:last-child');
- const toggleBtn = this.controlPanel.querySelector('.panel-toggle');
-
- if (buttonContainer && toggleBtn) {
- const isCollapsed = buttonContainer.style.display === 'none';
-
- if (isCollapsed) {
- buttonContainer.style.display = 'flex';
- toggleBtn.textContent = 'โผ';
- toggleBtn.style.transform = 'rotate(0deg)';
- this.controlPanel.classList.remove('collapsed');
- } else {
- buttonContainer.style.display = 'none';
- toggleBtn.textContent = 'โถ';
- toggleBtn.style.transform = 'rotate(-90deg)';
- this.controlPanel.classList.add('collapsed');
- }
-
- this.saveControlPanelPreferences();
- }
- }
-
- /**
- * Add real-time statistics display
- */
- addStatisticsDisplay() {
- if (!this.controlPanel) return;
-
- const statsDiv = document.createElement('div');
- statsDiv.className = 'control-panel-stats';
- statsDiv.style.cssText = `
- margin-top: 8px;
- padding-top: 8px;
- border-top: 1px solid #dee2e6;
- font-size: 12px;
- color: #6c757d;
- `;
-
- // Insert before button container
- const buttonContainer = this.controlPanel.querySelector('div:last-child');
- this.controlPanel.insertBefore(statsDiv, buttonContainer);
-
- // Update stats periodically
- this.updateControlPanelStats();
- setInterval(() => this.updateControlPanelStats(), 3000);
- }
-
- /**
- * Update control panel statistics
- */
- updateControlPanelStats() {
- const statsDiv = this.controlPanel?.querySelector('.control-panel-stats');
- if (!statsDiv) return;
-
- const status = this.sectionManager.getDocumentStatus();
- const eventStats = this.domRenderer.getEventStats();
-
- statsDiv.innerHTML = `
- ${status.totalSections} sections
- ${status.editingSections} editing
- ${eventStats.totalEvents} events
- `;
- }
-
- /**
- * Setup keyboard shortcuts for control panel
- */
- setupControlPanelKeyboard() {
- document.addEventListener('keydown', (e) => {
- this.handleControlPanelKeyboard(e);
- });
- }
-
- /**
- * Handle control panel keyboard shortcuts
- * @param {KeyboardEvent} e - Keyboard event
- */
- handleControlPanelKeyboard(e) {
- // Ctrl+P: Toggle panel
- if (e.ctrlKey && e.key === 'p') {
- e.preventDefault();
- this.toggleControlPanel();
- this.domRenderer.trackEvent('keyboard-shortcut', { shortcut: 'ctrl+p', action: 'toggle-panel' });
- }
-
- // Ctrl+S: Save document
- if (e.ctrlKey && e.key === 's') {
- e.preventDefault();
- this.saveDocument();
- this.domRenderer.trackEvent('keyboard-shortcut', { shortcut: 'ctrl+s', action: 'save' });
- }
-
- // Ctrl+Shift+S: Show status
- if (e.ctrlKey && e.shiftKey && e.key === 'S') {
- e.preventDefault();
- this.showDocumentStatus();
- this.domRenderer.trackEvent('keyboard-shortcut', { shortcut: 'ctrl+shift+s', action: 'status' });
- }
- }
-
- /**
- * Setup responsive design for control panel
- */
- setupResponsiveControlPanel() {
- this.adjustControlPanelForViewport(window.innerWidth);
-
- window.addEventListener('resize', () => {
- this.adjustControlPanelForViewport(window.innerWidth);
- });
- }
-
- /**
- * Adjust control panel for different viewport sizes
- * @param {number} width - Viewport width
- */
- adjustControlPanelForViewport(width) {
- if (!this.controlPanel) return;
-
- if (width < 768) {
- // Mobile layout
- this.controlPanel.style.cssText += `
- top: 10px;
- right: 10px;
- left: auto;
- min-width: 150px;
- font-size: 12px;
- `;
- } else {
- // Desktop layout
- this.controlPanel.style.cssText += `
- top: 20px;
- right: 20px;
- left: auto;
- min-width: 200px;
- font-size: 14px;
- `;
- }
- }
-
- /**
- * Save control panel preferences to localStorage
- */
- saveControlPanelPreferences() {
- if (!this.controlPanel) return;
-
- try {
- const preferences = {
- top: this.controlPanel.style.top,
- left: this.controlPanel.style.left,
- right: this.controlPanel.style.right,
- collapsed: this.controlPanel.classList.contains('collapsed'),
- theme: this.controlPanel.dataset.theme || 'light'
- };
-
- localStorage.setItem('markitect-control-panel-prefs', JSON.stringify(preferences));
- } catch (error) {
- console.warn('Could not save control panel preferences:', error);
- }
- }
-
- /**
- * Load control panel preferences from localStorage
- */
- loadControlPanelPreferences() {
- if (!this.controlPanel) return;
-
- try {
- const saved = localStorage.getItem('markitect-control-panel-prefs');
- if (saved) {
- const preferences = JSON.parse(saved);
-
- if (preferences.top) this.controlPanel.style.top = preferences.top;
- if (preferences.left) this.controlPanel.style.left = preferences.left;
- if (preferences.right) this.controlPanel.style.right = preferences.right;
-
- if (preferences.collapsed) {
- this.toggleControlPanel();
- }
-
- if (preferences.theme) {
- this.setControlPanelTheme(preferences.theme);
- }
- }
- } catch (error) {
- console.warn('Could not load control panel preferences:', error);
- }
- }
-
- /**
- * Setup control panel animations
- */
- setupControlPanelAnimations() {
- if (!this.controlPanel) return;
-
- this.controlPanel.style.transition = 'all 0.3s ease';
- this.animateControlPanel('fadeIn');
- }
-
- /**
- * Animate control panel
- * @param {string} animation - Animation type
- */
- animateControlPanel(animation) {
- if (!this.controlPanel) return;
-
- switch (animation) {
- case 'fadeIn':
- this.controlPanel.style.opacity = '0';
- setTimeout(() => {
- this.controlPanel.style.opacity = '1';
- }, 100);
- break;
- case 'slideIn':
- this.controlPanel.style.transform = 'translateX(100%)';
- setTimeout(() => {
- this.controlPanel.style.transform = 'translateX(0)';
- }, 100);
- break;
- }
- }
-
- /**
- * Set control panel theme
- * @param {string} theme - Theme name ('light', 'dark')
- */
- setControlPanelTheme(theme) {
- if (!this.controlPanel) return;
-
- this.controlPanel.dataset.theme = theme;
-
- const themes = {
- light: {
- background: 'rgba(248, 249, 250, 0.95)',
- border: '#dee2e6',
- text: '#495057',
- buttonColors: {
- save: '#28a745',
- reset: '#ffc107',
- status: '#17a2b8'
- }
- },
- dark: {
- background: 'rgba(33, 37, 41, 0.95)',
- border: '#495057',
- text: '#f8f9fa',
- buttonColors: {
- save: '#198754',
- reset: '#fd7e14',
- status: '#0dcaf0'
- }
- }
- };
-
- const selectedTheme = themes[theme] || themes.light;
-
- this.controlPanel.style.background = selectedTheme.background;
- this.controlPanel.style.borderColor = selectedTheme.border;
- this.controlPanel.style.color = selectedTheme.text;
-
- // Update button colors
- const buttons = this.controlPanel.querySelectorAll('button');
- buttons.forEach((button, index) => {
- const colors = Object.values(selectedTheme.buttonColors);
- if (colors[index]) {
- button.style.backgroundColor = colors[index];
- }
- });
-
- this.saveControlPanelPreferences();
- }
-
- saveDocument() {
- try {
- const markdown = this.sectionManager.getDocumentMarkdown();
-
- // Generate intelligent filename
- const filename = this.generateSaveFilename();
-
- // Create a download link
- const blob = new Blob([markdown], { type: 'text/markdown' });
- const url = URL.createObjectURL(blob);
- const a = document.createElement('a');
- a.href = url;
- a.download = filename;
- document.body.appendChild(a);
- a.click();
- document.body.removeChild(a);
- URL.revokeObjectURL(url);
-
- this.showMessage(`Document saved as ${filename}!`, 'success');
- console.log('Document saved:', filename, markdown.length, 'characters');
- } catch (error) {
- this.showMessage('Failed to save document: ' + error.message, 'error');
- console.error('Save failed:', error);
- }
- }
-
- resetAllSections() {
- if (!confirm('Reset all sections to their original content? This will lose all changes and cannot be undone.')) {
- return;
- }
-
- try {
- const sections = this.sectionManager.getAllSections();
- sections.forEach(section => {
- this.sectionManager.resetSection(section.id);
- });
-
- this.showMessage('All sections reset to original content', 'info');
- console.log('All sections reset');
- } catch (error) {
- this.showMessage('Failed to reset sections: ' + error.message, 'error');
- console.error('Reset failed:', error);
- }
- }
-
- showStatus() {
- const sections = this.sectionManager.getSectionStatus();
- const totalSections = sections.length;
- const editedSections = sections.filter(s => s.hasChanges).length;
- const currentlyEditing = sections.filter(s => s.isEditing).length;
-
- const statusHtml = `
- Document Status
- Total Sections: ${totalSections}
- Modified Sections: ${editedSections}
- Currently Editing: ${currentlyEditing}
-
- Section Details:
-
- ${sections.map(s => `
-
- ${s.id} (${s.type})
- - ${s.state}
- ${s.hasChanges ? ' โ๏ธ' : ''}
- ${s.isEditing ? ' ๐๏ธ' : ''}
-
- `).join('')}
-
- `;
-
- this.showModal('Document Status', statusHtml);
- }
-
- showMessage(message, type = 'info', options = {}) {
- // Enhanced professional message system with color-coded positioning
- const {
- position = 'top-center',
- duration = 3000,
- dismissible = true,
- icon = true,
- animation = true
- } = options;
-
- const messageDiv = document.createElement('div');
- messageDiv.className = `markitect-message markitect-message-${type}`;
-
- // Get positioning styles
- const positionStyles = this.getMessagePositionStyles(position);
-
- // Get color scheme for message type
- const colors = this.getMessageColors(type);
-
- // Create icon if enabled
- const iconHtml = icon ? this.getMessageIcon(type) : '';
-
- messageDiv.style.cssText = `
- position: fixed;
- ${positionStyles}
- background: ${colors.background};
- color: ${colors.text};
- border: 1px solid ${colors.border};
- border-left: 4px solid ${colors.accent};
- border-radius: 8px;
- padding: 16px 20px;
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
- font-size: 14px;
- font-weight: 500;
- z-index: 10001;
- box-shadow: 0 8px 24px rgba(0,0,0,0.12), 0 4px 8px rgba(0,0,0,0.08);
- backdrop-filter: blur(8px);
- max-width: 400px;
- min-width: 200px;
- transform: ${animation ? 'translateY(-20px)' : 'none'};
- opacity: ${animation ? '0' : '1'};
- transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
- cursor: ${dismissible ? 'pointer' : 'default'};
- `;
-
- // Set message content with icon
- messageDiv.innerHTML = `
-
- ${iconHtml}
-
- ${message}
-
- ${dismissible ? '
ร
' : ''}
-
- `;
-
- // Add to DOM and handle stacking
- document.body.appendChild(messageDiv);
- this.stackMessages();
-
- // Animation entrance
- if (animation) {
- requestAnimationFrame(() => {
- messageDiv.style.transform = 'translateY(0)';
- messageDiv.style.opacity = '1';
- });
- }
-
- // Auto-dismiss functionality
- const autoRemove = () => {
- if (messageDiv.parentNode) {
- messageDiv.style.transform = 'translateY(-20px)';
- messageDiv.style.opacity = '0';
- setTimeout(() => {
- if (messageDiv.parentNode) {
- messageDiv.parentNode.removeChild(messageDiv);
- this.stackMessages();
- }
- }, 300);
- }
- };
-
- // Manual dismiss functionality
- if (dismissible) {
- messageDiv.addEventListener('click', autoRemove);
- }
-
- // Auto-dismiss timer
- if (duration > 0) {
- setTimeout(autoRemove, duration);
- }
-
- return messageDiv;
- }
-
- /**
- * Get position styles for message positioning
- * @param {string} position - Position identifier
- * @returns {string} CSS positioning styles
- */
- getMessagePositionStyles(position) {
- const positions = {
- 'top-left': 'top: 20px; left: 20px;',
- 'top-center': 'top: 20px; left: 50%; transform: translateX(-50%);',
- 'top-right': 'top: 20px; right: 20px;',
- 'center': 'top: 50%; left: 50%; transform: translate(-50%, -50%);',
- 'bottom-left': 'bottom: 20px; left: 20px;',
- 'bottom-center': 'bottom: 20px; left: 50%; transform: translateX(-50%);',
- 'bottom-right': 'bottom: 20px; right: 20px;'
- };
- return positions[position] || positions['top-center'];
- }
-
- /**
- * Get color scheme for message type
- * @param {string} type - Message type
- * @returns {Object} Color scheme object
- */
- getMessageColors(type) {
- const schemes = {
- success: {
- background: '#d4edda',
- text: '#155724',
- border: '#c3e6cb',
- accent: '#28a745'
- },
- error: {
- background: '#f8d7da',
- text: '#721c24',
- border: '#f5c6cb',
- accent: '#dc3545'
- },
- warning: {
- background: '#fff3cd',
- text: '#856404',
- border: '#ffeaa7',
- accent: '#ffc107'
- },
- info: {
- background: '#d1ecf1',
- text: '#0c5460',
- border: '#bee5eb',
- accent: '#17a2b8'
- },
- debug: {
- background: '#e2e3e5',
- text: '#383d41',
- border: '#d6d8db',
- accent: '#6c757d'
- }
- };
- return schemes[type] || schemes.info;
- }
-
- /**
- * Get icon HTML for message type
- * @param {string} type - Message type
- * @returns {string} Icon HTML
- */
- getMessageIcon(type) {
- const icons = {
- success: 'โ
',
- error: 'โ
',
- warning: 'โ
',
- info: 'โน
',
- debug: '๐
'
- };
- return icons[type] || icons.info;
- }
-
- /**
- * Stack messages to prevent overlap
- */
- stackMessages() {
- const messages = Array.from(document.querySelectorAll('.markitect-message')).filter(el =>
- el.style.display !== 'none' && el.parentNode
- );
-
- // Group messages by position
- const messageGroups = {
- 'top': [],
- 'center': [],
- 'bottom': []
- };
-
- messages.forEach(msg => {
- const styles = msg.style;
- if (styles.top && styles.top !== 'auto' && styles.top !== '') {
- messageGroups.top.push(msg);
- } else if (styles.bottom && styles.bottom !== 'auto' && styles.bottom !== '') {
- messageGroups.bottom.push(msg);
- } else {
- messageGroups.center.push(msg);
- }
- });
-
- // Stack top messages downward
- let topOffset = 20;
- messageGroups.top.forEach(msg => {
- msg.style.top = `${topOffset}px`;
- topOffset += msg.offsetHeight + 12;
- });
-
- // Stack bottom messages upward
- let bottomOffset = 20;
- messageGroups.bottom.forEach(msg => {
- msg.style.bottom = `${bottomOffset}px`;
- bottomOffset += msg.offsetHeight + 12;
- });
- }
-
- showModal(title, content) {
- // Remove existing modal if present
- const existingModal = document.getElementById('markitect-modal');
- if (existingModal) {
- existingModal.remove();
- }
-
- const modalOverlay = document.createElement('div');
- modalOverlay.id = 'markitect-modal';
- modalOverlay.style.cssText = `
- position: fixed;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
- background: rgba(0, 0, 0, 0.5);
- display: flex;
- align-items: center;
- justify-content: center;
- z-index: 10000;
- `;
-
- const modal = document.createElement('div');
- modal.style.cssText = `
- background: white;
- border-radius: 8px;
- padding: 24px;
- max-width: 600px;
- max-height: 80vh;
- overflow-y: auto;
- box-shadow: 0 8px 32px rgba(0,0,0,0.3);
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
- `;
-
- const closeBtn = document.createElement('button');
- closeBtn.textContent = 'ร';
- closeBtn.style.cssText = `
- float: right;
- background: none;
- border: none;
- font-size: 24px;
- cursor: pointer;
- color: #6c757d;
- margin: -8px -8px 0 0;
- `;
-
- const modalContent = document.createElement('div');
- modalContent.innerHTML = `${title} ${content}`;
-
- function closeModal() {
- modalOverlay.remove();
- }
-
- closeBtn.addEventListener('click', closeModal);
- modalOverlay.addEventListener('click', (e) => {
- if (e.target === modalOverlay) closeModal();
- });
-
- modal.appendChild(closeBtn);
- modal.appendChild(modalContent);
- modalOverlay.appendChild(modal);
- document.body.appendChild(modalOverlay);
- }
-
- /**
- * Show comprehensive document status dialog with detailed statistics
- */
- showDocumentStatus() {
- const status = this.sectionManager.getDocumentStatus();
- const eventStats = this.domRenderer.getEventStats();
- const sections = this.sectionManager.getAllSections();
-
- // Calculate additional statistics
- const sectionTypes = {};
- const sectionSizes = { small: 0, medium: 0, large: 0 };
- let totalCharacters = 0;
- let averageLength = 0;
-
- sections.forEach(section => {
- const type = section.type || 'paragraph';
- sectionTypes[type] = (sectionTypes[type] || 0) + 1;
-
- const length = section.currentMarkdown.length;
- totalCharacters += length;
-
- if (length < 100) sectionSizes.small++;
- else if (length < 500) sectionSizes.medium++;
- else sectionSizes.large++;
- });
-
- if (sections.length > 0) {
- averageLength = Math.round(totalCharacters / sections.length);
- }
-
- // Create comprehensive status HTML
- const statusHtml = `
-
-
-
-
-
๐ Document Overview
-
-
Total Sections: ${status.totalSections}
-
Total Characters: ${totalCharacters.toLocaleString()}
-
Average Section Length: ${averageLength} chars
-
Document Status:
-
- ${status.hasUnsavedChanges ? 'โ ๏ธ Has Changes' : 'โ
All Saved'}
-
-
-
-
-
-
-
-
๐ Section States
-
-
Currently Editing: ${status.editingSections} sections
-
Modified (Unsaved): ${status.modifiedSections} sections
-
Saved: ${status.savedSections} sections
-
Original: ${status.totalSections - status.modifiedSections - status.savedSections} sections
-
-
-
-
-
-
-
-
๐ท๏ธ Section Types
-
- ${Object.entries(sectionTypes).map(([type, count]) =>
- `
${type.charAt(0).toUpperCase() + type.slice(1)}: ${count}
`
- ).join('')}
-
-
-
-
-
-
๐ Section Sizes
-
-
Small (<100 chars): ${sectionSizes.small}
-
Medium (100-500 chars): ${sectionSizes.medium}
-
Large (>500 chars): ${sectionSizes.large}
-
-
-
-
-
-
-
โก Event Statistics
-
-
Total Events: ${eventStats.totalEvents}
-
Section Clicks: ${eventStats.stats['section-click'] || 0}
-
Hover Events: ${(eventStats.stats['section-hover-enter'] || 0) + (eventStats.stats['section-hover-leave'] || 0)}
-
Keyboard Shortcuts: ${eventStats.stats['keyboard-shortcut'] || 0}
-
Context Menus: ${eventStats.stats['section-context-menu'] || 0}
-
Drag Events: ${(eventStats.stats['section-drag-start'] || 0) + (eventStats.stats['section-drag-over'] || 0)}
-
-
-
-
- ${eventStats.recentEvents.length > 0 ? `
-
-
๐ Recent Activity (Last 10 Events)
-
- ${eventStats.recentEvents.slice(-10).reverse().map(event => `
-
- ${event.type} - ${event.timestamp}
- ${event.data.sectionId ? `Section: ${event.data.sectionId.substring(0, 12)}... ` : ''}
- ${event.data.shortcut ? `Shortcut: ${event.data.shortcut} ` : ''}
-
- `).join('')}
-
-
- ` : ''}
-
-
-
-
๐ Section Details
-
-
-
-
- Section
- Type
- State
- Length
- Changes
-
-
-
- ${sections.map((section, index) => {
- const hasChanges = section.editingMarkdown !== section.currentMarkdown;
- const stateColor = section.state === 'editing' ? '#3498db' :
- section.state === 'modified' ? '#f39c12' :
- section.state === 'saved' ? '#27ae60' : '#95a5a6';
- const preview = section.currentMarkdown.substring(0, 40).replace(/\n/g, ' ') +
- (section.currentMarkdown.length > 40 ? '...' : '');
-
- return `
-
-
- ${preview}
-
-
-
- ${section.type || 'paragraph'}
-
-
-
-
- ${section.state || 'original'}
-
-
- ${section.currentMarkdown.length} chars
-
- ${hasChanges ? 'โ' : 'โ'}
-
-
- `;
- }).join('')}
-
-
-
-
-
- `;
-
- this.showModal('๐ Comprehensive Document Status', statusHtml);
- }
-
- /**
- * Toggle the debug panel on/off
- */
- toggleDebugPanel() {
- console.log('toggleDebugPanel called');
- const debugContainer = document.getElementById('debug-messages-container');
- const debugButton = document.getElementById('toggle-debug');
-
- console.log('Elements found:', {
- debugContainer: !!debugContainer,
- debugButton: !!debugButton
- });
-
- if (!debugContainer || !debugButton) return;
-
- if (debugPanelActive) {
- // Hide debug panel
- console.log('Hiding debug panel');
- debugContainer.style.display = 'none';
- debugButton.textContent = '๐ Debug';
- debugButton.style.background = '#6c757d';
- debugPanelActive = false;
- } else {
- // Show debug panel
- console.log('Showing debug panel');
- debugContainer.style.display = 'block';
- debugButton.textContent = '๐ Debug (ON)';
- debugButton.style.background = '#28a745';
- debugPanelActive = true;
- console.log('About to call this.updateDebugPanel()');
- this.updateDebugPanel();
- }
- console.log('debugPanelActive is now:', debugPanelActive);
- }
-
- /**
- * Update the debug panel with current messages
- */
- updateDebugPanel() {
- const debugContainer = document.getElementById('debug-messages-container');
- if (!debugContainer || !debugPanelActive) {
- console.log('updateDebugPanel skipped:', {
- hasContainer: !!debugContainer,
- debugPanelActive
- });
- return;
- }
-
- console.log('updateDebugPanel running, messages count:', debugMessages.length);
-
- if (debugMessages.length === 0) {
- debugContainer.innerHTML = 'No debug messages yet. Click sections to generate debug output.
';
- return;
- }
-
- // Show the last 50 messages in reverse order (newest first)
- const recentMessages = debugMessages.slice(-50).reverse();
-
- const messagesHtml = recentMessages.map(msg => {
- const categoryColor = {
- 'INFO': '#17a2b8',
- 'WARNING': '#ffc107',
- 'ERROR': '#dc3545',
- 'SUCCESS': '#28a745',
- 'DEBUG': '#6f42c1'
- }[msg.category] || '#6c757d';
-
- return `
-
- [${msg.timestamp}]
- ${msg.category}:
- ${msg.message}
-
- `;
- }).join('');
-
- debugContainer.innerHTML = `
-
- Debug Messages (${debugMessages.length} total, showing last ${recentMessages.length})
- Clear
-
-
- ${messagesHtml}
-
- `;
-
- // Add event listener for clear button
- const clearBtn = debugContainer.querySelector('#debug-clear-btn');
- if (clearBtn) {
- clearBtn.addEventListener('click', () => {
- console.log('Clear button clicked'); // Debug log
- window.clearDebugMessages();
- });
- }
-
- // Auto-scroll to bottom to show newest messages
- const scrollContainer = debugContainer.querySelector('div[style*="overflow-y"]');
- if (scrollContainer) {
- scrollContainer.scrollTop = scrollContainer.scrollHeight;
- }
- }
-
- /**
- * Initialize the debug system for this page load
- */
- initializeDebugSystem() {
- // Clear any debug messages from rendering time
- debugMessages = [];
- debugPanelActive = false;
-
- console.log('Debug system initialized - messages cleared for new session');
-
- // Add a welcome message to show the system is working
- debug('initializeDebugSystem: Debug system ready for new session', 'INFO');
- }
-
- getDocumentMarkdown() {
- return this.sectionManager.getDocumentMarkdown();
- }
-
- escapeRegex(str) {
- return str.replace(/[.*+?^${}()|[]\]/g, '\\\\$&');
- }
-
- convertDataUrlToReference(markdown) {
- if (!window.markitectBase64References) {
- return markdown;
- }
-
- let convertedMarkdown = markdown;
-
- Object.entries(window.markitectBase64References).forEach(([refId, refData]) => {
- const dataUrl = refData.full_data_url;
- const escapedDataUrl = this.escapeRegex(dataUrl);
- const dataUrlPattern = new RegExp(`!\\[([^\\]]*)\\]\\(${escapedDataUrl}\\)`, 'g');
- convertedMarkdown = convertedMarkdown.replace(dataUrlPattern, `![$1][${refId}]`);
- });
-
- return convertedMarkdown;
- }
-
- /**
- * Setup real-time status tracking
- */
- setupStatusTracking() {
- // Listen for status updates from SectionManager
- this.sectionManager.on('status-updated', (status) => {
- this.domRenderer.updateStatusDisplay(status);
- });
-
- // Start periodic status tracking
- this.sectionManager.startStatusTracking(2000); // Update every 2 seconds
-
- // Initial status display
- this.sectionManager.updateGlobalStatus();
-
- console.log('โ Real-time status tracking initialized');
- }
-
- /**
- * Generate intelligent save filename using 4-method fallback system
- * @returns {string} Generated filename with .md extension
- */
- generateSaveFilename() {
- // Method 1: Original filename from options
- if (this.options.originalFilename) {
- return this.sanitizeFilename(this.options.originalFilename);
- }
-
- // Method 2: Page title extraction
- const titleFilename = this.extractFilenameFromTitle();
- if (titleFilename) {
- return this.sanitizeFilename(titleFilename + '.md');
- }
-
- // Method 3: URL pathname analysis
- const urlFilename = this.extractFilenameFromUrl();
- if (urlFilename) {
- return this.sanitizeFilename(urlFilename + '.md');
- }
-
- // Method 4: First heading extraction
- const headingFilename = this.extractFilenameFromHeading();
- if (headingFilename) {
- return this.sanitizeFilename(headingFilename + '.md');
- }
-
- // Method 5: Timestamp generation (fallback)
- return this.generateTimestampFilename();
- }
-
- /**
- * Sanitize filename to be filesystem-safe
- * @param {string} filename - Raw filename
- * @returns {string} Sanitized filename
- */
- sanitizeFilename(filename) {
- if (!filename) return 'document.md';
-
- // Remove or replace filesystem-unsafe characters
- let sanitized = filename
- .replace(/[\/\\:*?"<>|]/g, '-') // Replace unsafe chars with dashes
- .replace(/\s+/g, '-') // Replace spaces with dashes
- .replace(/-+/g, '-') // Replace multiple dashes with single dash
- .replace(/^-|-$/g, '') // Remove leading/trailing dashes
- .trim();
-
- // Ensure it ends with .md
- if (!sanitized.endsWith('.md')) {
- sanitized += '.md';
- }
-
- // Ensure it's not empty
- if (sanitized === '.md') {
- sanitized = 'document.md';
- }
-
- return sanitized;
- }
-
- /**
- * Extract filename from page title
- * @returns {string|null} Filename or null if not suitable
- */
- extractFilenameFromTitle() {
- if (typeof document === 'undefined' || !document.title) {
- return null;
- }
-
- let title = document.title.trim();
-
- // Remove common website suffixes
- title = title
- .split('|')[0] // Remove " | Website"
- .split('-')[0] // Remove " - Website"
- .split('โข')[0] // Remove " โข Website"
- .trim();
-
- if (title.length < 3 || title.length > 100) {
- return null;
- }
-
- return title;
- }
-
- /**
- * Extract filename from URL pathname
- * @returns {string|null} Filename or null if not suitable
- */
- extractFilenameFromUrl() {
- if (typeof window === 'undefined' || !window.location) {
- return null;
- }
-
- const pathname = window.location.pathname;
- if (!pathname || pathname === '/') {
- return null;
- }
-
- // Get the last segment of the path
- const segments = pathname.split('/').filter(s => s.length > 0);
- if (segments.length === 0) {
- return null;
- }
-
- const lastSegment = segments[segments.length - 1];
-
- // Remove file extensions
- const filenameBase = lastSegment.replace(/\.[^.]*$/, '');
-
- if (filenameBase.length < 3 || filenameBase.length > 100) {
- return null;
- }
-
- return filenameBase;
- }
-
- /**
- * Extract filename from first heading in markdown
- * @returns {string|null} Filename or null if no heading found
- */
- extractFilenameFromHeading() {
- if (!this.originalMarkdown) {
- return null;
- }
-
- const lines = this.originalMarkdown.split('\n');
-
- // Find first heading line
- for (const line of lines) {
- const trimmed = line.trim();
- if (/^#{1,6}\s/.test(trimmed)) {
- // Extract heading text (remove # symbols and trim)
- const headingText = trimmed.replace(/^#{1,6}\s*/, '').trim();
-
- if (headingText.length >= 3 && headingText.length <= 100) {
- return headingText;
- }
- }
- }
-
- return null;
- }
-
- /**
- * Generate timestamp-based filename as final fallback
- * @returns {string} Timestamp-based filename
- */
- generateTimestampFilename() {
- const now = new Date();
- const timestamp = now.getFullYear().toString() +
- (now.getMonth() + 1).toString().padStart(2, '0') +
- now.getDate().toString().padStart(2, '0') + '-' +
- now.getHours().toString().padStart(2, '0') +
- now.getMinutes().toString().padStart(2, '0');
-
- return `document-${timestamp}.md`;
- }
-
- /**
- * Cleanup method to stop status tracking
- */
- destroy() {
- if (this.sectionManager) {
- this.sectionManager.stopStatusTracking();
- }
- }
-}
-
-// Initialize the clean editor system
-let markitectCleanEditor;
-
-function initializeCleanEditor() {
- debug('1: initializeCleanEditor called', 'INIT');
-
- const container = document.getElementById('markdown-content');
- if (!container) {
- debug('2: FAILED - Markdown content container not found', 'ERROR');
- return;
- }
- debug('3: Container found', 'INIT');
-
- if (typeof window.MarkitectEditor === 'undefined') {
- debug('4: FAILED - MarkitectEditor not found', 'ERROR');
- return;
- }
- debug('5: MarkitectEditor found', 'INIT');
-
- debug('6: Creating editor with content length: ' + markdownContentWithDogtag.length, 'INIT');
-
- try {
- markitectCleanEditor = new window.MarkitectEditor.MarkitectCleanEditor(markdownContentWithDogtag, container, {
- cleanMarkdownContent: markdownContent,
- dogtagContent: dogtagContent
- });
- debug('7: Editor instance created', 'INIT');
-
- window.markitectCleanEditor = markitectCleanEditor; // Make globally available
- debug('8: Editor made globally available', 'INIT');
-
- const contentAfterInit = container.innerHTML;
- debug('9: Container has content: ' + (contentAfterInit.length > 0 ? 'YES (' + contentAfterInit.length + ' chars)' : 'NO'), 'INIT');
-
- console.log('โ
Clean section editor initialized successfully');
- } catch (error) {
- debug('ERROR in editor creation: ' + error.message, 'ERROR');
- throw error;
- }
-}
-
-// Document scroll indicators
-function initializeScrollIndicators() {
- console.log('โ
Document scroll indicators initialized');
-}
-
-// Export for module systems
-if (typeof module !== 'undefined' && module.exports) {
- module.exports = { EditState, SectionType, Section, SectionManager, DOMRenderer, MarkitectCleanEditor };
- global.EditState = EditState;
- global.SectionType = SectionType;
- global.Section = Section;
-} else {
- window.MarkitectEditor = { EditState, SectionType, Section, SectionManager, DOMRenderer, MarkitectCleanEditor };
- window.EditState = EditState;
- window.SectionType = SectionType;
- window.Section = Section;
-}
\ No newline at end of file
diff --git a/markitect/static/js/components/debug-panel.js b/markitect/static/js/components/debug-panel.js
deleted file mode 100644
index d22706a0..00000000
--- a/markitect/static/js/components/debug-panel.js
+++ /dev/null
@@ -1,191 +0,0 @@
-/**
- * DebugPanel Component
- *
- * Extracted from monolithic editor.js as part of architecture refactoring.
- * Handles debug message display and management for client-side debugging.
- *
- * Dependencies:
- * - None (standalone component)
- */
-
-/**
- * DebugPanel - Manages debug message display and interaction
- */
-class DebugPanel {
- constructor() {
- this.messages = [];
- this.isActive = false;
- this.maxMessages = 1000; // Keep last 1000 messages
- }
-
- /**
- * Add a debug message
- */
- addMessage(message, category = 'INFO') {
- const messageObj = {
- message,
- category,
- timestamp: new Date().toLocaleTimeString()
- };
-
- this.messages.push(messageObj);
-
- // Keep only last maxMessages
- if (this.messages.length > this.maxMessages) {
- this.messages = this.messages.slice(-this.maxMessages);
- }
-
- // Auto-update if panel is visible
- if (this.isActive) {
- this.update();
- }
- }
-
- /**
- * Toggle the debug panel on/off
- */
- toggle() {
- const debugContainer = document.getElementById('debug-messages-container');
- const debugButton = document.getElementById('toggle-debug');
-
- if (!debugContainer || !debugButton) {
- console.warn('DebugPanel: Required DOM elements not found');
- return;
- }
-
- if (this.isActive) {
- this.hide();
- } else {
- this.show();
- }
- }
-
- /**
- * Show the debug panel
- */
- show() {
- const debugContainer = document.getElementById('debug-messages-container');
- const debugButton = document.getElementById('toggle-debug');
-
- if (!debugContainer || !debugButton) {
- console.warn('DebugPanel: Required DOM elements not found');
- return;
- }
-
- debugContainer.style.display = 'block';
- debugButton.textContent = '๐ Debug (ON)';
- debugButton.style.background = '#28a745';
- this.isActive = true;
- this.update();
- }
-
- /**
- * Hide the debug panel
- */
- hide() {
- const debugContainer = document.getElementById('debug-messages-container');
- const debugButton = document.getElementById('toggle-debug');
-
- if (!debugContainer || !debugButton) {
- console.warn('DebugPanel: Required DOM elements not found');
- return;
- }
-
- debugContainer.style.display = 'none';
- debugButton.textContent = '๐ Debug';
- debugButton.style.background = '#6c757d';
- this.isActive = false;
- }
-
- /**
- * Update the debug panel with current messages
- */
- update() {
- const debugContainer = document.getElementById('debug-messages-container');
- if (!debugContainer || !this.isActive) {
- return;
- }
-
- if (this.messages.length === 0) {
- debugContainer.innerHTML = 'No debug messages yet. Click sections to generate debug output.
';
- return;
- }
-
- // Show the last 50 messages in reverse order (newest first)
- const recentMessages = this.messages.slice(-50).reverse();
-
- const messagesHtml = recentMessages.map(msg => {
- const categoryColor = {
- 'INFO': '#17a2b8',
- 'WARNING': '#ffc107',
- 'ERROR': '#dc3545',
- 'SUCCESS': '#28a745',
- 'DEBUG': '#6f42c1'
- }[msg.category] || '#6c757d';
-
- return `
-
- [${msg.timestamp}]
- ${msg.category}:
- ${msg.message}
-
- `;
- }).join('');
-
- debugContainer.innerHTML = `
-
- Debug Messages (${this.messages.length} total, showing last ${recentMessages.length})
- Clear
-
-
- ${messagesHtml}
-
- `;
-
- // Add event listener for clear button
- const clearBtn = debugContainer.querySelector('#debug-clear-btn');
- if (clearBtn) {
- clearBtn.addEventListener('click', () => {
- this.clear();
- });
- }
-
- // Auto-scroll to bottom to show newest messages
- const scrollContainer = debugContainer.querySelector('div[style*="overflow-y"]');
- if (scrollContainer) {
- scrollContainer.scrollTop = scrollContainer.scrollHeight;
- }
- }
-
- /**
- * Clear all debug messages
- */
- clear() {
- this.messages = [];
- this.update();
- }
-
- /**
- * Get the number of messages
- */
- getMessageCount() {
- return this.messages.length;
- }
-
- /**
- * Get recent messages
- */
- getRecentMessages(count = 10) {
- return this.messages.slice(-count);
- }
-}
-
-// Export for use in tests and other modules
-if (typeof module !== 'undefined' && module.exports) {
- module.exports = { DebugPanel };
-}
-
-// Export for browser use
-if (typeof window !== 'undefined') {
- window.DebugPanel = DebugPanel;
-}
\ No newline at end of file
diff --git a/markitect/static/js/components/document-controls.js b/markitect/static/js/components/document-controls.js
deleted file mode 100644
index fb83ebd8..00000000
--- a/markitect/static/js/components/document-controls.js
+++ /dev/null
@@ -1,279 +0,0 @@
-/**
- * DocumentControls Component
- *
- * Extracted from monolithic editor.js as part of architecture refactoring.
- * Handles the floating control panel and document-level actions.
- *
- * Dependencies:
- * - None (standalone component)
- */
-
-/**
- * DocumentControls - Manages the floating control panel and its buttons
- */
-class DocumentControls {
- constructor() {
- this.controlPanel = null;
- this.buttons = new Map();
- this.eventHandlers = new Map();
- this.isVisible = true;
- }
-
- /**
- * Create the control panel and add it to the DOM
- */
- create() {
- if (this.controlPanel) {
- this.destroy(); // Remove existing panel
- }
-
- // Also remove any existing panel with the same ID in the DOM
- const existingPanel = document.getElementById('markitect-global-controls');
- if (existingPanel && existingPanel.parentNode) {
- existingPanel.parentNode.removeChild(existingPanel);
- }
-
- // Create the floating control panel
- this.controlPanel = document.createElement('div');
- this.controlPanel.id = 'markitect-global-controls';
- this.controlPanel.style.cssText = `
- position: fixed;
- top: 20px;
- right: 20px;
- background: rgba(248, 249, 250, 0.95);
- border: 1px solid #dee2e6;
- border-radius: 8px;
- padding: 12px;
- box-shadow: 0 4px 12px rgba(0,0,0,0.15);
- z-index: 1000;
- backdrop-filter: blur(8px);
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
- font-size: 14px;
- min-width: 200px;
- `;
-
- // Add title
- const title = document.createElement('div');
- title.style.cssText = `
- font-weight: 600;
- margin-bottom: 8px;
- color: #495057;
- border-bottom: 1px solid #dee2e6;
- padding-bottom: 4px;
- `;
- title.textContent = 'Document Controls';
-
- // Create button container
- const buttonContainer = document.createElement('div');
- buttonContainer.id = 'button-container';
- buttonContainer.style.cssText = `
- display: flex;
- flex-direction: column;
- gap: 6px;
- `;
-
- this.controlPanel.appendChild(title);
- this.controlPanel.appendChild(buttonContainer);
-
- // Add default buttons
- this.addDefaultButtons();
-
- // Add debug messages container
- this.addDebugContainer();
-
- // Add to DOM
- document.body.appendChild(this.controlPanel);
- }
-
- /**
- * Add default buttons to the control panel
- */
- addDefaultButtons() {
- // Save Document button
- this.addButton('save-document', '๐พ Save Document', '#28a745');
-
- // Reset All button
- this.addButton('reset-all', '๐ Reset All', '#ffc107', '#212529');
-
- // Show Status button
- this.addButton('show-status', '๐ Show Status', '#17a2b8');
-
- // Debug button
- this.addButton('toggle-debug', '๐ Debug', '#6c757d');
- }
-
- /**
- * Add debug container to the control panel
- */
- addDebugContainer() {
- const debugContainer = document.createElement('div');
- debugContainer.id = 'debug-messages-container';
- debugContainer.style.cssText = `
- margin-top: 12px;
- max-height: 300px;
- overflow-y: auto;
- border: 1px solid #dee2e6;
- border-radius: 4px;
- background: #f8f9fa;
- padding: 8px;
- font-family: 'Courier New', monospace;
- font-size: 12px;
- line-height: 1.4;
- display: none;
- `;
-
- this.controlPanel.appendChild(debugContainer);
- }
-
- /**
- * Add a button to the control panel
- */
- addButton(id, text, backgroundColor, textColor = 'white') {
- const buttonContainer = this.controlPanel.querySelector('#button-container');
- if (!buttonContainer) {
- throw new Error('Button container not found. Call create() first.');
- }
-
- const button = document.createElement('button');
- button.id = id;
- button.textContent = text;
- button.style.cssText = `
- background: ${backgroundColor};
- color: ${textColor};
- border: none;
- padding: 8px 12px;
- border-radius: 4px;
- cursor: pointer;
- font-size: 13px;
- font-weight: 500;
- transition: background-color 0.2s;
- `;
-
- buttonContainer.appendChild(button);
- this.buttons.set(id, button);
-
- return button;
- }
-
- /**
- * Remove a button from the control panel
- */
- removeButton(id) {
- const button = this.buttons.get(id);
- if (button && button.parentNode) {
- button.parentNode.removeChild(button);
- this.buttons.delete(id);
- this.eventHandlers.delete(id);
- }
- }
-
- /**
- * Set event handlers for buttons
- */
- setEventHandlers(handlers) {
- for (const [buttonId, handler] of Object.entries(handlers)) {
- const button = this.buttons.get(buttonId);
- if (button) {
- // Remove existing handler if any
- if (this.eventHandlers.has(buttonId)) {
- button.removeEventListener('click', this.eventHandlers.get(buttonId));
- }
-
- // Add new handler
- button.addEventListener('click', handler);
- this.eventHandlers.set(buttonId, handler);
- }
- }
- }
-
- /**
- * Show the control panel
- */
- show() {
- if (this.controlPanel) {
- this.controlPanel.style.display = 'block';
- this.isVisible = true;
- }
- }
-
- /**
- * Hide the control panel
- */
- hide() {
- if (this.controlPanel) {
- this.controlPanel.style.display = 'none';
- this.isVisible = false;
- }
- }
-
- /**
- * Update status display (can be extended as needed)
- */
- updateStatus(status) {
- // This method can be extended to show status information
- // For now, it just stores the status for potential display
- this.lastStatus = status;
-
- // Could update a status indicator in the panel if needed
- if (status && this.controlPanel) {
- const title = this.controlPanel.querySelector('div');
- if (title) {
- const statusText = `Document Controls (${status.totalSections} sections, ${status.editingSections} editing)`;
- // Could update title or add status indicator
- }
- }
- }
-
- /**
- * Get the control panel element
- */
- getControlPanel() {
- return this.controlPanel;
- }
-
- /**
- * Destroy the control panel and clean up
- */
- destroy() {
- if (this.controlPanel && this.controlPanel.parentNode) {
- this.controlPanel.parentNode.removeChild(this.controlPanel);
- }
-
- // Clean up references
- this.controlPanel = null;
- this.buttons.clear();
- this.eventHandlers.clear();
- this.isVisible = true;
- }
-
- /**
- * Check if the control panel is visible
- */
- isVisible() {
- return this.isVisible && this.controlPanel && this.controlPanel.style.display !== 'none';
- }
-
- /**
- * Get all button IDs
- */
- getButtonIds() {
- return Array.from(this.buttons.keys());
- }
-
- /**
- * Get a specific button by ID
- */
- getButton(id) {
- return this.buttons.get(id);
- }
-}
-
-// Export for use in tests and other modules
-if (typeof module !== 'undefined' && module.exports) {
- module.exports = { DocumentControls };
-}
-
-// Export for browser use
-if (typeof window !== 'undefined') {
- window.DocumentControls = DocumentControls;
-}
\ No newline at end of file
diff --git a/markitect/static/js/components/dom-renderer.js b/markitect/static/js/components/dom-renderer.js
deleted file mode 100644
index 20748483..00000000
--- a/markitect/static/js/components/dom-renderer.js
+++ /dev/null
@@ -1,1128 +0,0 @@
-/**
- * DOMRenderer Component
- *
- * Extracted from monolithic editor.js as part of architecture refactoring.
- * Handles all DOM interactions and UI rendering for section editing.
- *
- * Dependencies:
- * - FloatingMenu component (to be extracted)
- * - debug function (imported from utils)
- */
-
-// Import dependencies (placeholders for now)
-function debug(message, category = 'INFO') {
- console.log(`DEBUG ${category}: ${message}`);
-}
-
-/**
- * Simple FloatingMenu implementation (will be extracted to separate component later)
- */
-class FloatingMenu {
- constructor(sectionId, type, renderer) {
- this.sectionId = sectionId;
- this.type = type;
- this.renderer = renderer;
- this.element = null;
- this.isVisible = false;
- }
-
- show(contentElement, controlsElement) {
- if (this.isVisible) this.hide();
-
- const targetElement = this.renderer.findSectionElement(this.sectionId);
- if (!targetElement) return null;
-
- // Get content dimensions and position
- const rect = targetElement.getBoundingClientRect();
- const viewport = {
- width: window.innerWidth,
- height: window.innerHeight
- };
-
- // Calculate content width and responsive extension
- const contentWidth = rect.width;
- const buttonAreaWidth = 120; // Space needed for buttons
- const minMenuWidth = Math.max(300, contentWidth); // At least content width or 300px
- const preferredMenuWidth = contentWidth + buttonAreaWidth;
-
- // Check if we have space to extend to the right
- const spaceOnRight = viewport.width - rect.right;
- const canExtendRight = spaceOnRight >= buttonAreaWidth + 20; // 20px margin
-
- // Determine final menu width
- let menuWidth;
- if (canExtendRight && viewport.width >= 800) { // Only on wide screens
- menuWidth = Math.min(preferredMenuWidth, viewport.width - rect.left - 20);
- } else {
- menuWidth = Math.min(minMenuWidth, viewport.width - 40); // 20px margins
- }
-
- // Create floating menu element
- this.element = document.createElement('div');
- this.element.className = 'ui-edit-floating-menu';
- this.element.style.cssText = `
- position: fixed;
- z-index: 10000;
- background: white;
- border: 1px solid #ddd;
- border-radius: 8px;
- box-shadow: 0 4px 12px rgba(0,0,0,0.15);
- padding: 0;
- width: ${menuWidth}px;
- box-sizing: border-box;
- `;
-
- // Add headline
- const headline = document.createElement('div');
- headline.className = 'ui-edit-headline';
- headline.textContent = `Editing ${this.type === 'image' ? 'Image' : 'Section'}`;
- headline.style.cssText = `
- background: #f8f9fa;
- border-bottom: 1px solid #ddd;
- padding: 8px 16px;
- font-weight: 600;
- font-size: 12px;
- color: #495057;
- border-radius: 8px 8px 0 0;
- text-transform: uppercase;
- letter-spacing: 0.5px;
- `;
-
- // Create content wrapper with padding
- const contentWrapper = document.createElement('div');
- contentWrapper.style.cssText = `
- padding: 16px;
- `;
-
- this.element.appendChild(headline);
-
- // Position directly over content (overlay positioning)
- let left = rect.left;
- let top = rect.top;
-
- // Ensure menu doesn't go off-screen horizontally
- if (left + menuWidth > viewport.width) {
- left = viewport.width - menuWidth - 20;
- }
- if (left < 10) {
- left = 10;
- }
-
- // For vertical positioning, prefer staying on top of content
- // Only move if absolutely necessary
- const menuHeight = this.type === 'image' ? 350 : 200; // Better height estimates
- const wouldGoOffBottom = top + menuHeight > viewport.height;
- const wouldGoOffTop = top < 10;
-
- if (wouldGoOffBottom && !wouldGoOffTop) {
- // Try to fit by moving up, but keep some overlay if possible
- const maxTop = viewport.height - menuHeight - 10;
- top = Math.max(rect.top - 50, maxTop); // Prefer staying near original position
- } else if (wouldGoOffTop) {
- top = 10; // Minimum distance from top
- }
- // Otherwise, keep the original overlay position
-
- this.element.style.left = `${left}px`;
- this.element.style.top = `${top}px`;
-
- // Add content to wrapper
- if (contentElement) {
- contentWrapper.appendChild(contentElement);
- }
- if (controlsElement) {
- contentWrapper.appendChild(controlsElement);
- }
-
- this.element.appendChild(contentWrapper);
-
- // Add close button to headline
- const closeButton = document.createElement('button');
- closeButton.textContent = 'ร';
- closeButton.style.cssText = `
- position: absolute;
- top: 4px;
- right: 8px;
- background: none;
- border: none;
- font-size: 18px;
- cursor: pointer;
- color: #666;
- width: 24px;
- height: 24px;
- display: flex;
- align-items: center;
- justify-content: center;
- border-radius: 4px;
- transition: background-color 0.2s ease;
- `;
- closeButton.addEventListener('mouseover', () => {
- closeButton.style.backgroundColor = '#e9ecef';
- });
- closeButton.addEventListener('mouseout', () => {
- closeButton.style.backgroundColor = 'transparent';
- });
- closeButton.addEventListener('click', (event) => {
- event.stopPropagation();
- this.hide();
- });
- this.element.appendChild(closeButton);
-
- document.body.appendChild(this.element);
- this.isVisible = true;
-
- return this.element;
- }
-
- hide() {
- if (this.element && this.element.parentNode) {
- this.element.parentNode.removeChild(this.element);
- }
- this.element = null;
- this.isVisible = false;
-
- // Stop editing state in the section manager
- const section = this.renderer.sectionManager.sections.get(this.sectionId);
- if (section && section.isEditing()) {
- section.stopEditing();
- }
-
- // Remove from editing sections
- this.renderer.editingSections.delete(this.sectionId);
- }
-}
-
-/**
- * DOMRenderer - Handles DOM interactions and section rendering
- */
-class DOMRenderer {
- constructor(sectionManager, container) {
- this.sectionManager = sectionManager;
- this.container = container;
- this.editingSections = new Set();
- this.currentFloatingMenu = null;
- this.eventListenersAttached = false;
- this.lastClickTime = 0;
- this.clickDebounceMs = 300; // Prevent rapid clicks
-
- // Enhanced Event System - Track event types
- this.eventHistory = [];
- this.eventStats = {
- 'section-click': 0,
- 'section-hover-enter': 0,
- 'section-hover-leave': 0,
- 'keyboard-shortcut': 0,
- 'section-drag-start': 0,
- 'section-drag-over': 0,
- 'section-drop': 0,
- 'section-focus-in': 0,
- 'section-focus-out': 0,
- 'section-context-menu': 0
- };
-
- // Bind event handlers
- this.handleSectionClick = this.handleSectionClick.bind(this);
- this.handleKeydown = this.handleKeydown.bind(this);
-
- this.setupEventListeners();
- }
-
- setupEventListeners() {
- this.sectionManager.on('sections-created', (data) => {
- this.renderAllSections(data.sections);
- });
- this.sectionManager.on('edit-started', (data) => {
- debug('EVENT: edit-started event received for: ' + data.sectionId, 'EVENT');
- this.showEditor(data.sectionId, data.content);
- });
- }
-
- /**
- * Render all sections to the DOM
- */
- renderAllSections(sections) {
- debug('21: renderAllSections called with ' + sections.length + ' sections', 'RENDER');
-
- // Clear container
- this.container.innerHTML = '';
- debug('22: Container cleared', 'RENDER');
-
- const contentArea = this.container.querySelector('#markdown-content') || this.container;
-
- // Render each section
- sections.forEach((section, index) => {
- debug('23: Creating element for section ' + (index + 1) + ': ' + section.id, 'RENDER');
- const element = this.renderSection(section);
- if (element) {
- contentArea.appendChild(element);
- }
- });
-
- debug('24: All section elements added to container', 'RENDER');
-
- // Attach event listeners only once
- if (!this.eventListenersAttached) {
- this.container.addEventListener('click', this.handleSectionClick);
- this.eventListenersAttached = true;
- debug('25: Enhanced event listeners attached for the first time', 'RENDER');
- } else {
- debug('25: Event listeners already attached, skipping', 'RENDER');
- }
-
- debug('25: Container content length: ' + this.container.innerHTML.length, 'RENDER');
- }
-
- /**
- * Render a single section to DOM element
- */
- renderSection(section) {
- const element = document.createElement('div');
- element.className = 'ui-edit-section';
- element.setAttribute('data-section-id', section.id);
-
- // Add section content
- // Render all sections using markdown rendering (images need HTML conversion too)
- const content = this.simpleMarkdownRender(section.currentMarkdown);
- element.innerHTML = content;
-
- // Add styling
- element.style.cssText = `
- margin: 16px 0;
- padding: 12px;
- border: 1px solid transparent;
- border-radius: 4px;
- cursor: pointer;
- transition: all 0.2s ease;
- `;
-
- element.addEventListener('mouseenter', () => {
- element.style.backgroundColor = 'rgba(0, 122, 204, 0.05)';
- element.style.borderColor = 'rgba(0, 122, 204, 0.2)';
- });
-
- element.addEventListener('mouseleave', () => {
- if (!section.isEditing()) {
- element.style.backgroundColor = 'transparent';
- element.style.borderColor = 'transparent';
- }
- });
-
- debug('SECTION: Section element setup complete for ' + section.id, 'SECTION');
- return element;
- }
-
- /**
- * Simple markdown rendering (placeholder)
- */
- simpleMarkdownRender(markdown) {
- return markdown
- .replace(/^# (.*$)/gim, '$1 ')
- .replace(/^## (.*$)/gim, '$1 ')
- .replace(/^### (.*$)/gim, '$1 ')
- .replace(/!\[(.*?)\]\((.*?)\)/gim, ' ')
- .replace(/\[([^\]]+)\]\(([^)]+)\)/gim, ' $1 ')
- .replace(/\*\*(.*?)\*\*/gim, '$1 ')
- .replace(/\*(.*?)\*/gim, '$1 ')
- .replace(/\n/gim, ' ');
- }
-
- /**
- * Find DOM element for a section
- */
- findSectionElement(sectionId) {
- return this.container.querySelector(`[data-section-id="${sectionId}"]`);
- }
-
- /**
- * Handle section click events
- */
- handleSectionClick(event) {
- debug('handleSectionClick: Click detected on target: ' + event.target.tagName + ' ' + (event.target.className || ''), 'CLICK');
-
- // Debounce rapid clicks
- const now = Date.now();
- if (now - this.lastClickTime < this.clickDebounceMs) {
- debug('handleSectionClick: Click debounced (too rapid)', 'CLICK');
- return;
- }
- this.lastClickTime = now;
-
- // Don't handle clicks on form elements, buttons, or links
- if (event.target.closest('textarea, button, input, a')) {
- debug('handleSectionClick: Ignoring click on form element', 'CLICK');
- return;
- }
-
- const sectionElement = event.target.closest('.ui-edit-section');
- debug('handleSectionClick: Found section element: ' + (sectionElement ? sectionElement.getAttribute('data-section-id') : 'null'), 'CLICK');
- if (!sectionElement) return;
-
- const sectionId = sectionElement.getAttribute('data-section-id');
- debug('handleSectionClick: Section ID: ' + sectionId, 'CLICK');
- if (!sectionId) return;
-
- // Track the click event
- this.trackEvent('section-click', {
- sectionId,
- event,
- timestamp: Date.now()
- });
-
- // Check if this section is already being edited
- const section = this.sectionManager.sections.get(sectionId);
- debug('handleSectionClick: Found section object: ' + (section ? 'YES' : 'NO'), 'CLICK');
-
- if (section && section.isEditing()) {
- debug('handleSectionClick: Section already being edited, checking if dialog is visible: ' + sectionId, 'CLICK');
- // If section is editing but no dialog is visible, allow re-opening
- const existingDialog = document.querySelector('.ui-edit-floating-menu');
- if (existingDialog) {
- debug('handleSectionClick: Dialog already visible, ignoring click', 'CLICK');
- return;
- } else {
- debug('handleSectionClick: Section editing but no dialog visible, proceeding to show editor', 'CLICK');
- }
- }
-
- debug('handleSectionClick: About to start editing for section: ' + sectionId, 'CLICK');
-
- try {
- debug('handleSectionClick: Calling sectionManager.startEditing', 'CLICK');
- this.sectionManager.startEditing(sectionId);
- debug('handleSectionClick: Successfully called startEditing', 'CLICK');
- } catch (error) {
- debug('handleSectionClick: ERROR in startEditing: ' + error.message, 'ERROR');
- console.error('Failed to start editing:', error);
- }
- }
-
- /**
- * Show editor for a section
- */
- showEditor(sectionId, content) {
- debug('showEditor: called for section: ' + sectionId, 'EDITOR');
-
- const element = this.findSectionElement(sectionId);
- debug('showEditor: Found element: ' + (element ? 'YES' : 'NO'), 'EDITOR');
- if (!element) return;
-
- debug('showEditor: About to hide current editor', 'EDITOR');
- this.hideCurrentEditor();
- debug('showEditor: Hidden current editor', 'EDITOR');
-
- const section = this.sectionManager.sections.get(sectionId);
- const isImageSection = section && section.isImage();
-
- if (isImageSection) {
- this.showImageEditor(sectionId, section);
- return;
- }
-
- // Create content area for text editing
- const editorContent = document.createElement('div');
- editorContent.className = 'ui-edit-editor-content';
-
- // Check if we have space for side-by-side layout
- const targetElement = this.findSectionElement(sectionId);
- const rect = targetElement ? targetElement.getBoundingClientRect() : null;
- const viewport = { width: window.innerWidth, height: window.innerHeight };
- const hasWideLayout = rect && viewport.width >= 800 && (viewport.width - rect.right) >= 120;
-
- if (hasWideLayout) {
- // Side-by-side layout: textarea on left, controls on right
- editorContent.style.cssText = `
- display: flex;
- gap: 16px;
- flex: 1;
- min-width: 0;
- align-items: flex-start;
- `;
- } else {
- // Stacked layout: textarea above, controls below
- editorContent.style.cssText = `
- display: flex;
- flex-direction: column;
- gap: 12px;
- flex: 1;
- min-width: 0;
- `;
- }
-
- // Create textarea container
- const textareaContainer = document.createElement('div');
- textareaContainer.style.cssText = hasWideLayout ? 'flex: 1; min-width: 0;' : 'width: 100%;';
-
- // Create textarea
- const textarea = document.createElement('textarea');
- textarea.value = content || section.currentMarkdown;
- textarea.style.cssText = `
- width: 100%;
- min-height: 120px;
- padding: 12px;
- border: 1px solid #ddd;
- border-radius: 4px;
- font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
- font-size: 14px;
- line-height: 1.5;
- resize: vertical;
- box-sizing: border-box;
- `;
-
- // Create controls
- const controls = document.createElement('div');
- if (hasWideLayout) {
- controls.style.cssText = `
- display: flex;
- flex-direction: column;
- gap: 8px;
- min-width: 100px;
- flex-shrink: 0;
- `;
- } else {
- controls.style.cssText = `
- display: flex;
- gap: 8px;
- justify-content: flex-end;
- flex-wrap: wrap;
- `;
- }
-
- const acceptButton = document.createElement('button');
- acceptButton.textContent = hasWideLayout ? 'โ' : 'Accept';
- acceptButton.style.cssText = `
- background: #28a745;
- color: white;
- border: none;
- padding: ${hasWideLayout ? '8px 12px' : '8px 16px'};
- border-radius: 4px;
- cursor: pointer;
- ${hasWideLayout ? 'width: 100%;' : ''}
- font-size: ${hasWideLayout ? '14px' : '13px'};
- `;
-
- const cancelButton = document.createElement('button');
- cancelButton.textContent = hasWideLayout ? 'โ' : 'Cancel';
- cancelButton.style.cssText = `
- background: #dc3545;
- color: white;
- border: none;
- padding: ${hasWideLayout ? '8px 12px' : '8px 16px'};
- border-radius: 4px;
- cursor: pointer;
- ${hasWideLayout ? 'width: 100%;' : ''}
- font-size: ${hasWideLayout ? '14px' : '13px'};
- `;
-
- const resetButton = document.createElement('button');
- resetButton.textContent = hasWideLayout ? 'โบ' : 'โบ Reset';
- resetButton.style.cssText = `
- background: #fd7e14;
- color: white;
- border: none;
- padding: ${hasWideLayout ? '8px 12px' : '8px 16px'};
- border-radius: 4px;
- cursor: pointer;
- ${hasWideLayout ? 'width: 100%;' : ''}
- font-size: ${hasWideLayout ? '14px' : '13px'};
- `;
-
- controls.appendChild(acceptButton);
- controls.appendChild(cancelButton);
- controls.appendChild(resetButton);
-
- // Assemble the layout
- textareaContainer.appendChild(textarea);
-
- if (hasWideLayout) {
- editorContent.appendChild(textareaContainer);
- editorContent.appendChild(controls);
- } else {
- editorContent.appendChild(textareaContainer);
- editorContent.appendChild(controls);
- }
-
- // Create floating menu
- const floatingMenu = new FloatingMenu(sectionId, 'text', this);
- this.currentFloatingMenu = floatingMenu;
- this.editingSections.add(sectionId);
-
- floatingMenu.show(editorContent);
-
- // Add event listeners
- acceptButton.addEventListener('click', () => {
- this.sectionManager.updateContent(sectionId, textarea.value);
- this.sectionManager.acceptChanges(sectionId);
- floatingMenu.hide();
- this.currentFloatingMenu = null; // Clear reference
- });
-
- cancelButton.addEventListener('click', () => {
- this.sectionManager.cancelChanges(sectionId);
- floatingMenu.hide();
- this.currentFloatingMenu = null; // Clear reference
- });
-
- resetButton.addEventListener('click', () => {
- // Reset textarea to original content and apply the change
- const section = this.sectionManager.sections.get(sectionId);
- if (section) {
- textarea.value = section.originalMarkdown;
- // Actually update the section content to original and accept the changes
- this.sectionManager.updateContent(sectionId, section.originalMarkdown);
- this.sectionManager.acceptChanges(sectionId);
- // Close the editor
- floatingMenu.hide();
- this.currentFloatingMenu = null;
- }
- });
-
- // Auto-focus textarea
- setTimeout(() => textarea.focus(), 100);
- }
-
- /**
- * 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.originalMarkdown,
- 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;
- }
-
- // Check if we have space for side-by-side layout
- const targetElement = this.findSectionElement(sectionId);
- const rect = targetElement ? targetElement.getBoundingClientRect() : null;
- const viewport = { width: window.innerWidth, height: window.innerHeight };
- const hasWideLayout = rect && viewport.width >= 800 && (viewport.width - rect.right) >= 120;
-
- // Create image editor content area
- const editorContent = document.createElement('div');
- editorContent.className = 'ui-edit-image-content';
-
- if (hasWideLayout) {
- // Side-by-side layout: content on left, controls on right
- editorContent.style.cssText = `
- display: flex;
- gap: 16px;
- flex: 1;
- min-width: 0;
- align-items: flex-start;
- `;
- } else {
- // Stacked layout: content above, controls below
- editorContent.style.cssText = `
- display: flex;
- flex-direction: column;
- gap: 15px;
- flex: 1;
- min-width: 0;
- `;
- }
-
- // Create content container for image and alt text
- const contentContainer = document.createElement('div');
- contentContainer.style.cssText = hasWideLayout ? 'flex: 1; min-width: 0;' : 'width: 100%;';
- if (!hasWideLayout) {
- contentContainer.style.cssText += `
- display: flex;
- flex-direction: column;
- gap: 15px;
- `;
- } else {
- contentContainer.style.cssText += `
- display: flex;
- flex-direction: column;
- gap: 12px;
- `;
- }
-
- // 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 container
- contentContainer.appendChild(imagePreview);
- contentContainer.appendChild(altTextContainer);
- contentContainer.appendChild(changeIndicator);
- contentContainer.appendChild(fileInput);
-
- // Create controls
- const controls = document.createElement('div');
- controls.className = 'ui-edit-controls';
- if (hasWideLayout) {
- controls.style.cssText = `
- display: flex;
- flex-direction: column;
- gap: 8px;
- min-width: 100px;
- flex-shrink: 0;
- `;
- } else {
- controls.style.cssText = `
- display: flex;
- flex-direction: column;
- gap: 8px;
- width: 100%;
- `;
- }
-
- const acceptBtn = document.createElement('button');
- acceptBtn.textContent = hasWideLayout ? 'โ' : 'โ Accept';
- acceptBtn.style.cssText = `
- padding: ${hasWideLayout ? '8px 12px' : '8px 12px'};
- font-size: ${hasWideLayout ? '14px' : '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 = hasWideLayout ? 'โ' : 'โ Cancel';
- cancelBtn.style.cssText = `
- padding: ${hasWideLayout ? '8px 12px' : '8px 12px'};
- font-size: ${hasWideLayout ? '14px' : '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 = hasWideLayout ? 'โบ' : 'โบ Reset';
- resetBtn.style.cssText = `
- padding: ${hasWideLayout ? '8px 12px' : '8px 12px'};
- font-size: ${hasWideLayout ? '14px' : '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', (event) => {
- event.preventDefault();
- event.stopPropagation();
-
- // Reset to original content
- const originalImageMatch = stagingState.originalMarkdown.match(/!\[(.*?)\]\((.*?)\)/);
-
- if (originalImageMatch) {
- const [, originalAltText, originalImageSrc] = originalImageMatch;
-
- // Update staging state to original values
- 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 = originalAltText;
-
- // Trigger input event to ensure UI consistency
- const inputEvent = new Event('input', { bubbles: true, cancelable: true });
- altTextInput.dispatchEvent(inputEvent);
-
- // Reset preview to original image
- updateImagePreview(originalImageSrc, originalAltText);
-
- // Update change indicator
- updateChangeIndicator();
-
- // Actually update the section content to original and accept the changes
- this.sectionManager.updateContent(sectionId, stagingState.originalMarkdown);
- this.sectionManager.acceptChanges(sectionId);
-
- // Close the editor
- this.currentFloatingMenu.hide();
- this.currentFloatingMenu = null;
- }
- });
-
- // Assemble the final layout
- if (hasWideLayout) {
- editorContent.appendChild(contentContainer);
- editorContent.appendChild(controls);
- } else {
- editorContent.appendChild(contentContainer);
- editorContent.appendChild(controls);
- }
-
- // Create floating menu
- const floatingMenu = new FloatingMenu(sectionId, 'image', this);
- this.currentFloatingMenu = floatingMenu;
- this.editingSections.add(sectionId);
-
- floatingMenu.show(editorContent);
- }
-
- /**
- * Hide current editor
- */
- hideCurrentEditor() {
- debug('EDITOR: hideCurrentEditor called', 'EDITOR');
-
- if (this.currentFloatingMenu) {
- this.currentFloatingMenu.hide();
- this.currentFloatingMenu = null;
- }
-
- debug('EDITOR: hideCurrentEditor completed', 'EDITOR');
- }
-
- /**
- * Track event for analytics
- */
- trackEvent(eventType, data) {
- const eventRecord = {
- type: eventType,
- data: data,
- timestamp: new Date().toISOString()
- };
-
- this.eventHistory.push(eventRecord);
- if (this.eventStats.hasOwnProperty(eventType)) {
- this.eventStats[eventType]++;
- }
-
- // Keep only last 100 events
- if (this.eventHistory.length > 100) {
- this.eventHistory = this.eventHistory.slice(-100);
- }
- }
-
- /**
- * Get event statistics
- */
- getEventStats() {
- const totalEvents = Object.values(this.eventStats).reduce((sum, count) => sum + count, 0);
-
- return {
- stats: { ...this.eventStats },
- totalEvents,
- recentEvents: this.eventHistory.slice(-10)
- };
- }
-
- /**
- * Handle keyboard shortcuts
- */
- handleKeydown(event) {
- // Basic keyboard shortcut handling
- if (event.ctrlKey || event.metaKey) {
- if (event.key === 'Enter') {
- // Accept changes
- const activeSection = Array.from(this.editingSections)[0];
- if (activeSection) {
- this.trackEvent('keyboard-shortcut', { shortcut: 'ctrl+enter', action: 'accept' });
- }
- } else if (event.key === 'Escape') {
- // Cancel changes
- const activeSection = Array.from(this.editingSections)[0];
- if (activeSection) {
- this.trackEvent('keyboard-shortcut', { shortcut: 'escape', action: 'cancel' });
- this.hideCurrentEditor();
- }
- }
- }
- }
-}
-
-// Export for use in tests and other modules
-if (typeof module !== 'undefined' && module.exports) {
- module.exports = { DOMRenderer, FloatingMenu };
-}
-
-// Export for browser use
-if (typeof window !== 'undefined') {
- window.DOMRenderer = DOMRenderer;
- window.FloatingMenu = FloatingMenu;
-}
\ No newline at end of file
diff --git a/markitect/static/js/config-loader.js b/markitect/static/js/config-loader.js
deleted file mode 100644
index 70964a7c..00000000
--- a/markitect/static/js/config-loader.js
+++ /dev/null
@@ -1,168 +0,0 @@
-/**
- * Configuration Loader - Clean interface between Python and JavaScript
- *
- * This module provides the ONLY interface for Python-generated data.
- * All dynamic data from Python must be passed through this JSON configuration.
- */
-
-class MarkitectConfig {
- constructor() {
- this.config = null;
- this.loaded = false;
-
- // Simple immediate loading - if script is loaded, DOM is ready
- this.loadConfig();
- }
-
- loadConfig() {
- try {
- const configElement = document.getElementById('markitect-config');
- if (!configElement) {
- throw new Error('Markitect configuration not found - missing markitect-config script element');
- }
-
- this.config = JSON.parse(configElement.textContent);
- this.loaded = true;
- console.log('โ
Markitect configuration loaded successfully');
-
- // Validate required fields
- this.validateConfig();
-
- } catch (error) {
- console.error('โ Failed to load Markitect configuration:', error);
- this.config = this.getDefaultConfig();
- }
- }
-
- validateConfig() {
- const required = ['markdownContent', 'mode'];
- const missing = required.filter(key => !(key in this.config));
-
- if (missing.length > 0) {
- console.warn('โ ๏ธ Missing required config fields:', missing);
- }
- }
-
- getDefaultConfig() {
- return {
- markdownContent: '# Default Content\n\nConfiguration failed to load.',
- markdownContentWithDogtag: '# Default Content\n\nConfiguration failed to load.',
- dogtagContent: '',
- mode: 'edit',
- theme: 'github',
- keyboardShortcuts: true,
- autosave: false,
- sections: true,
- originalFilename: 'document',
- version: 'Markitect v0.8.1',
- repoName: 'Markitect',
- base64References: {}
- };
- }
-
- // Getter methods for clean access
- get markdownContent() {
- return this.config.markdownContent || '';
- }
-
- get markdownContentWithDogtag() {
- return this.config.markdownContentWithDogtag || this.markdownContent;
- }
-
- get dogtagContent() {
- return this.config.dogtagContent || '';
- }
-
- get mode() {
- return this.config.mode || 'edit';
- }
-
- get isEditMode() {
- return this.mode === 'edit';
- }
-
- get isInsertMode() {
- return this.mode === 'insert';
- }
-
- get theme() {
- return this.config.theme || 'github';
- }
-
- get originalFilename() {
- return this.config.originalFilename || 'document';
- }
-
- get version() {
- return this.config.version || 'Markitect v0.8.1';
- }
-
- get repoName() {
- return this.config.repoName || 'Markitect';
- }
-
- get keyboardShortcuts() {
- return this.config.keyboardShortcuts !== false;
- }
-
- get base64References() {
- return this.config.base64References || {};
- }
-
- get restrictedHeadingLevels() {
- return this.config.restrictedHeadingLevels || [1, 2, 3];
- }
-
- // Check if config is ready for access
- isReady() {
- return this.loaded && this.config !== null;
- }
-
- // Wait for config to be ready
- waitForReady(callback, maxWait = 5000) {
- const startTime = Date.now();
- const checkReady = () => {
- if (this.isReady()) {
- callback();
- } else if (Date.now() - startTime < maxWait) {
- setTimeout(checkReady, 50);
- } else {
- console.error('โ Configuration loading timeout after', maxWait, 'ms');
- callback(); // Call anyway with default config
- }
- };
- checkReady();
- }
-
- // Get full editor configuration object
- getEditorConfig() {
- if (!this.isReady()) {
- console.warn('โ ๏ธ Configuration not ready, using defaults');
- return this.getDefaultConfig();
- }
-
- return {
- mode: this.mode,
- theme: this.theme,
- keyboardShortcuts: this.keyboardShortcuts,
- autosave: this.config.autosave || false,
- sections: this.config.sections !== false,
- originalFilename: this.originalFilename,
- version: this.version,
- repoName: this.repoName,
- restrictedHeadingLevels: this.restrictedHeadingLevels
- };
- }
-}
-
-// Global configuration instance
-window.markitectConfig = new MarkitectConfig();
-
-// Legacy compatibility - expose common config values globally
-window.editorConfig = window.markitectConfig.getEditorConfig();
-window.markitectBase64References = window.markitectConfig.base64References;
-
-// Export for module use
-if (typeof module !== 'undefined' && module.exports) {
- module.exports = MarkitectConfig;
-}
\ No newline at end of file
diff --git a/markitect/static/js/core/debug-system.js b/markitect/static/js/core/debug-system.js
deleted file mode 100644
index e9776da6..00000000
--- a/markitect/static/js/core/debug-system.js
+++ /dev/null
@@ -1,290 +0,0 @@
-/**
- * Independent Debug System for Markitect
- * Uses IndexedDB for persistence and provides selection-based filtering
- */
-class MarkitectDebugSystem {
- constructor() {
- this.db = null;
- this.messages = [];
- this.maxMessages = 1000;
- this.isEnabled = true;
- this.subscribers = [];
-
- // Selection and filtering system
- this.selectionCriteria = {
- includeDocumentEvents: true,
- includeSystemEvents: false,
- includeControlEvents: true,
- includeEditingEvents: true,
- includeNavigationEvents: false,
- includedHeadings: new Set(), // Track which document headings to monitor
- excludedSources: new Set(['ContentsControl', 'DocumentNavigator'])
- };
-
- this.init();
- }
-
- // Initialize IndexedDB for persistence
- async init() {
- return new Promise((resolve, reject) => {
- const request = indexedDB.open('MarkitectDebugDB', 1);
-
- request.onerror = () => reject(request.error);
- request.onsuccess = () => {
- this.db = request.result;
- this.loadMessages().then(resolve);
- };
-
- request.onupgradeneeded = (e) => {
- const db = e.target.result;
- if (!db.objectStoreNames.contains('messages')) {
- const store = db.createObjectStore('messages', { keyPath: 'id', autoIncrement: true });
- store.createIndex('timestamp', 'timestamp', { unique: false });
- store.createIndex('category', 'category', { unique: false });
- }
- };
- });
- }
-
- // Add a debug message with selection filtering
- async addMessage(message, category = 'INFO', source = 'System', context = {}) {
- // Check if this message should be included based on selection criteria
- if (!this.shouldIncludeMessage(message, category, source, context)) {
- return null;
- }
-
- const messageObj = {
- timestamp: new Date().toISOString(),
- message: String(message),
- category: category.toUpperCase(),
- source: String(source),
- context: context || {},
- id: null // Will be set by IndexedDB
- };
-
- // Store in IndexedDB if available
- if (this.db) {
- try {
- await this.saveMessage(messageObj);
- } catch (error) {
- console.warn('Failed to save debug message to IndexedDB:', error);
- }
- }
-
- // Store in memory
- this.messages.unshift(messageObj);
-
- // Limit memory storage
- if (this.messages.length > this.maxMessages) {
- this.messages = this.messages.slice(0, this.maxMessages);
- }
-
- // Notify subscribers
- this.notifySubscribers(messageObj);
-
- // Console output for development
- const consoleMethod = category.toLowerCase() === 'error' ? 'error' :
- category.toLowerCase() === 'warning' ? 'warn' : 'log';
- console[consoleMethod](`[${source}] ${message}`, context);
-
- return messageObj;
- }
-
- // Selection filtering logic
- shouldIncludeMessage(message, category, source, context) {
- if (!this.isEnabled) return false;
-
- const eventType = context.eventType || 'UNKNOWN';
- const criteria = this.selectionCriteria;
-
- // Check event type filters
- switch (eventType.toUpperCase()) {
- case 'DOCUMENT':
- if (!criteria.includeDocumentEvents) return false;
- break;
- case 'SYSTEM':
- if (!criteria.includeSystemEvents) return false;
- break;
- case 'CONTROL':
- if (!criteria.includeControlEvents) return false;
- break;
- case 'EDITING':
- if (!criteria.includeEditingEvents) return false;
- break;
- case 'NAVIGATION':
- if (!criteria.includeNavigationEvents) return false;
- break;
- }
-
- // Check excluded sources
- if (criteria.excludedSources.has(source)) {
- return false;
- }
-
- // Check heading-specific filtering
- if (context.sectionId && criteria.includedHeadings.size > 0) {
- const sectionElement = document.getElementById(context.sectionId);
- if (sectionElement) {
- const heading = sectionElement.querySelector('h1, h2, h3, h4, h5, h6');
- if (heading && !criteria.includedHeadings.has(heading.textContent.trim())) {
- return false;
- }
- }
- }
-
- return true;
- }
-
- // Save message to IndexedDB
- async saveMessage(messageObj) {
- return new Promise((resolve, reject) => {
- const transaction = this.db.transaction(['messages'], 'readwrite');
- const store = transaction.objectStore('messages');
- const request = store.add(messageObj);
-
- request.onsuccess = () => resolve(request.result);
- request.onerror = () => reject(request.error);
- });
- }
-
- // Load messages from IndexedDB
- async loadMessages() {
- if (!this.db) return [];
-
- return new Promise((resolve, reject) => {
- const transaction = this.db.transaction(['messages'], 'readonly');
- const store = transaction.objectStore('messages');
- const request = store.getAll();
-
- request.onsuccess = () => {
- this.messages = request.result.reverse(); // Most recent first
- resolve(this.messages);
- };
- request.onerror = () => reject(request.error);
- });
- }
-
- // Clear all messages
- async clearMessages() {
- this.messages = [];
-
- if (this.db) {
- return new Promise((resolve, reject) => {
- const transaction = this.db.transaction(['messages'], 'readwrite');
- const store = transaction.objectStore('messages');
- const request = store.clear();
-
- request.onsuccess = () => resolve();
- request.onerror = () => reject(request.error);
- });
- }
- }
-
- // Get filtered messages
- getMessages(filter = {}) {
- let filteredMessages = [...this.messages];
-
- if (filter.category) {
- filteredMessages = filteredMessages.filter(msg =>
- msg.category.toLowerCase() === filter.category.toLowerCase()
- );
- }
-
- if (filter.source) {
- filteredMessages = filteredMessages.filter(msg =>
- msg.source.toLowerCase().includes(filter.source.toLowerCase())
- );
- }
-
- if (filter.since) {
- const sinceDate = new Date(filter.since);
- filteredMessages = filteredMessages.filter(msg =>
- new Date(msg.timestamp) >= sinceDate
- );
- }
-
- if (filter.limit) {
- filteredMessages = filteredMessages.slice(0, filter.limit);
- }
-
- return filteredMessages;
- }
-
- // Update selection criteria
- updateSelectionCriteria(updates) {
- Object.assign(this.selectionCriteria, updates);
- this.notifySubscribers({ type: 'criteria-updated', criteria: this.selectionCriteria });
- }
-
- // Add heading to monitoring
- addHeadingToMonitoring(headingText) {
- this.selectionCriteria.includedHeadings.add(headingText);
- }
-
- // Remove heading from monitoring
- removeHeadingFromMonitoring(headingText) {
- this.selectionCriteria.includedHeadings.delete(headingText);
- }
-
- // Scan document for available headings
- scanDocumentHeadings() {
- const headings = document.querySelectorAll('h1, h2, h3, h4, h5, h6');
- return Array.from(headings)
- .map(h => h.textContent.trim())
- .filter(text => text.length > 0 && !text.toLowerCase().includes('control'));
- }
-
- // Subscribe to debug messages
- subscribe(callback) {
- this.subscribers.push(callback);
- return () => {
- const index = this.subscribers.indexOf(callback);
- if (index > -1) {
- this.subscribers.splice(index, 1);
- }
- };
- }
-
- // Notify all subscribers
- notifySubscribers(message) {
- this.subscribers.forEach(callback => {
- try {
- callback(message);
- } catch (error) {
- console.error('Debug subscriber error:', error);
- }
- });
- }
-
- // Toggle debug system
- setEnabled(enabled) {
- this.isEnabled = enabled;
- this.addMessage(
- `Debug system ${enabled ? 'enabled' : 'disabled'}`,
- 'INFO',
- 'DebugSystem',
- { eventType: 'SYSTEM' }
- );
- }
-
- // Get statistics
- getStats() {
- const stats = {
- total: this.messages.length,
- byCategory: {},
- bySource: {},
- enabled: this.isEnabled,
- criteria: { ...this.selectionCriteria }
- };
-
- this.messages.forEach(msg => {
- stats.byCategory[msg.category] = (stats.byCategory[msg.category] || 0) + 1;
- stats.bySource[msg.source] = (stats.bySource[msg.source] || 0) + 1;
- });
-
- return stats;
- }
-}
-
-// Initialize and expose globally
-window.MarkitectDebugSystem = new MarkitectDebugSystem();
\ No newline at end of file
diff --git a/markitect/static/js/core/section-manager.js b/markitect/static/js/core/section-manager.js
deleted file mode 100644
index b1dc6fd0..00000000
--- a/markitect/static/js/core/section-manager.js
+++ /dev/null
@@ -1,544 +0,0 @@
-/**
- * SectionManager Component
- *
- * Extracted from monolithic editor.js as part of architecture refactoring.
- * Manages the collection of sections and their state transitions.
- *
- * Dependencies:
- * - EditState enum (imported)
- * - SectionType enum (imported)
- * - Section class (imported)
- * - debug function (imported)
- */
-
-// Import dependencies - these will be separate modules
-const EditState = Object.freeze({
- ORIGINAL: 'original',
- EDITING: 'editing',
- MODIFIED: 'modified',
- SAVED: 'saved'
-});
-
-const SectionType = Object.freeze({
- HEADING: 'heading',
- PARAGRAPH: 'paragraph',
- LIST: 'list',
- CODE: 'code',
- QUOTE: 'quote',
- TABLE: 'table',
- HR: 'hr',
- IMAGE: 'image'
-});
-
-// Debug function (will be extracted to utils)
-function debug(message, category = 'INFO') {
- // Simple console debug for now - will be enhanced later
- console.log(`DEBUG ${category}: ${message}`);
-}
-
-/**
- * Section Class - manages individual section state and content
- */
-class Section {
- constructor(id, markdown, type) {
- this.id = id;
- this.originalMarkdown = markdown;
- this.currentMarkdown = markdown;
- this.editingMarkdown = markdown;
- this.pendingMarkdown = null;
- this.type = type;
- this.state = EditState.ORIGINAL;
- this.domElement = null;
- this.lastSaved = null;
- this.created = new Date();
- }
-
- static generateId(markdown, position, strategy = 'hash', parentId = null) {
- return this.generateIdWithStrategy(markdown, position, strategy, parentId);
- }
-
- static generateIdWithStrategy(markdown, position, strategy = 'hash', parentId = null) {
- const sanitizedContent = this.sanitizeContentForId(markdown);
- const normalizedContent = this.normalizeContentForHashing(sanitizedContent);
- const sectionType = this.detectType(markdown);
-
- switch (strategy) {
- case 'timestamp':
- return this.generateTimestampId(normalizedContent, position, sectionType);
- case 'sequential':
- return this.generateSequentialId(normalizedContent, position, sectionType);
- case 'hierarchical':
- return this.generateHierarchicalId(normalizedContent, position, parentId);
- case 'hash':
- default:
- return this.generateAdvancedId(normalizedContent, position, sectionType);
- }
- }
-
- static generateAdvancedId(content, position, sectionType) {
- const contentHash = this.generateCryptoHash(content);
- const safeType = sectionType || 'paragraph';
- const typePrefix = safeType.substring(0, 3);
- const positionHex = position.toString(16).padStart(2, '0');
-
- return `section-${typePrefix}-${contentHash}-${positionHex}`;
- }
-
- static generateCryptoHash(content) {
- let hash = 0;
- if (content.length === 0) return '00000000';
-
- for (let i = 0; i < content.length; i++) {
- const char = content.charCodeAt(i);
- hash = ((hash << 5) - hash) + char;
- hash = hash & hash;
- }
-
- const hexHash = Math.abs(hash).toString(16).padStart(8, '0');
- return hexHash.substring(0, 8);
- }
-
- static normalizeContentForHashing(content) {
- if (!content || typeof content !== 'string') {
- return '';
- }
-
- return content
- .trim()
- .replace(/\s+/g, ' ')
- .replace(/\r\n/g, '\n')
- .toLowerCase();
- }
-
- static sanitizeContentForId(content) {
- if (!content || typeof content !== 'string') {
- return '';
- }
-
- return content
- .replace(/<[^>]*>/g, '')
- .replace(/javascript:/gi, '')
- .replace(/[^\w\s\-_.#]/g, '')
- .trim();
- }
-
- static generateTimestampId(content, position = 0, sectionType = 'paragraph') {
- const timestamp = Date.now().toString(36);
- const contentSnippet = this.generateCryptoHash(content || '').substring(0, 4);
- const safeType = sectionType || 'paragraph';
- const typePrefix = safeType.substring(0, 3);
-
- return `section-${typePrefix}-${contentSnippet}-${timestamp}`;
- }
-
- static generateSequentialId(content, position, sectionType = 'paragraph') {
- const safeType = sectionType || 'paragraph';
- const typePrefix = safeType.substring(0, 3);
- const seqNumber = (position || 0).toString().padStart(3, '0');
- const contentHash = this.generateCryptoHash(content || '').substring(0, 4);
-
- return `section-${typePrefix}-seq${seqNumber}-${contentHash}`;
- }
-
- static generateHierarchicalId(content, position, parentId = null) {
- const contentHash = this.generateCryptoHash(content || '').substring(0, 6);
-
- if (parentId) {
- const childIndex = (position || 0).toString().padStart(2, '0');
- return `${parentId}-child-${childIndex}-${contentHash}`;
- } else {
- return `section-root-${position || 0}-${contentHash}`;
- }
- }
-
- static detectType(markdown) {
- if (!markdown || typeof markdown !== 'string') {
- return SectionType.PARAGRAPH;
- }
-
- const content = markdown.replace(/^\n+|\n+$/g, '');
- if (!content) {
- return SectionType.PARAGRAPH;
- }
-
- const trimmed = content.trim();
-
- // Detection order matters - most specific first
- if (this.isHeading(trimmed)) {
- return SectionType.HEADING;
- }
-
- if (this.isImage(trimmed)) {
- return SectionType.IMAGE;
- }
-
- if (this.isCodeBlock(trimmed)) {
- return SectionType.CODE;
- }
-
- return SectionType.PARAGRAPH;
- }
-
- static isHeading(trimmed) {
- const headingPattern = /^#{1,6}\s+.+/;
- return headingPattern.test(trimmed);
- }
-
- static isImage(trimmed) {
- const imagePattern = /!\[.*?\]\([^)]+\)/;
- return imagePattern.test(trimmed);
- }
-
- static isCodeBlock(trimmed) {
- if (trimmed.startsWith('```') || trimmed.startsWith('~~~')) {
- return true;
- }
- if (trimmed.includes('```') || trimmed.includes('~~~')) {
- const codeBlockPattern = /```[\s\S]*?```|~~~[\s\S]*?~~~/;
- if (codeBlockPattern.test(trimmed)) {
- return true;
- }
- }
- return false;
- }
-
- startEdit() {
- if (this.state === EditState.EDITING) {
- throw new Error(`Section ${this.id} is already being edited`);
- }
- this.editingMarkdown = this.pendingMarkdown || this.currentMarkdown;
- this.state = EditState.EDITING;
- return this.editingMarkdown;
- }
-
- updateContent(markdown) {
- if (this.state !== EditState.EDITING) {
- throw new Error(`Section ${this.id} is not in editing state`);
- }
- this.editingMarkdown = markdown;
- }
-
- acceptChanges() {
- if (this.state !== EditState.EDITING) {
- throw new Error(`Section ${this.id} is not in editing state`);
- }
- this.currentMarkdown = this.editingMarkdown;
- this.editingMarkdown = null;
- this.pendingMarkdown = null;
- this.state = EditState.SAVED;
- this.lastSaved = new Date();
- return this.currentMarkdown;
- }
-
- cancelChanges() {
- if (this.state !== EditState.EDITING) {
- throw new Error(`Section ${this.id} is not in editing state`);
- }
- this.editingMarkdown = null;
- if (this.pendingMarkdown !== null) {
- this.state = EditState.MODIFIED;
- return this.pendingMarkdown;
- } else if (this.lastSaved !== null) {
- this.state = EditState.SAVED;
- return this.currentMarkdown;
- } else {
- this.state = this.hasChanges() ? EditState.MODIFIED : EditState.ORIGINAL;
- return this.currentMarkdown;
- }
- }
-
- stopEditing() {
- if (this.state !== EditState.EDITING) {
- return this.state;
- }
-
- if (this.editingMarkdown && this.editingMarkdown !== this.currentMarkdown) {
- this.pendingMarkdown = this.editingMarkdown;
- this.state = EditState.MODIFIED;
- } else {
- this.pendingMarkdown = null;
- if (this.lastSaved !== null) {
- this.state = EditState.SAVED;
- } else {
- this.state = this.hasChanges() ? EditState.MODIFIED : EditState.ORIGINAL;
- }
- }
-
- this.editingMarkdown = null;
- return this.state;
- }
-
- resetToOriginal() {
- this.currentMarkdown = this.originalMarkdown;
- this.editingMarkdown = this.originalMarkdown;
- this.pendingMarkdown = null;
- this.state = EditState.ORIGINAL;
- return this.originalMarkdown;
- }
-
- isEditing() {
- return this.state === EditState.EDITING;
- }
-
- hasChanges() {
- return this.currentMarkdown !== this.originalMarkdown;
- }
-
- getStatus() {
- return {
- id: this.id,
- state: this.state,
- hasChanges: this.hasChanges(),
- isEditing: this.isEditing(),
- contentLength: this.currentMarkdown.length,
- lastSaved: this.lastSaved,
- type: this.type,
- originalLength: this.originalMarkdown.length,
- currentLength: this.currentMarkdown.length
- };
- }
-
- isImage() {
- return this.type === SectionType.IMAGE;
- }
-
- redetectType(content = null) {
- const markdown = content || this.currentMarkdown;
- const oldType = this.type;
- this.type = Section.detectType(markdown);
-
- if (oldType !== this.type) {
- debug(`Section ${this.id} type changed from ${oldType} to ${this.type}`, 'TYPE_DETECTION');
- }
-
- return this.type;
- }
-}
-
-/**
- * SectionManager - Manages the collection of sections
- */
-class SectionManager {
- constructor() {
- this.sections = new Map();
- this.listeners = new Map();
- this.statusInterval = null;
- this.lastStatusUpdate = new Date().toISOString();
- }
-
- on(event, callback) {
- if (!this.listeners.has(event)) {
- this.listeners.set(event, []);
- }
- this.listeners.get(event).push(callback);
- }
-
- emit(event, data) {
- if (this.listeners.has(event)) {
- this.listeners.get(event).forEach(callback => callback(data));
- }
- }
-
- createSectionsFromMarkdown(markdownContent) {
- // Split content into blocks separated by double newlines
- const blocks = markdownContent.split(/\n\s*\n/);
- const sections = [];
- let position = 0;
-
- for (const block of blocks) {
- const trimmedBlock = block.trim();
- if (!trimmedBlock) continue;
-
- // Check if this block should be split further
- const lines = trimmedBlock.split('\n');
- let currentSection = '';
-
- for (let i = 0; i < lines.length; i++) {
- const line = lines[i];
- const isHeading = /^#{1,6}\s/.test(line.trim());
- const isImage = /^\s*!\[.*?\]\(.*?\)\s*$/.test(line);
-
- // Each heading or image starts a new section
- if ((isHeading || isImage) && currentSection.trim()) {
- // Save the previous section
- const sectionId = Section.generateId(currentSection, position);
- const sectionType = Section.detectType(currentSection);
- const section = new Section(sectionId, currentSection.trim(), sectionType);
- sections.push(section);
- this.sections.set(sectionId, section);
- position++;
- currentSection = line;
- } else {
- if (currentSection) currentSection += '\n';
- currentSection += line;
- }
- }
-
- // Save the final section from this block
- if (currentSection.trim()) {
- const sectionId = Section.generateId(currentSection, position);
- const sectionType = Section.detectType(currentSection);
- const section = new Section(sectionId, currentSection.trim(), sectionType);
- sections.push(section);
- this.sections.set(sectionId, section);
- position++;
- }
- }
-
- this.emit('sections-created', { sections, count: sections.length });
- return sections;
- }
-
- startEditing(sectionId) {
- debug('MANAGER: startEditing called for: ' + sectionId, 'MANAGER');
-
- const section = this.sections.get(sectionId);
- if (!section) {
- throw new Error(`Section ${sectionId} not found`);
- }
-
- if (section.isEditing()) {
- debug('MANAGER: Section already in editing state: ' + sectionId, 'MANAGER');
- return section.editingMarkdown;
- }
-
- debug('MANAGER: Starting edit for section: ' + sectionId, 'MANAGER');
- const content = section.startEdit();
-
- debug('MANAGER: About to emit edit-started event for: ' + sectionId, 'MANAGER');
- this.emit('edit-started', { sectionId, content, section: section.getStatus() });
- debug('MANAGER: Emitted edit-started event for: ' + sectionId, 'MANAGER');
-
- return content;
- }
-
- updateContent(sectionId, markdown) {
- const section = this.sections.get(sectionId);
- if (!section) {
- throw new Error(`Section ${sectionId} not found`);
- }
-
- const oldType = section.type;
- section.updateContent(markdown);
- const newType = section.redetectType(markdown);
-
- const eventData = {
- sectionId,
- markdown,
- section: section.getStatus(),
- typeChanged: oldType !== newType,
- oldType,
- newType
- };
-
- this.emit('content-updated', eventData);
-
- if (oldType !== newType) {
- this.emit('section-type-changed', {
- sectionId,
- oldType,
- newType,
- section: section.getStatus()
- });
- }
- }
-
- acceptChanges(sectionId) {
- const section = this.sections.get(sectionId);
- if (!section) {
- throw new Error(`Section ${sectionId} not found`);
- }
-
- const content = section.acceptChanges();
- this.emit('changes-accepted', { sectionId, content, section: section.getStatus() });
- return content;
- }
-
- cancelChanges(sectionId) {
- const section = this.sections.get(sectionId);
- if (!section) {
- throw new Error(`Section ${sectionId} not found`);
- }
-
- const content = section.cancelChanges();
- this.emit('changes-cancelled', { sectionId, content, section: section.getStatus() });
- return content;
- }
-
- resetSection(sectionId) {
- const section = this.sections.get(sectionId);
- if (!section) {
- throw new Error(`Section ${sectionId} not found`);
- }
-
- const content = section.resetToOriginal();
- this.emit('section-reset', { sectionId, content, section: section.getStatus() });
- return content;
- }
-
- getDocumentMarkdown() {
- const sortedSections = Array.from(this.sections.values())
- .sort((a, b) => a.created - b.created);
-
- return sortedSections.map(section => section.currentMarkdown).join('\n\n');
- }
-
- getAllSections() {
- return Array.from(this.sections.values());
- }
-
- getDocumentStatus() {
- const sections = Array.from(this.sections.values());
- const editingSections = sections.filter(section => section.isEditing).length;
-
- return {
- totalSections: sections.length,
- editingSections: editingSections
- };
- }
-
- extractHeadings(content) {
- if (!content) return [];
- const lines = content.split('\n');
- return lines.filter(line => /^#{1,6}\s/.test(line.trim()));
- }
-
- handleSectionSplit(sectionId, newContent) {
- const section = this.sections.get(sectionId);
- if (!section) {
- throw new Error(`Section ${sectionId} not found`);
- }
-
- // Remove the original section
- this.sections.delete(sectionId);
-
- // Create new sections from the content
- const newSections = this.createSectionsFromMarkdown(newContent);
-
- // Emit section-split event
- this.emit('section-split', {
- originalSectionId: sectionId,
- newSections: newSections,
- count: newSections.length
- });
-
- return newSections;
- }
-
- createSectionsFromContent(content) {
- return this.createSectionsFromMarkdown(content);
- }
-}
-
-// Export for use in tests and other modules
-if (typeof module !== 'undefined' && module.exports) {
- module.exports = { SectionManager, Section, EditState, SectionType };
-}
-
-// Export for browser use
-if (typeof window !== 'undefined') {
- window.SectionManager = SectionManager;
- window.Section = Section;
- window.EditState = EditState;
- window.SectionType = SectionType;
-}
\ No newline at end of file
diff --git a/markitect/static/js/main-updated.js b/markitect/static/js/main-updated.js
deleted file mode 100644
index 1ce2fea3..00000000
--- a/markitect/static/js/main-updated.js
+++ /dev/null
@@ -1,227 +0,0 @@
-/**
- * Main Markitect JavaScript Entry Point - Clean Architecture Version
- *
- * Uses ONLY the JSON configuration interface - NO Python-generated JavaScript!
- * Initializes all controls and systems when document is ready
- * Implements graceful degradation for missing dependencies
- */
-
-// Main application module
-const MarkitectMain = {
- initialized: false,
- config: null,
-
- // Initialize the complete application
- initialize: function() {
- if (this.initialized) {
- console.log('โ ๏ธ MarkitectMain already initialized, skipping');
- return;
- }
-
- console.log('๐ MarkitectMain initializing...');
-
- try {
- // Get configuration - if not loaded, use defaults
- this.config = window.markitectConfig;
- if (!this.config || !this.config.loaded) {
- console.warn('โ ๏ธ Configuration not loaded, proceeding with defaults');
- this.config = {
- markdownContent: document.querySelector('#markdown-content')?.textContent || '',
- mode: 'edit',
- theme: 'github'
- };
- }
-
- // Initialize core systems
- this.initializeCoreComponents();
- this.initializeControlPanels();
- this.setupEventHandlers();
- this.renderContent();
-
- this.initialized = true;
- console.log('โ
MarkitectMain initialization complete');
-
- } catch (error) {
- console.error('โ MarkitectMain initialization failed:', error);
- this.fallbackMode();
- }
- },
-
- // Initialize core modular components
- initializeCoreComponents: function() {
- console.log('๐ง Initializing core components...');
-
- const container = document.getElementById('markdown-content') || document.body;
-
- // Initialize section manager
- if (typeof SectionManager !== 'undefined') {
- this.sectionManager = new SectionManager();
- console.log('โ
SectionManager initialized');
- } else {
- throw new Error('SectionManager not available');
- }
-
- // Initialize DOM renderer
- if (typeof DOMRenderer !== 'undefined') {
- this.domRenderer = new DOMRenderer(this.sectionManager, container);
- console.log('โ
DOMRenderer initialized');
- } else {
- throw new Error('DOMRenderer not available');
- }
-
- // Initialize debug panel
- if (typeof DebugPanel !== 'undefined') {
- this.debugPanel = new DebugPanel();
- console.log('โ
DebugPanel initialized');
- }
-
- // Legacy DocumentControls removed - functionality now in enhanced control panels
- },
-
- // Initialize enhanced control panels with compass positioning
- initializeControlPanels: function() {
- console.log('๐๏ธ Initializing enhanced control panels with compass positioning...');
-
- // ContentsControl (West)
- if (typeof ContentsControl !== 'undefined') {
- this.contentsControl = new ContentsControl();
- this.contentsControl.config.position = 'w';
- this.contentsControl.show();
- window.contentsControl = this.contentsControl;
- console.log('โ
ContentsControl initialized (West) with enhanced ControlBase');
- }
-
- // StatusControl (East)
- if (typeof StatusControl !== 'undefined') {
- this.statusControl = new StatusControl();
- this.statusControl.config.position = 'e';
- this.statusControl.show();
- window.statusControl = this.statusControl;
- console.log('โ
StatusControl initialized (East) with enhanced ControlBase');
- }
-
- // DebugControl (Southeast)
- if (typeof DebugControl !== 'undefined') {
- this.debugControl = new DebugControl();
- this.debugControl.config.position = 'se';
- this.debugControl.show();
- window.debugControl = this.debugControl;
- console.log('โ
DebugControl initialized (Southeast) with enhanced ControlBase');
- }
-
- // EditControl (Northeast)
- if (typeof EditControl !== 'undefined') {
- this.editControl = new EditControl();
- this.editControl.config.position = 'ne';
- this.editControl.show();
- window.editControl = this.editControl;
- console.log('โ
EditControl initialized (Northeast) with enhanced ControlBase');
- }
- },
-
- // Setup core event handlers (enhanced control panels handle their own events)
- setupEventHandlers: function() {
- console.log('๐ Setting up core event handlers...');
-
- // Setup section manager event handlers for debug panel
- if (this.sectionManager && this.debugPanel) {
- this.sectionManager.on('sections-created', (data) => {
- this.debugPanel.addMessage(`Created ${data.count} sections`, 'INFO');
- });
-
- this.sectionManager.on('edit-started', (data) => {
- this.debugPanel.addMessage(`Edit started for section: ${data.sectionId}`, 'DEBUG');
- });
-
- this.sectionManager.on('changes-accepted', (data) => {
- this.debugPanel.addMessage(`Changes accepted for section: ${data.sectionId}`, 'SUCCESS');
- this.updateSectionDOM(data.sectionId);
- });
-
- this.sectionManager.on('changes-cancelled', (data) => {
- this.debugPanel.addMessage(`Changes cancelled for section: ${data.sectionId}`, 'WARNING');
- });
- }
-
- // Make core components available globally for enhanced controls
- window.sectionManager = this.sectionManager;
- window.domRenderer = this.domRenderer;
- window.debugPanel = this.debugPanel;
-
- console.log('โ
Core event handlers and global references set up');
- },
-
- // Render content using the configuration
- renderContent: function() {
- console.log('๐ Rendering markdown content...');
-
- const markdownToRender = this.config.markdownContent || '';
- if (markdownToRender.trim()) {
- const sections = this.sectionManager.createSectionsFromMarkdown(markdownToRender);
- this.domRenderer.renderAllSections(sections);
-
- if (this.debugPanel) {
- this.debugPanel.addMessage(`Initialized with ${sections.length} sections`, 'INFO');
- }
- console.log(`โ
Rendered ${sections.length} sections`);
- } else {
- if (this.debugPanel) {
- this.debugPanel.addMessage('No markdown content to initialize', 'WARNING');
- }
- console.warn('โ ๏ธ No markdown content to render');
- }
- },
-
- // Update section DOM after changes
- updateSectionDOM: function(sectionId) {
- try {
- const section = this.sectionManager.sections.get(sectionId);
- if (section) {
- const sectionElement = this.domRenderer.findSectionElement(sectionId);
- if (sectionElement) {
- const newElement = this.domRenderer.renderSection(section);
- sectionElement.parentNode.replaceChild(newElement, sectionElement);
-
- if (this.debugPanel) {
- this.debugPanel.addMessage(`DOM updated for section: ${sectionId}`, 'INFO');
- }
- }
- }
- } catch (error) {
- console.error('โ Failed to update section DOM:', error);
- }
- },
-
- // Fallback mode if initialization fails
- fallbackMode: function() {
- console.warn('โ ๏ธ Running in fallback mode');
-
- // Basic content rendering fallback
- const contentDiv = document.getElementById('markdown-content');
- if (contentDiv && this.config && this.config.markdownContent) {
- const basicHtml = this.config.markdownContent
- .replace(/^# (.*$)/gim, '$1 ')
- .replace(/^## (.*$)/gim, '$1 ')
- .replace(/^### (.*$)/gim, '$1 ')
- .replace(/\n\n/g, '')
- .replace(/\n/g, ' ');
-
- contentDiv.innerHTML = `
${basicHtml}
`;
- console.log('โ
Fallback content rendered');
- }
- }
-};
-
-// Make components globally available for debugging
-window.MarkitectMain = MarkitectMain;
-
-// Auto-initialize when DOM is ready
-if (document.readyState === 'loading') {
- document.addEventListener('DOMContentLoaded', function() {
- // Small delay to ensure config is loaded
- setTimeout(() => MarkitectMain.initialize(), 100);
- });
-} else {
- // DOM already ready
- setTimeout(() => MarkitectMain.initialize(), 100);
-}
\ No newline at end of file
diff --git a/markitect/static/js/main.js b/markitect/static/js/main.js
deleted file mode 100644
index 40f8c482..00000000
--- a/markitect/static/js/main.js
+++ /dev/null
@@ -1,201 +0,0 @@
-/**
- * Main Markitect JavaScript Entry Point
- * Initializes all controls and systems when document is ready
- * Implements graceful degradation for missing dependencies
- * Supports Fail Fast strict mode for development
- */
-
-// Development mode detection
-const MARKITECT_STRICT_MODE = (
- window.location.hostname === 'localhost' ||
- window.location.hostname === '127.0.0.1' ||
- window.location.search.includes('strict=true') ||
- window.markitectStrictMode === true
-);
-
-// Utility functions for safe initialization
-const MarkitectMain = {
- // Safe dependency checking with timeout
- checkDependencies: function() {
- const dependencies = {
- debugSystem: !!window.MarkitectDebugSystem,
- control: !!window.Control,
- statusControl: !!window.StatusControl,
- debugControl: !!window.DebugControl,
- contentsControl: !!window.ContentsControl,
- editControl: !!window.EditControl
- };
-
- console.log('๐ Dependency check results:', dependencies);
- return dependencies;
- },
-
- // Safe logging that works even without debug system
- safeLog: function(message, level = 'INFO', component = 'Main', data = {}) {
- console.log(`[${level}] ${component}: ${message}`);
-
- // In strict mode, throw on errors for immediate development feedback
- if (MARKITECT_STRICT_MODE && level === 'ERROR') {
- console.error(`๐จ STRICT MODE: Throwing error for immediate diagnosis`);
- throw new Error(`${component}: ${message}`);
- }
-
- // Try to use debug system if available
- if (window.MarkitectDebugSystem && window.MarkitectDebugSystem.addMessage) {
- try {
- window.MarkitectDebugSystem.addMessage(message, level, component, { ...data, eventType: 'SYSTEM' });
- } catch (error) {
- console.warn('Debug system logging failed:', error);
- if (MARKITECT_STRICT_MODE) {
- throw error; // Fail fast in development
- }
- }
- }
- },
-
- // Safe control initialization with fallbacks
- initializeControl: function(controlClass, controlName, icon = '๐ง') {
- const timeout = setTimeout(() => {
- const message = `${controlName} initialization timed out`;
- console.warn(message);
- if (MARKITECT_STRICT_MODE) {
- throw new Error(message); // Fail fast in development
- }
- }, 5000);
-
- try {
- if (!controlClass) {
- const message = `${controlName} class not available, skipping`;
- this.safeLog(message, MARKITECT_STRICT_MODE ? 'ERROR' : 'WARNING');
- clearTimeout(timeout);
- return null;
- }
-
- const controlInstance = new controlClass();
- if (!controlInstance || typeof controlInstance.createControl !== 'function') {
- throw new Error(`Invalid ${controlName} instance`);
- }
-
- const element = controlInstance.createControl();
- if (!element) {
- throw new Error(`${controlName} failed to create element`);
- }
-
- clearTimeout(timeout);
- this.safeLog(`${controlName} initialized successfully`, 'SUCCESS');
- return controlInstance;
-
- } catch (error) {
- clearTimeout(timeout);
- this.safeLog(`${controlName} initialization failed: ${error.message}`, 'ERROR');
-
- // Create minimal fallback control if core Control class exists
- if (window.Control && controlName === 'StatusControl') {
- return this.createFallbackControl(controlName, icon);
- }
-
- return null;
- }
- },
-
- // Create minimal fallback control for essential controls
- createFallbackControl: function(name, icon) {
- try {
- const fallback = Object.create(window.Control);
- fallback.config = {
- icon: icon,
- title: `${name} (Fallback)`,
- className: `${name.toLowerCase()}-fallback`,
- defaultContent: `${name} is running in fallback mode due to initialization issues.`,
- ariaLabel: `${name} Fallback Control`,
- position: 'e'
- };
-
- const element = fallback.createControl();
- if (element) {
- this.safeLog(`${name} fallback control created`, 'INFO');
- return { control: fallback };
- }
- } catch (error) {
- this.safeLog(`Fallback control creation failed: ${error.message}`, 'ERROR');
- }
- return null;
- },
-
- // Main initialization with comprehensive error handling
- initialize: function() {
- this.safeLog('๐ Initializing Markitect controls and systems...', 'INFO');
-
- // Check dependencies first
- const deps = this.checkDependencies();
-
- if (!deps.control) {
- this.safeLog('โ Core Control system not available, cannot initialize UI controls', 'ERROR');
- return;
- }
-
- const initializedControls = {};
- let successCount = 0;
- let totalAttempts = 0;
-
- // Initialize controls with graceful degradation
- const controlsToInit = [
- { class: window.StatusControl, name: 'StatusControl', key: 'statusControl', icon: '๐', essential: true },
- { class: window.DebugControl, name: 'DebugControl', key: 'debugControl', icon: '๐ชฒ', essential: false },
- { class: window.ContentsControl, name: 'ContentsControl', key: 'contentsControl', icon: 'โฐ', essential: false },
- { class: window.EditControl, name: 'EditControl', key: 'editControl', icon: 'โ๏ธ', essential: false }
- ];
-
- controlsToInit.forEach(({ class: controlClass, name, key, icon, essential }) => {
- totalAttempts++;
- const instance = this.initializeControl(controlClass, name, icon);
-
- if (instance) {
- initializedControls[key] = instance.control || instance;
- window[key] = initializedControls[key];
- successCount++;
- } else if (essential) {
- this.safeLog(`Essential control ${name} failed to initialize`, 'ERROR');
- }
- });
-
- // Report initialization results
- const successRate = Math.round((successCount / totalAttempts) * 100);
- if (successCount === totalAttempts) {
- this.safeLog('โ
All controls initialized successfully', 'SUCCESS');
- } else if (successCount > 0) {
- this.safeLog(`โ ๏ธ Partial initialization: ${successCount}/${totalAttempts} controls (${successRate}%) initialized`, 'WARNING');
- } else {
- this.safeLog('โ No controls could be initialized', 'ERROR');
- }
-
- // Set up global error handlers for runtime protection
- this.setupErrorHandlers();
-
- this.safeLog(`โ
Markitect initialization complete (${successCount}/${totalAttempts} controls active)`, 'INFO');
- },
-
- // Set up global error handlers
- setupErrorHandlers: function() {
- // Catch unhandled errors
- window.addEventListener('error', (event) => {
- this.safeLog(`Unhandled error: ${event.message} at ${event.filename}:${event.lineno}`, 'ERROR');
- });
-
- // Catch unhandled promise rejections
- window.addEventListener('unhandledrejection', (event) => {
- this.safeLog(`Unhandled promise rejection: ${event.reason}`, 'ERROR');
- event.preventDefault(); // Prevent console spam
- });
- }
-};
-
-// Initialize when DOM is ready with additional safety
-if (document.readyState === 'loading') {
- document.addEventListener('DOMContentLoaded', () => {
- setTimeout(() => MarkitectMain.initialize(), 100); // Brief delay for dependencies
- });
-} else {
- // DOM already loaded
- setTimeout(() => MarkitectMain.initialize(), 100);
-}
\ No newline at end of file
diff --git a/markitect/static/js/plugins/document-navigator-plugin.js b/markitect/static/js/plugins/document-navigator-plugin.js
deleted file mode 100644
index e95907cf..00000000
--- a/markitect/static/js/plugins/document-navigator-plugin.js
+++ /dev/null
@@ -1,207 +0,0 @@
-/**
- * DocumentNavigator Plugin Definition
- *
- * Plugin definition for the Substack-style document navigation widget.
- * Provides floating table of contents with smooth scrolling and scroll spy.
- */
-export default {
- name: 'DocumentNavigator',
- version: '1.0.0',
- description: 'Substack-style floating document navigation with table of contents',
- author: 'Markitect Core',
- category: 'navigation',
-
- // Dependencies that must be loaded first
- dependencies: ['UIWidget'],
-
- // Mixins to apply (none required for this widget)
- mixins: [],
-
- // Lazy load the actual widget class
- async load() {
- const { DocumentNavigator } = await import('../widgets/navigation/DocumentNavigator.js');
- return DocumentNavigator;
- },
-
- // Default configuration
- defaultOptions: {
- position: 'left', // 'left' or 'right' side
- collapsed: true, // Start in collapsed state
- autoHide: true, // Hide on mobile devices
- maxHeadingLevel: 3, // Include H1, H2, H3
- enableScrollSpy: true, // Highlight current section
- smoothScroll: true, // Smooth scroll to headings
- animationDuration: 300, // Animation timing in ms
- minHeadings: 2, // Minimum headings to show widget
- theme: 'default', // Theme variant
-
- // Layout options
- width: '280px', // Expanded width
- collapsedWidth: '40px', // Collapsed width
- offset: { // Position offset
- top: '80px',
- side: '20px'
- },
-
- // Accessibility
- enableKeyboard: true, // Keyboard navigation support
- ariaLabel: 'Document Navigation'
- },
-
- // Plugin lifecycle hooks
- async onLoad(instance, options) {
- console.log('DocumentNavigator plugin loaded:', {
- headings: instance.headings.length,
- position: options.position,
- collapsed: options.collapsed
- });
-
- // Auto-initialize after load
- await instance.initialize();
-
- return instance;
- },
-
- async onUnload(instance) {
- console.log('DocumentNavigator plugin unloading');
- await instance.destroy();
- },
-
- // Feature flags and capabilities
- capabilities: {
- draggable: false, // Not draggable (fixed position)
- resizable: false, // Not resizable (fixed width)
- themeable: true, // Supports themes
- persistent: false, // Rebuilds on page changes
- responsive: true, // Responsive behavior
- keyboard: true, // Keyboard accessible
- scrollSpy: true, // Scroll spy functionality
- smoothScroll: true // Smooth scroll navigation
- },
-
- // Integration requirements
- requirements: {
- container: true, // Requires container element
- headings: true, // Requires document headings
- scrollable: true // Requires scrollable content
- },
-
- // Event types emitted by this widget
- events: [
- 'rendered', // Widget rendered to DOM
- 'navigate', // User navigated to heading
- 'toggle', // Widget expanded/collapsed
- 'theme-changed', // Theme was changed
- 'destroyed' // Widget was destroyed
- ],
-
- // CSS classes used by this widget
- cssClasses: [
- 'document-navigator', // Main widget class
- 'navigator-toggle', // Toggle button
- 'navigator-list', // Navigation list
- 'navigator-item', // Navigation items
- 'navigator-link', // Navigation links
- 'navigator-header', // List header
- 'navigator-close', // Close button
- 'navigator-empty' // Empty state
- ],
-
- // Theme variants
- themes: {
- default: {
- backgroundColor: 'rgba(255, 255, 255, 0.95)',
- borderColor: '#e1e5e9',
- textColor: '#333',
- activeColor: '#1976d2',
- activeBackground: '#e3f2fd'
- },
- dark: {
- backgroundColor: 'rgba(45, 45, 45, 0.95)',
- borderColor: '#555',
- textColor: '#e0e0e0',
- activeColor: '#64b5f6',
- activeBackground: '#1e3a8a'
- },
- minimal: {
- backgroundColor: 'rgba(248, 249, 250, 0.90)',
- borderColor: '#dee2e6',
- textColor: '#495057',
- activeColor: '#007bff',
- activeBackground: '#e7f1ff'
- }
- },
-
- // Usage examples
- examples: {
- basic: {
- description: 'Basic document navigator on the left side',
- code: `
- const navigator = await widgetSystem.createWidget('DocumentNavigator');
- await navigator.show();
- `
- },
- customized: {
- description: 'Customized navigator with specific options',
- code: `
- const navigator = await widgetSystem.createWidget('DocumentNavigator', {
- position: 'right',
- collapsed: false,
- maxHeadingLevel: 4,
- theme: 'dark'
- });
- await navigator.show();
- `
- },
- withContainer: {
- description: 'Navigator for specific container content',
- code: `
- const container = document.getElementById('article-content');
- const navigator = await widgetSystem.createWidget('DocumentNavigator', {
- container: container,
- minHeadings: 1
- });
- await navigator.show();
- `
- }
- },
-
- // Development and testing helpers
- dev: {
- testHeadingStructure() {
- // Helper to create test content with headings
- const testContent = `
- Chapter 1: Introduction
- Lorem ipsum content...
- Section 1.1: Overview
- Subsection 1.1.1: Details
- Section 1.2: Implementation
- Chapter 2: Advanced Topics
- Section 2.1: Performance
- `;
-
- const container = document.createElement('div');
- container.innerHTML = testContent;
- container.style.cssText = 'height: 2000px; padding: 2rem;';
- document.body.appendChild(container);
-
- return container;
- },
-
- async createTestInstance(options = {}) {
- // Helper to create test instance with sample content
- const container = this.testHeadingStructure();
-
- const navigator = new (await this.load())({
- container,
- collapsed: false,
- ...options
- });
-
- await navigator.initialize();
- await navigator.render();
-
- return { navigator, container };
- }
- }
-};
\ No newline at end of file
diff --git a/markitect/static/js/tests/refactor-test-runner.js b/markitect/static/js/tests/refactor-test-runner.js
deleted file mode 100644
index ecc97529..00000000
--- a/markitect/static/js/tests/refactor-test-runner.js
+++ /dev/null
@@ -1,216 +0,0 @@
-#!/usr/bin/env node
-
-/**
- * TDD Test Runner for JavaScript Refactoring
- *
- * Drives component extraction and testing during architecture refactoring.
- * Ensures all functionality remains stable while achieving separation of concerns.
- */
-
-class RefactorTestRunner {
- constructor() {
- this.tests = [];
- this.passed = 0;
- this.failed = 0;
- this.currentSuite = null;
- this.setupDOM();
- }
-
- setupDOM() {
- // Set up minimal DOM environment for testing
- if (typeof document === 'undefined') {
- const { JSDOM } = require('jsdom');
- const dom = new JSDOM('', {
- url: 'http://localhost',
- pretendToBeVisual: true,
- resources: 'usable'
- });
-
- global.window = dom.window;
- global.document = dom.window.document;
- global.HTMLElement = dom.window.HTMLElement;
- global.Event = dom.window.Event;
- global.CustomEvent = dom.window.CustomEvent;
-
- // Only set navigator if it doesn't exist
- if (typeof global.navigator === 'undefined') {
- global.navigator = dom.window.navigator;
- }
- }
- }
-
- describe(suiteName, fn) {
- console.log(`\n๐ ${suiteName}`);
- this.currentSuite = suiteName;
- fn();
- this.currentSuite = null;
- }
-
- it(testName, fn) {
- const fullName = this.currentSuite ? `${this.currentSuite}: ${testName}` : testName;
-
- try {
- fn();
- console.log(` โ
${testName}`);
- this.passed++;
- } catch (error) {
- console.log(` โ ${testName}`);
- console.log(` Error: ${error.message}`);
- if (error.stack) {
- console.log(` Stack: ${error.stack.split('\n')[1]?.trim()}`);
- }
- this.failed++;
- }
- }
-
- expect(actual) {
- return {
- toBe: (expected) => {
- if (actual !== expected) {
- throw new Error(`Expected ${expected}, got ${actual}`);
- }
- },
- toBeTruthy: () => {
- if (!actual) {
- throw new Error(`Expected truthy value, got ${actual}`);
- }
- },
- toBeFalsy: () => {
- if (actual) {
- throw new Error(`Expected falsy value, got ${actual}`);
- }
- },
- toEqual: (expected) => {
- if (JSON.stringify(actual) !== JSON.stringify(expected)) {
- throw new Error(`Expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`);
- }
- },
- toContain: (expected) => {
- if (!actual.includes(expected)) {
- throw new Error(`Expected ${actual} to contain ${expected}`);
- }
- },
- toHaveProperty: (property) => {
- if (!(property in actual)) {
- throw new Error(`Expected object to have property ${property}`);
- }
- },
- toBeInstanceOf: (expectedClass) => {
- if (!(actual instanceof expectedClass)) {
- throw new Error(`Expected instance of ${expectedClass.name}, got ${actual.constructor.name}`);
- }
- }
- };
- }
-
- /**
- * Test that a component can be extracted from the monolith without breaking functionality
- */
- testComponentExtraction(componentName, extractFn, originalTests) {
- this.describe(`Component Extraction: ${componentName}`, () => {
- this.it('should extract without syntax errors', () => {
- try {
- const component = extractFn();
- this.expect(component).toBeTruthy();
- } catch (error) {
- throw new Error(`Component extraction failed: ${error.message}`);
- }
- });
-
- this.it('should maintain original API', () => {
- const component = extractFn();
- originalTests.forEach(test => {
- try {
- test(component);
- } catch (error) {
- throw new Error(`API compatibility test failed: ${error.message}`);
- }
- });
- });
- });
- }
-
- /**
- * Test component integration after extraction
- */
- testComponentIntegration(components, integrationTests) {
- this.describe('Component Integration', () => {
- integrationTests.forEach((test, index) => {
- this.it(`integration test ${index + 1}`, () => {
- test(components);
- });
- });
- });
- }
-
- /**
- * Setup test environment with mock dependencies
- */
- setupTestEnvironment() {
- // Create test container
- const container = document.createElement('div');
- container.id = 'test-container';
- container.innerHTML = '
';
- document.body.appendChild(container);
-
- // Mock any global dependencies
- global.mockSectionManager = {
- sections: new Map(),
- createSectionsFromMarkdown: () => [],
- startEditing: () => true,
- stopEditing: () => true,
- getAllSections: () => []
- };
-
- return { container };
- }
-
- /**
- * Cleanup test environment
- */
- cleanupTestEnvironment() {
- const container = document.getElementById('test-container');
- if (container) {
- container.remove();
- }
-
- // Clear any global mocks
- delete global.mockSectionManager;
- }
-
- async run() {
- console.log('๐งช TDD Refactoring Test Runner Starting...\n');
-
- const startTime = Date.now();
-
- // Run all collected tests
- // Tests will be added by importing component test files
-
- const endTime = Date.now();
- const duration = endTime - startTime;
-
- console.log(`\n๐ Test Results:`);
- console.log(` โ
Passed: ${this.passed}`);
- console.log(` โ Failed: ${this.failed}`);
- console.log(` โฑ๏ธ Duration: ${duration}ms`);
-
- if (this.failed > 0) {
- console.log(`\nโ ${this.failed} test(s) failed. Refactoring should not proceed.`);
- process.exit(1);
- } else {
- console.log(`\nโ
All tests passed! Refactoring is safe to continue.`);
- }
- }
-}
-
-// Export for use in component tests
-if (typeof module !== 'undefined' && module.exports) {
- module.exports = { RefactorTestRunner };
-}
-
-// Export for browser use
-if (typeof window !== 'undefined') {
- window.RefactorTestRunner = RefactorTestRunner;
-}
-
-module.exports = RefactorTestRunner;
\ No newline at end of file
diff --git a/markitect/static/js/tests/test-component-integration.js b/markitect/static/js/tests/test-component-integration.js
deleted file mode 100644
index 2107dc99..00000000
--- a/markitect/static/js/tests/test-component-integration.js
+++ /dev/null
@@ -1,521 +0,0 @@
-#!/usr/bin/env node
-
-/**
- * Comprehensive Component Integration Test
- *
- * Tests that extracted components work together properly.
- * Verifies the complete workflow: Section Creation โ Rendering โ Editing โ Saving
- */
-
-const RefactorTestRunner = require('./refactor-test-runner.js');
-
-const runner = new RefactorTestRunner();
-
-runner.describe('Component Integration Tests', () => {
-
- runner.it('should load all extracted components', () => {
- try {
- // Load extracted components
- const sectionModule = require('../core/section-manager.js');
- const domModule = require('../components/dom-renderer.js');
-
- runner.expect(sectionModule.SectionManager).toBeTruthy();
- runner.expect(sectionModule.Section).toBeTruthy();
- runner.expect(domModule.DOMRenderer).toBeTruthy();
- runner.expect(domModule.FloatingMenu).toBeTruthy();
-
- // Set globals for other tests
- global.ExtractedSectionManager = sectionModule.SectionManager;
- global.ExtractedSection = sectionModule.Section;
- global.ExtractedDOMRenderer = domModule.DOMRenderer;
- global.ExtractedFloatingMenu = domModule.FloatingMenu;
- global.ExtractedEditState = sectionModule.EditState;
-
- } catch (error) {
- throw new Error(`Failed to load extracted components: ${error.message}`);
- }
- });
-
- runner.it('should support complete section creation workflow', () => {
- const SectionManager = global.ExtractedSectionManager;
- const DOMRenderer = global.ExtractedDOMRenderer;
-
- // Setup
- const container = document.createElement('div');
- container.innerHTML = '
';
- document.body.appendChild(container);
-
- const sectionManager = new SectionManager();
- const domRenderer = new DOMRenderer(sectionManager, container);
-
- // Test workflow: Create sections from markdown
- const testMarkdown = `# Main Heading
-This is the introduction content.
-
-## Subheading One
-Content for first subsection.
-
-
-
-## Subheading Two
-Content for second subsection.`;
-
- const sections = sectionManager.createSectionsFromMarkdown(testMarkdown);
-
-
- // Verify sections were created
- // Expected: heading+paragraph, heading+paragraph, image, heading+paragraph = 4 sections
- runner.expect(sections.length).toBe(4);
- runner.expect(sections[0].type).toBe('heading');
- runner.expect(sections[2].type).toBe('image');
-
- // Verify DOM rendering
- domRenderer.renderAllSections(sections);
- const renderedElements = container.querySelectorAll('.ui-edit-section');
- runner.expect(renderedElements.length).toBe(sections.length);
-
- // Cleanup
- document.body.removeChild(container);
- });
-
- runner.it('should support complete editing workflow', () => {
- const SectionManager = global.ExtractedSectionManager;
- const DOMRenderer = global.ExtractedDOMRenderer;
- const EditState = global.ExtractedEditState;
-
- // Setup
- const container = document.createElement('div');
- container.innerHTML = '
';
- document.body.appendChild(container);
-
- const sectionManager = new SectionManager();
- const domRenderer = new DOMRenderer(sectionManager, container);
-
- // Create and render sections
- const testMarkdown = '# Test Heading\nOriginal content here.';
- const sections = sectionManager.createSectionsFromMarkdown(testMarkdown);
- domRenderer.renderAllSections(sections);
-
- const sectionId = sections[0].id;
- const section = sectionManager.sections.get(sectionId);
-
- // Test workflow: Start editing
- runner.expect(section.state).toBe(EditState.ORIGINAL);
- runner.expect(section.isEditing()).toBeFalsy();
-
- const content = sectionManager.startEditing(sectionId);
- runner.expect(content).toContain('Test Heading');
- runner.expect(section.isEditing()).toBeTruthy();
- runner.expect(section.state).toBe(EditState.EDITING);
-
- // Test workflow: Update content
- const newContent = '# Updated Heading\nModified content here.';
- sectionManager.updateContent(sectionId, newContent);
- runner.expect(section.editingMarkdown).toBe(newContent);
-
- // Test workflow: Accept changes
- sectionManager.acceptChanges(sectionId);
- runner.expect(section.currentMarkdown).toBe(newContent);
- runner.expect(section.state).toBe(EditState.SAVED);
- runner.expect(section.isEditing()).toBeFalsy();
-
- // Cleanup
- document.body.removeChild(container);
- });
-
- runner.it('should support accept/cancel button functionality', () => {
- const SectionManager = global.ExtractedSectionManager;
- const DOMRenderer = global.ExtractedDOMRenderer;
-
- // Setup
- const container = document.createElement('div');
- container.innerHTML = '
';
- document.body.appendChild(container);
-
- const sectionManager = new SectionManager();
- const domRenderer = new DOMRenderer(sectionManager, container);
-
- // Create and render sections
- const testMarkdown = '# Test Heading\nOriginal content here.';
- const sections = sectionManager.createSectionsFromMarkdown(testMarkdown);
- domRenderer.renderAllSections(sections);
-
- const sectionId = sections[0].id;
- const section = sectionManager.sections.get(sectionId);
-
- // Start editing to trigger floating menu with buttons
- sectionManager.startEditing(sectionId);
-
- // Check if floating menu exists
- runner.expect(domRenderer.currentFloatingMenu).toBeTruthy();
- runner.expect(domRenderer.currentFloatingMenu.isVisible).toBeTruthy();
-
- // Find buttons in the floating menu
- const menuElement = domRenderer.currentFloatingMenu.element;
- runner.expect(menuElement).toBeTruthy();
-
- const buttons = menuElement.querySelectorAll('button');
- runner.expect(buttons.length >= 2).toBeTruthy(); // At least Accept and Cancel buttons
-
- const acceptBtn = Array.from(buttons).find(btn => btn.textContent === 'Accept');
- const cancelBtn = Array.from(buttons).find(btn => btn.textContent === 'Cancel');
-
- runner.expect(acceptBtn).toBeTruthy();
- runner.expect(cancelBtn).toBeTruthy();
-
- // Test Accept button functionality
- runner.expect(section.isEditing()).toBeTruthy();
-
- // Simulate updating content and clicking Accept
- const textarea = menuElement.querySelector('textarea');
- runner.expect(textarea).toBeTruthy();
- textarea.value = '# Updated Heading\nUpdated content via button.';
-
- acceptBtn.click();
-
- // After clicking Accept, section should be saved and menu hidden
- runner.expect(section.isEditing()).toBeFalsy();
- runner.expect(section.currentMarkdown).toContain('Updated Heading');
- runner.expect(domRenderer.currentFloatingMenu).toBeFalsy();
-
- // Cleanup
- document.body.removeChild(container);
- });
-
- runner.it('should support cancel button functionality', () => {
- const SectionManager = global.ExtractedSectionManager;
- const DOMRenderer = global.ExtractedDOMRenderer;
-
- // Setup
- const container = document.createElement('div');
- container.innerHTML = '
';
- document.body.appendChild(container);
-
- const sectionManager = new SectionManager();
- const domRenderer = new DOMRenderer(sectionManager, container);
-
- // Create and render sections
- const testMarkdown = '# Original Heading\nOriginal content here.';
- const sections = sectionManager.createSectionsFromMarkdown(testMarkdown);
- domRenderer.renderAllSections(sections);
-
- const sectionId = sections[0].id;
- const section = sectionManager.sections.get(sectionId);
-
- // Start editing
- sectionManager.startEditing(sectionId);
-
- // Find buttons in the floating menu
- const menuElement = domRenderer.currentFloatingMenu.element;
- const cancelBtn = Array.from(menuElement.querySelectorAll('button')).find(btn => btn.textContent === 'Cancel');
-
- runner.expect(cancelBtn).toBeTruthy();
- runner.expect(section.isEditing()).toBeTruthy();
-
- // Simulate changing content but then canceling
- const textarea = menuElement.querySelector('textarea');
- textarea.value = '# Changed Heading\nThis should be discarded.';
-
- cancelBtn.click();
-
- // After clicking Cancel, section should not be saved and menu hidden
- runner.expect(section.isEditing()).toBeFalsy();
- runner.expect(section.currentMarkdown).toContain('Original Heading'); // Original content preserved
- runner.expect(domRenderer.currentFloatingMenu).toBeFalsy();
-
- // Cleanup
- document.body.removeChild(container);
- });
-
- runner.it('should support event-driven communication', () => {
- const SectionManager = global.ExtractedSectionManager;
- const DOMRenderer = global.ExtractedDOMRenderer;
-
- // Setup
- const container = document.createElement('div');
- container.innerHTML = '
';
- document.body.appendChild(container);
-
- const sectionManager = new SectionManager();
- const domRenderer = new DOMRenderer(sectionManager, container);
-
- // Track events
- let sectionsCreatedEvent = null;
- let editStartedEvent = null;
-
- sectionManager.on('sections-created', (data) => {
- sectionsCreatedEvent = data;
- });
-
- sectionManager.on('edit-started', (data) => {
- editStartedEvent = data;
- });
-
- // Test event: sections-created
- const testMarkdown = '# Test\nContent';
- const sections = sectionManager.createSectionsFromMarkdown(testMarkdown);
-
- runner.expect(sectionsCreatedEvent).toBeTruthy();
- runner.expect(sectionsCreatedEvent.sections).toEqual(sections);
- runner.expect(sectionsCreatedEvent.count).toBe(1);
-
- // Test event: edit-started
- const sectionId = sections[0].id;
- sectionManager.startEditing(sectionId);
-
- runner.expect(editStartedEvent).toBeTruthy();
- runner.expect(editStartedEvent.sectionId).toBe(sectionId);
- runner.expect(editStartedEvent.content).toContain('Test');
-
- // Cleanup
- document.body.removeChild(container);
- });
-
- runner.it('should support section type detection and rendering', () => {
- const SectionManager = global.ExtractedSectionManager;
- const DOMRenderer = global.ExtractedDOMRenderer;
- const Section = global.ExtractedSection;
-
- // Setup
- const container = document.createElement('div');
- container.innerHTML = '
';
- document.body.appendChild(container);
-
- const sectionManager = new SectionManager();
- const domRenderer = new DOMRenderer(sectionManager, container);
-
- // Test different section types
- const testMarkdown = `# Heading Section
-Regular paragraph content.
-
-
-
-\`\`\`javascript
-// Code section
-console.log('test');
-\`\`\``;
-
- const sections = sectionManager.createSectionsFromMarkdown(testMarkdown);
-
-
- // Verify type detection - adjusted for actual parsing behavior
- // Expected: heading+paragraph, image, code = 3 sections
- runner.expect(sections[0].type).toBe('heading'); // Combined heading+paragraph
- runner.expect(sections[1].type).toBe('image'); // Image section
- runner.expect(sections[2].type).toBe('code'); // Code section
-
- // Verify image detection
- runner.expect(sections[1].isImage()).toBeTruthy(); // Image is now at index 1
- runner.expect(sections[0].isImage()).toBeFalsy();
-
- // Verify rendering handles different types
- domRenderer.renderAllSections(sections);
- const renderedElements = container.querySelectorAll('.ui-edit-section');
- runner.expect(renderedElements.length).toBe(sections.length);
-
- // Cleanup
- document.body.removeChild(container);
- });
-
- runner.it('should support FloatingMenu integration', () => {
- const SectionManager = global.ExtractedSectionManager;
- const DOMRenderer = global.ExtractedDOMRenderer;
- const FloatingMenu = global.ExtractedFloatingMenu;
-
- // Setup
- const container = document.createElement('div');
- container.innerHTML = '
';
- document.body.appendChild(container);
-
- const sectionManager = new SectionManager();
- const domRenderer = new DOMRenderer(sectionManager, container);
-
- // Create and render sections
- const testMarkdown = '# Test Heading\nTest content';
- const sections = sectionManager.createSectionsFromMarkdown(testMarkdown);
- domRenderer.renderAllSections(sections);
-
- const sectionId = sections[0].id;
-
- // Test showing editor (which uses FloatingMenu)
- domRenderer.showEditor(sectionId, 'test content');
-
- // Verify floating menu state
- runner.expect(domRenderer.currentFloatingMenu).toBeTruthy();
- runner.expect(domRenderer.currentFloatingMenu.sectionId).toBe(sectionId);
- runner.expect(domRenderer.currentFloatingMenu.isVisible).toBeTruthy();
- runner.expect(domRenderer.editingSections.has(sectionId)).toBeTruthy();
-
- // Test hiding editor
- domRenderer.hideCurrentEditor();
- runner.expect(domRenderer.currentFloatingMenu).toBeFalsy();
- runner.expect(domRenderer.editingSections.has(sectionId)).toBeFalsy();
-
- // Cleanup
- document.body.removeChild(container);
- });
-
- runner.it('should support complete click-to-edit workflow', () => {
- const SectionManager = global.ExtractedSectionManager;
- const DOMRenderer = global.ExtractedDOMRenderer;
-
- // Setup
- const container = document.createElement('div');
- container.innerHTML = '
';
- document.body.appendChild(container);
-
- const sectionManager = new SectionManager();
- const domRenderer = new DOMRenderer(sectionManager, container);
-
- // Create and render sections
- const testMarkdown = '# Test Heading\nTest content for editing';
- const sections = sectionManager.createSectionsFromMarkdown(testMarkdown);
- domRenderer.renderAllSections(sections);
-
- const sectionId = sections[0].id;
- const element = domRenderer.findSectionElement(sectionId);
-
- // Simulate click event
- const clickEvent = new Event('click', { bubbles: true });
- Object.defineProperty(clickEvent, 'target', { value: element });
-
- // Test complete workflow
- domRenderer.handleSectionClick(clickEvent);
-
- // Verify editing state was triggered
- const section = sectionManager.sections.get(sectionId);
- runner.expect(section.isEditing()).toBeTruthy();
- runner.expect(domRenderer.editingSections.has(sectionId)).toBeTruthy();
- runner.expect(domRenderer.currentFloatingMenu).toBeTruthy();
-
- // Cleanup
- document.body.removeChild(container);
- });
-
- runner.it('should support document status tracking', () => {
- const SectionManager = global.ExtractedSectionManager;
- const DOMRenderer = global.ExtractedDOMRenderer;
-
- const sectionManager = new SectionManager();
- const container = document.createElement('div');
- const domRenderer = new DOMRenderer(sectionManager, container);
-
- // Test initial status
- let status = sectionManager.getDocumentStatus();
- runner.expect(status.totalSections).toBe(0);
- runner.expect(status.editingSections).toBe(0);
-
- // Create sections
- const testMarkdown = '# Section 1\nContent 1\n\n# Section 2\nContent 2';
- const sections = sectionManager.createSectionsFromMarkdown(testMarkdown);
-
- status = sectionManager.getDocumentStatus();
- runner.expect(status.totalSections).toBe(2);
- runner.expect(status.editingSections).toBe(2); // Bug compatibility (isEditing property exists)
-
- // Test getAllSections
- const allSections = sectionManager.getAllSections();
- runner.expect(allSections.length).toBe(2);
- runner.expect(allSections[0].currentMarkdown).toContain('Section 1');
- runner.expect(allSections[1].currentMarkdown).toContain('Section 2');
- });
-
- runner.it('should support event tracking and analytics', () => {
- const SectionManager = global.ExtractedSectionManager;
- const DOMRenderer = global.ExtractedDOMRenderer;
-
- const container = document.createElement('div');
- const sectionManager = new SectionManager();
- const domRenderer = new DOMRenderer(sectionManager, container);
-
- // Test event tracking
- domRenderer.trackEvent('test-event', { data: 'test' });
- domRenderer.trackEvent('section-click', { sectionId: 'test-123' });
-
- const stats = domRenderer.getEventStats();
- runner.expect(stats.totalEvents).toBe(1); // Only section-click is tracked in stats
- runner.expect(stats.stats['section-click']).toBe(1);
- runner.expect(stats.recentEvents.length).toBe(2);
- runner.expect(stats.recentEvents[0].type).toBe('test-event');
- runner.expect(stats.recentEvents[1].type).toBe('section-click');
- });
-
- // Integration stress test
- runner.it('should handle complex document with multiple operations', () => {
- const SectionManager = global.ExtractedSectionManager;
- const DOMRenderer = global.ExtractedDOMRenderer;
-
- // Setup
- const container = document.createElement('div');
- container.innerHTML = '
';
- document.body.appendChild(container);
-
- const sectionManager = new SectionManager();
- const domRenderer = new DOMRenderer(sectionManager, container);
-
- // Complex document
- const complexMarkdown = `# Document Title
-Introduction paragraph with some content.
-
-## Section A
-Content for section A with details.
-
-
-
-### Subsection A.1
-More detailed content here.
-
-\`\`\`javascript
-function test() {
- console.log('code block');
-}
-\`\`\`
-
-## Section B
-Final section content.`;
-
- // Create and render
- const sections = sectionManager.createSectionsFromMarkdown(complexMarkdown);
- domRenderer.renderAllSections(sections);
-
- runner.expect(sections.length).toBe(6); // Adjusted based on actual parsing
-
- // Test editing multiple sections
- const firstSection = sections[0];
- const imageSection = sections.find(s => s.isImage());
- const codeSection = sections.find(s => s.type === 'code');
-
- // Edit first section
- sectionManager.startEditing(firstSection.id);
- sectionManager.updateContent(firstSection.id, '# Updated Title\nUpdated intro.');
- sectionManager.acceptChanges(firstSection.id);
-
- // Edit image section
- sectionManager.startEditing(imageSection.id);
- sectionManager.updateContent(imageSection.id, '');
- sectionManager.acceptChanges(imageSection.id);
-
- // Verify changes
- runner.expect(firstSection.currentMarkdown).toContain('Updated Title');
- runner.expect(imageSection.currentMarkdown).toContain('Updated Image');
-
- // Verify document reconstruction
- const finalMarkdown = sectionManager.getDocumentMarkdown();
- runner.expect(finalMarkdown).toContain('Updated Title');
- runner.expect(finalMarkdown).toContain('Updated Image');
- runner.expect(finalMarkdown).toContain('Section B');
-
- // Cleanup
- document.body.removeChild(container);
- });
-});
-
-module.exports = runner;
-
-// Run tests if called directly
-if (require.main === module) {
- console.log('๐งช Running Component Integration Tests');
- runner.run().then(() => {
- console.log('โ
Component integration tests completed');
- });
-}
\ No newline at end of file
diff --git a/markitect/static/js/tests/test-debugpanel-extraction.js b/markitect/static/js/tests/test-debugpanel-extraction.js
deleted file mode 100644
index 5dca6cae..00000000
--- a/markitect/static/js/tests/test-debugpanel-extraction.js
+++ /dev/null
@@ -1,191 +0,0 @@
-#!/usr/bin/env node
-
-/**
- * TDD Test for Debug Panel Component Extraction
- *
- * Tests the extraction of DebugPanel from the monolithic editor.js
- * DebugPanel handles debug message display and management.
- */
-
-const RefactorTestRunner = require('./refactor-test-runner.js');
-
-const runner = new RefactorTestRunner();
-
-// Define expected DebugPanel API
-const EXPECTED_DEBUGPANEL_API = [
- 'constructor',
- 'toggle',
- 'update',
- 'clear',
- 'addMessage',
- 'show',
- 'hide',
- 'getMessageCount',
- 'getRecentMessages'
-];
-
-runner.describe('DebugPanel Component Extraction', () => {
-
- runner.it('should define expected API methods', () => {
- const expectedMethods = EXPECTED_DEBUGPANEL_API;
- runner.expect(expectedMethods.length).toBe(9);
- runner.expect(expectedMethods).toContain('toggle');
- runner.expect(expectedMethods).toContain('update');
- runner.expect(expectedMethods).toContain('addMessage');
- });
-
- runner.it('should load extracted DebugPanel component', () => {
- // Load the extracted component
- delete require.cache[require.resolve('../components/debug-panel.js')];
-
- try {
- const module = require('../components/debug-panel.js');
- runner.expect(module.DebugPanel).toBeTruthy();
-
- // Set global for other tests
- global.ExtractedDebugPanel = module.DebugPanel;
- } catch (error) {
- throw new Error(`Failed to load extracted DebugPanel: ${error.message}`);
- }
- });
-
- runner.it('should preserve constructor functionality', () => {
- const DebugPanel = global.ExtractedDebugPanel;
-
- const debugPanel = new DebugPanel();
- runner.expect(debugPanel).toBeInstanceOf(DebugPanel);
- runner.expect(debugPanel.messages).toBeInstanceOf(Array);
- runner.expect(debugPanel.isActive).toBeFalsy();
- });
-
- runner.it('should preserve message handling functionality', () => {
- const DebugPanel = global.ExtractedDebugPanel;
-
- const debugPanel = new DebugPanel();
-
- // Test adding messages
- debugPanel.addMessage('Test message', 'INFO');
- runner.expect(debugPanel.getMessageCount()).toBe(1);
-
- const recentMessages = debugPanel.getRecentMessages(1);
- runner.expect(recentMessages.length).toBe(1);
- runner.expect(recentMessages[0].message).toBe('Test message');
- runner.expect(recentMessages[0].category).toBe('INFO');
- });
-
- runner.it('should preserve toggle functionality', () => {
- const DebugPanel = global.ExtractedDebugPanel;
-
- // Create container element
- const container = document.createElement('div');
- container.id = 'debug-messages-container';
- container.style.display = 'none';
- document.body.appendChild(container);
-
- const debugButton = document.createElement('button');
- debugButton.id = 'toggle-debug';
- debugButton.textContent = '๐ Debug';
- document.body.appendChild(debugButton);
-
- const debugPanel = new DebugPanel();
-
- // Test toggle on
- debugPanel.toggle();
- runner.expect(debugPanel.isActive).toBeTruthy();
-
- // Test toggle off
- debugPanel.toggle();
- runner.expect(debugPanel.isActive).toBeFalsy();
-
- // Cleanup
- document.body.removeChild(container);
- document.body.removeChild(debugButton);
- });
-
- runner.it('should preserve update functionality', () => {
- const DebugPanel = global.ExtractedDebugPanel;
-
- const container = document.createElement('div');
- container.id = 'debug-messages-container';
- document.body.appendChild(container);
-
- const debugButton = document.createElement('button');
- debugButton.id = 'toggle-debug';
- debugButton.textContent = '๐ Debug';
- document.body.appendChild(debugButton);
-
- const debugPanel = new DebugPanel();
- debugPanel.show();
-
- debugPanel.addMessage('Test message 1', 'INFO');
- debugPanel.addMessage('Test message 2', 'ERROR');
- debugPanel.update();
-
- runner.expect(container.innerHTML.length > 100).toBeTruthy();
- runner.expect(container.innerHTML).toContain('Test message 1');
- runner.expect(container.innerHTML).toContain('Test message 2');
-
- // Cleanup
- document.body.removeChild(container);
- document.body.removeChild(debugButton);
- });
-
- runner.it('should preserve clear functionality', () => {
- const DebugPanel = global.ExtractedDebugPanel;
-
- const debugPanel = new DebugPanel();
-
- debugPanel.addMessage('Test message 1', 'INFO');
- debugPanel.addMessage('Test message 2', 'ERROR');
- runner.expect(debugPanel.getMessageCount()).toBe(2);
-
- debugPanel.clear();
- runner.expect(debugPanel.getMessageCount()).toBe(0);
- });
-
- runner.it('should have core debug panel methods', () => {
- const DebugPanel = global.ExtractedDebugPanel;
-
- const debugPanel = new DebugPanel();
-
- // Should have core methods
- runner.expect(typeof debugPanel.toggle === 'function').toBeTruthy();
- runner.expect(typeof debugPanel.update === 'function').toBeTruthy();
- runner.expect(typeof debugPanel.addMessage === 'function').toBeTruthy();
- runner.expect(typeof debugPanel.clear === 'function').toBeTruthy();
- });
-
- runner.it('should handle message categories properly', () => {
- const DebugPanel = global.ExtractedDebugPanel;
-
- const debugPanel = new DebugPanel();
-
- // Test different message categories
- debugPanel.addMessage('Info message', 'INFO');
- debugPanel.addMessage('Warning message', 'WARNING');
- debugPanel.addMessage('Error message', 'ERROR');
- debugPanel.addMessage('Success message', 'SUCCESS');
-
- const messages = debugPanel.getRecentMessages(4);
- runner.expect(messages.length).toBe(4);
-
- const categories = messages.map(m => m.category);
- runner.expect(categories).toContain('INFO');
- runner.expect(categories).toContain('WARNING');
- runner.expect(categories).toContain('ERROR');
- runner.expect(categories).toContain('SUCCESS');
- });
-});
-
-module.exports = {
- runner,
- EXPECTED_DEBUGPANEL_API
-};
-
-// Run tests if called directly
-if (require.main === module) {
- console.log('๐งช Testing DebugPanel Component Extraction');
- runner.run().then(() => {
- console.log('โ
DebugPanel extraction tests completed');
- });
-}
\ No newline at end of file
diff --git a/markitect/static/js/tests/test-debugpanel-integration.js b/markitect/static/js/tests/test-debugpanel-integration.js
deleted file mode 100644
index af03ff83..00000000
--- a/markitect/static/js/tests/test-debugpanel-integration.js
+++ /dev/null
@@ -1,210 +0,0 @@
-#!/usr/bin/env node
-
-/**
- * DebugPanel Integration Test
- *
- * Tests that the extracted DebugPanel component integrates properly
- * with the existing SectionManager and DOMRenderer components.
- */
-
-const RefactorTestRunner = require('./refactor-test-runner.js');
-
-const runner = new RefactorTestRunner();
-
-runner.describe('DebugPanel Integration Tests', () => {
-
- runner.it('should load all extracted components including DebugPanel', () => {
- try {
- // Load extracted components
- const sectionModule = require('../core/section-manager.js');
- const domModule = require('../components/dom-renderer.js');
- const debugModule = require('../components/debug-panel.js');
-
- runner.expect(sectionModule.SectionManager).toBeTruthy();
- runner.expect(domModule.DOMRenderer).toBeTruthy();
- runner.expect(debugModule.DebugPanel).toBeTruthy();
-
- // Set globals for other tests
- global.ExtractedSectionManager = sectionModule.SectionManager;
- global.ExtractedDOMRenderer = domModule.DOMRenderer;
- global.ExtractedDebugPanel = debugModule.DebugPanel;
-
- } catch (error) {
- throw new Error(`Failed to load extracted components: ${error.message}`);
- }
- });
-
- runner.it('should support debug panel with section editing workflow', () => {
- const SectionManager = global.ExtractedSectionManager;
- const DOMRenderer = global.ExtractedDOMRenderer;
- const DebugPanel = global.ExtractedDebugPanel;
-
- // Setup DOM elements
- const container = document.createElement('div');
- container.innerHTML = '
';
- document.body.appendChild(container);
-
- const debugContainer = document.createElement('div');
- debugContainer.id = 'debug-messages-container';
- debugContainer.style.display = 'none';
- document.body.appendChild(debugContainer);
-
- const debugButton = document.createElement('button');
- debugButton.id = 'toggle-debug';
- debugButton.textContent = '๐ Debug';
- document.body.appendChild(debugButton);
-
- // Create components
- const sectionManager = new SectionManager();
- const domRenderer = new DOMRenderer(sectionManager, container);
- const debugPanel = new DebugPanel();
-
- // Test workflow: Create sections and debug them
- const testMarkdown = '# Test Heading\nTest content for debugging';
- const sections = sectionManager.createSectionsFromMarkdown(testMarkdown);
- domRenderer.renderAllSections(sections);
-
- // Add debug messages
- debugPanel.addMessage('Section created: ' + sections[0].id, 'INFO');
- debugPanel.addMessage('DOM rendered successfully', 'SUCCESS');
-
- runner.expect(debugPanel.getMessageCount()).toBe(2);
-
- // Test showing debug panel
- debugPanel.show();
- runner.expect(debugPanel.isActive).toBeTruthy();
-
- // Test debug panel content
- const messages = debugPanel.getRecentMessages(2);
- runner.expect(messages[0].message).toContain('Section created');
- runner.expect(messages[1].message).toContain('DOM rendered');
-
- // Cleanup
- document.body.removeChild(container);
- document.body.removeChild(debugContainer);
- document.body.removeChild(debugButton);
- });
-
- runner.it('should support debug panel clearing and message management', () => {
- const DebugPanel = global.ExtractedDebugPanel;
-
- const debugPanel = new DebugPanel();
-
- // Add multiple messages
- for (let i = 0; i < 10; i++) {
- debugPanel.addMessage(`Test message ${i}`, i % 2 === 0 ? 'INFO' : 'WARNING');
- }
-
- runner.expect(debugPanel.getMessageCount()).toBe(10);
-
- // Test getting recent messages
- const recentFive = debugPanel.getRecentMessages(5);
- runner.expect(recentFive.length).toBe(5);
- runner.expect(recentFive[4].message).toContain('Test message 9');
-
- // Test clearing
- debugPanel.clear();
- runner.expect(debugPanel.getMessageCount()).toBe(0);
- });
-
- runner.it('should handle debug panel DOM integration properly', () => {
- const DebugPanel = global.ExtractedDebugPanel;
-
- // Setup DOM
- const debugContainer = document.createElement('div');
- debugContainer.id = 'debug-messages-container';
- debugContainer.style.display = 'none';
- document.body.appendChild(debugContainer);
-
- const debugButton = document.createElement('button');
- debugButton.id = 'toggle-debug';
- debugButton.textContent = '๐ Debug';
- debugButton.style.background = '#6c757d';
- document.body.appendChild(debugButton);
-
- const debugPanel = new DebugPanel();
-
- // Test initial state
- runner.expect(debugPanel.isActive).toBeFalsy();
- runner.expect(debugContainer.style.display).toBe('none');
-
- // Test toggle on
- debugPanel.toggle();
- runner.expect(debugPanel.isActive).toBeTruthy();
- runner.expect(debugContainer.style.display).toBe('block');
- runner.expect(debugButton.textContent).toContain('Debug (ON)');
-
- // Test toggle off
- debugPanel.toggle();
- runner.expect(debugPanel.isActive).toBeFalsy();
- runner.expect(debugContainer.style.display).toBe('none');
- runner.expect(debugButton.textContent).toBe('๐ Debug');
-
- // Cleanup
- document.body.removeChild(debugContainer);
- document.body.removeChild(debugButton);
- });
-
- runner.it('should handle missing DOM elements gracefully', () => {
- const DebugPanel = global.ExtractedDebugPanel;
-
- const debugPanel = new DebugPanel();
-
- // Try to toggle without DOM elements (should not throw)
- try {
- debugPanel.toggle();
- debugPanel.show();
- debugPanel.hide();
- debugPanel.update();
- runner.expect(true).toBeTruthy(); // If we get here, no errors were thrown
- } catch (error) {
- throw new Error(`DebugPanel should handle missing DOM gracefully: ${error.message}`);
- }
- });
-
- runner.it('should support event-driven debug message addition', () => {
- const SectionManager = global.ExtractedSectionManager;
- const DebugPanel = global.ExtractedDebugPanel;
-
- const sectionManager = new SectionManager();
- const debugPanel = new DebugPanel();
-
- // Listen to section manager events and add debug messages
- let eventCount = 0;
-
- sectionManager.on('sections-created', (data) => {
- debugPanel.addMessage(`Sections created: ${data.count} sections`, 'INFO');
- eventCount++;
- });
-
- sectionManager.on('edit-started', (data) => {
- debugPanel.addMessage(`Edit started for section: ${data.sectionId}`, 'DEBUG');
- eventCount++;
- });
-
- // Create sections
- const testMarkdown = '# Test\nContent';
- const sections = sectionManager.createSectionsFromMarkdown(testMarkdown);
-
- // Start editing
- sectionManager.startEditing(sections[0].id);
-
- // Verify debug messages were added
- runner.expect(eventCount).toBe(2);
- runner.expect(debugPanel.getMessageCount()).toBe(2);
-
- const messages = debugPanel.getRecentMessages(2);
- runner.expect(messages[0].message).toContain('Sections created');
- runner.expect(messages[1].message).toContain('Edit started');
- });
-});
-
-module.exports = runner;
-
-// Run tests if called directly
-if (require.main === module) {
- console.log('๐งช Running DebugPanel Integration Tests');
- runner.run().then(() => {
- console.log('โ
DebugPanel integration tests completed');
- });
-}
\ No newline at end of file
diff --git a/markitect/static/js/tests/test-document-navigator-runner.html b/markitect/static/js/tests/test-document-navigator-runner.html
deleted file mode 100644
index 2c8a7621..00000000
--- a/markitect/static/js/tests/test-document-navigator-runner.html
+++ /dev/null
@@ -1,193 +0,0 @@
-
-
-
-
-
- DocumentNavigator TDD Test Runner
-
-
-
-
-
-
-
-
-
-
-
-
Test Chapter 1
-
Sample content for testing heading extraction.
-
Section 1.1
-
Subsection 1.1.1
-
More sample content.
-
Section 1.2
-
Test Chapter 2
-
-
-
\ No newline at end of file
diff --git a/markitect/static/js/tests/test-document-navigator.js b/markitect/static/js/tests/test-document-navigator.js
deleted file mode 100644
index e6a79f99..00000000
--- a/markitect/static/js/tests/test-document-navigator.js
+++ /dev/null
@@ -1,432 +0,0 @@
-/**
- * TDD Test Suite for DocumentNavigator Widget
- *
- * Tests the Substack-style floating navigation widget for document headings.
- * Following TDD methodology: write tests first, then implement functionality.
- */
-
-// Simple test runner for browser environment
-class DocumentNavigatorTestRunner {
- constructor() {
- this.tests = [];
- this.results = {
- passed: 0,
- failed: 0,
- total: 0
- };
- }
-
- test(name, testFn) {
- this.tests.push({ name, testFn });
- }
-
- expect(actual) {
- return {
- toBe: (expected) => {
- if (actual !== expected) {
- throw new Error(`Expected ${actual} to be ${expected}`);
- }
- },
- toBeInstanceOf: (expectedClass) => {
- if (!(actual instanceof expectedClass)) {
- throw new Error(`Expected ${actual} to be instance of ${expectedClass.name}`);
- }
- },
- toBeTruthy: () => {
- if (!actual) {
- throw new Error(`Expected ${actual} to be truthy`);
- }
- },
- toBeFalsy: () => {
- if (actual) {
- throw new Error(`Expected ${actual} to be falsy`);
- }
- },
- toContain: (expected) => {
- if (typeof actual === 'string' && !actual.includes(expected)) {
- throw new Error(`Expected "${actual}" to contain "${expected}"`);
- }
- if (Array.isArray(actual) && !actual.includes(expected)) {
- throw new Error(`Expected array to contain ${expected}`);
- }
- },
- toHaveLength: (expected) => {
- if (actual.length !== expected) {
- throw new Error(`Expected length ${actual.length} to be ${expected}`);
- }
- },
- toBeGreaterThan: (expected) => {
- if (actual <= expected) {
- throw new Error(`Expected ${actual} to be greater than ${expected}`);
- }
- }
- };
- }
-
- async run() {
- console.log('๐งช Running DocumentNavigator TDD Test Suite...\n');
-
- for (const { name, testFn } of this.tests) {
- this.results.total++;
-
- try {
- await testFn.call(this);
- this.results.passed++;
- console.log(`โ
${name}`);
- } catch (error) {
- this.results.failed++;
- console.log(`โ ${name}`);
- console.log(` ${error.message}\n`);
- }
- }
-
- this.printSummary();
- }
-
- printSummary() {
- console.log(`\n๐ Test Results:`);
- console.log(` Passed: ${this.results.passed}`);
- console.log(` Failed: ${this.results.failed}`);
- console.log(` Total: ${this.results.total}`);
-
- if (this.results.failed === 0) {
- console.log(`\n๐ All tests passed!`);
- } else {
- console.log(`\nโ ${this.results.failed} test(s) failed.`);
- }
- }
-}
-
-// Create test runner
-const runner = new DocumentNavigatorTestRunner();
-
-// Test Suite: DocumentNavigator Widget
-runner.test('DocumentNavigator class should exist and be importable', async function() {
- // This test will fail initially - we haven't created the class yet
- try {
- const { DocumentNavigator } = await import('../widgets/navigation/DocumentNavigator.js');
- this.expect(DocumentNavigator).toBeTruthy();
- this.expect(typeof DocumentNavigator).toBe('function');
- } catch (error) {
- throw new Error(`DocumentNavigator class not found: ${error.message}`);
- }
-});
-
-runner.test('DocumentNavigator should extend UIWidget', async function() {
- const { DocumentNavigator } = await import('../widgets/navigation/DocumentNavigator.js');
- const { UIWidget } = await import('../widgets/base/UIWidget.js');
-
- const navigator = new DocumentNavigator();
- this.expect(navigator).toBeInstanceOf(UIWidget);
-});
-
-runner.test('DocumentNavigator should initialize with default configuration', async function() {
- const { DocumentNavigator } = await import('../widgets/navigation/DocumentNavigator.js');
-
- const navigator = new DocumentNavigator();
-
- // Test default configuration
- this.expect(navigator.config.position).toBe('left');
- this.expect(navigator.config.collapsed).toBe(true);
- this.expect(navigator.config.autoHide).toBe(true);
- this.expect(navigator.config.maxHeadingLevel).toBe(3);
- this.expect(navigator.config.enableScrollSpy).toBe(true);
-});
-
-runner.test('DocumentNavigator should accept custom configuration', async function() {
- const { DocumentNavigator } = await import('../widgets/navigation/DocumentNavigator.js');
-
- const customConfig = {
- position: 'right',
- collapsed: false,
- maxHeadingLevel: 4,
- theme: 'dark'
- };
-
- const navigator = new DocumentNavigator(customConfig);
-
- this.expect(navigator.config.position).toBe('right');
- this.expect(navigator.config.collapsed).toBe(false);
- this.expect(navigator.config.maxHeadingLevel).toBe(4);
- this.expect(navigator.config.theme).toBe('dark');
-});
-
-runner.test('DocumentNavigator should render floating panel element', async function() {
- const { DocumentNavigator } = await import('../widgets/navigation/DocumentNavigator.js');
-
- const navigator = new DocumentNavigator();
- await navigator.render();
-
- this.expect(navigator.element).toBeInstanceOf(HTMLElement);
- this.expect(navigator.element.classList.contains('document-navigator')).toBeTruthy();
- this.expect(navigator.element.style.position).toBe('fixed');
-});
-
-runner.test('DocumentNavigator should have toggle button in collapsed state', async function() {
- const { DocumentNavigator } = await import('../widgets/navigation/DocumentNavigator.js');
-
- const navigator = new DocumentNavigator({ collapsed: true });
- await navigator.render();
-
- const toggleButton = navigator.findElement('.navigator-toggle');
- this.expect(toggleButton).toBeInstanceOf(HTMLElement);
- this.expect(toggleButton.style.display).not.toBe('none');
-
- const navList = navigator.findElement('.navigator-list');
- this.expect(navList.style.display).toBe('none');
-});
-
-runner.test('DocumentNavigator should extract headings from document', async function() {
- const { DocumentNavigator } = await import('../widgets/navigation/DocumentNavigator.js');
-
- // Create test document with headings
- const testContainer = document.createElement('div');
- testContainer.innerHTML = `
- First Heading
- Some content
- Second Heading
- Third Heading
- More content
- Fourth Heading
- `;
- document.body.appendChild(testContainer);
-
- const navigator = new DocumentNavigator({
- container: testContainer,
- maxHeadingLevel: 3
- });
-
- const headings = navigator.extractHeadings();
-
- this.expect(headings).toHaveLength(4);
- this.expect(headings[0].tagName).toBe('H1');
- this.expect(headings[0].textContent).toBe('First Heading');
- this.expect(headings[1].tagName).toBe('H2');
- this.expect(headings[2].tagName).toBe('H3');
- this.expect(headings[3].tagName).toBe('H2');
-
- // Cleanup
- document.body.removeChild(testContainer);
-});
-
-runner.test('DocumentNavigator should build navigation hierarchy', async function() {
- const { DocumentNavigator } = await import('../widgets/navigation/DocumentNavigator.js');
-
- // Create test document with nested headings
- const testContainer = document.createElement('div');
- testContainer.innerHTML = `
- Chapter 1
- Section 1.1
- Subsection 1.1.1
- Subsection 1.1.2
- Section 1.2
- Chapter 2
- `;
- document.body.appendChild(testContainer);
-
- const navigator = new DocumentNavigator({ container: testContainer });
- await navigator.render();
-
- const navItems = navigator.buildNavigationTree();
-
- // Should have hierarchical structure
- this.expect(navItems).toHaveLength(2); // 2 H1 elements
- this.expect(navItems[0].children).toHaveLength(2); // 2 H2 under first H1
- this.expect(navItems[0].children[0].children).toHaveLength(2); // 2 H3 under first H2
-
- // Cleanup
- document.body.removeChild(testContainer);
-});
-
-runner.test('DocumentNavigator should handle click navigation', async function() {
- const { DocumentNavigator } = await import('../widgets/navigation/DocumentNavigator.js');
-
- // Create test document
- const testContainer = document.createElement('div');
- testContainer.innerHTML = `
- Target Heading
- Spacer content
- `;
- document.body.appendChild(testContainer);
-
- const navigator = new DocumentNavigator({ container: testContainer });
- await navigator.render();
-
- // Simulate click on navigation item
- const navItem = navigator.findElement('[data-target="target-heading"]');
- this.expect(navItem).toBeTruthy();
-
- // Mock scrollIntoView for testing
- const targetElement = document.getElementById('target-heading');
- let scrollCalled = false;
- targetElement.scrollIntoView = () => { scrollCalled = true; };
-
- // Click navigation item
- navItem.click();
-
- this.expect(scrollCalled).toBeTruthy();
-
- // Cleanup
- document.body.removeChild(testContainer);
-});
-
-runner.test('DocumentNavigator should support expand/collapse functionality', async function() {
- const { DocumentNavigator } = await import('../widgets/navigation/DocumentNavigator.js');
-
- const navigator = new DocumentNavigator({ collapsed: true });
- await navigator.render();
-
- // Should start collapsed
- this.expect(navigator.isCollapsed).toBeTruthy();
-
- const toggleButton = navigator.findElement('.navigator-toggle');
- const navList = navigator.findElement('.navigator-list');
-
- // Toggle to expanded
- await navigator.expand();
- this.expect(navigator.isCollapsed).toBeFalsy();
- this.expect(navList.style.display).not.toBe('none');
-
- // Toggle back to collapsed
- await navigator.collapse();
- this.expect(navigator.isCollapsed).toBeTruthy();
- this.expect(navList.style.display).toBe('none');
-});
-
-runner.test('DocumentNavigator should implement scroll spy functionality', async function() {
- const { DocumentNavigator } = await import('../widgets/navigation/DocumentNavigator.js');
-
- // Create test document with multiple sections
- const testContainer = document.createElement('div');
- testContainer.innerHTML = `
-
- Section 1
-
- Section 2
-
- Section 3
-
- `;
- document.body.appendChild(testContainer);
-
- const navigator = new DocumentNavigator({
- container: testContainer,
- enableScrollSpy: true
- });
- await navigator.render();
-
- // Test current section detection
- const currentSection = navigator.getCurrentSection();
- this.expect(currentSection).toBeTruthy();
-
- // Cleanup
- document.body.removeChild(testContainer);
-});
-
-runner.test('DocumentNavigator should handle responsive behavior', async function() {
- const { DocumentNavigator } = await import('../widgets/navigation/DocumentNavigator.js');
-
- const navigator = new DocumentNavigator({ autoHide: true });
- await navigator.render();
-
- // Mock viewport resize
- const originalInnerWidth = window.innerWidth;
-
- // Test mobile viewport
- Object.defineProperty(window, 'innerWidth', { value: 500, configurable: true });
- navigator.handleResize();
- this.expect(navigator.element.style.display).toBe('none');
-
- // Test desktop viewport
- Object.defineProperty(window, 'innerWidth', { value: 1200, configurable: true });
- navigator.handleResize();
- this.expect(navigator.element.style.display).not.toBe('none');
-
- // Restore original
- Object.defineProperty(window, 'innerWidth', { value: originalInnerWidth, configurable: true });
-});
-
-runner.test('DocumentNavigator should provide keyboard navigation support', async function() {
- const { DocumentNavigator } = await import('../widgets/navigation/DocumentNavigator.js');
-
- const navigator = new DocumentNavigator();
- await navigator.render();
-
- // Test keyboard shortcuts
- let expandCalled = false;
- let collapseCalled = false;
-
- navigator.expand = async () => { expandCalled = true; };
- navigator.collapse = async () => { collapseCalled = true; };
-
- // Simulate keyboard events
- const element = navigator.element;
-
- // Test Escape key (should collapse)
- const escapeEvent = new KeyboardEvent('keydown', { key: 'Escape' });
- element.dispatchEvent(escapeEvent);
- this.expect(collapseCalled).toBeTruthy();
-
- // Test Enter/Space key (should expand)
- const enterEvent = new KeyboardEvent('keydown', { key: 'Enter' });
- element.dispatchEvent(enterEvent);
- this.expect(expandCalled).toBeTruthy();
-});
-
-runner.test('DocumentNavigator should emit events for user interactions', async function() {
- const { DocumentNavigator } = await import('../widgets/navigation/DocumentNavigator.js');
-
- const navigator = new DocumentNavigator();
- await navigator.render();
-
- // Test event emission
- let navigationEvent = null;
- navigator.addEventListener('navigate', (e) => {
- navigationEvent = e;
- });
-
- let toggleEvent = null;
- navigator.addEventListener('toggle', (e) => {
- toggleEvent = e;
- });
-
- // Trigger navigation
- navigator.navigateToHeading('test-heading');
- this.expect(navigationEvent).toBeTruthy();
- this.expect(navigationEvent.detail.target).toBe('test-heading');
-
- // Trigger toggle
- await navigator.toggle();
- this.expect(toggleEvent).toBeTruthy();
-});
-
-runner.test('DocumentNavigator should handle empty document gracefully', async function() {
- const { DocumentNavigator } = await import('../widgets/navigation/DocumentNavigator.js');
-
- // Create empty container
- const emptyContainer = document.createElement('div');
- document.body.appendChild(emptyContainer);
-
- const navigator = new DocumentNavigator({ container: emptyContainer });
-
- const headings = navigator.extractHeadings();
- this.expect(headings).toHaveLength(0);
-
- await navigator.render();
- const navList = navigator.findElement('.navigator-list');
- this.expect(navList.children).toHaveLength(0);
-
- // Should show empty state message
- const emptyMessage = navigator.findElement('.navigator-empty');
- this.expect(emptyMessage).toBeTruthy();
-
- // Cleanup
- document.body.removeChild(emptyContainer);
-});
-
-// Export test runner for use in HTML
-window.runDocumentNavigatorTests = () => runner.run();
-
-console.log('๐ DocumentNavigator TDD Test Suite loaded. Run with: runDocumentNavigatorTests()');
-
-export { runner };
\ No newline at end of file
diff --git a/markitect/static/js/tests/test-documentcontrols-extraction.js b/markitect/static/js/tests/test-documentcontrols-extraction.js
deleted file mode 100644
index 2d5607ca..00000000
--- a/markitect/static/js/tests/test-documentcontrols-extraction.js
+++ /dev/null
@@ -1,218 +0,0 @@
-#!/usr/bin/env node
-
-/**
- * TDD Test for Document Controls Component Extraction
- *
- * Tests the extraction of DocumentControls from the monolithic editor.js
- * DocumentControls handles the floating control panel and its actions.
- */
-
-const RefactorTestRunner = require('./refactor-test-runner.js');
-
-const runner = new RefactorTestRunner();
-
-// Define expected DocumentControls API
-const EXPECTED_DOCUMENTCONTROLS_API = [
- 'constructor',
- 'create',
- 'destroy',
- 'show',
- 'hide',
- 'addButton',
- 'removeButton',
- 'setEventHandlers',
- 'updateStatus',
- 'getControlPanel'
-];
-
-runner.describe('DocumentControls Component Extraction', () => {
-
- runner.it('should define expected API methods', () => {
- const expectedMethods = EXPECTED_DOCUMENTCONTROLS_API;
- runner.expect(expectedMethods.length).toBe(10);
- runner.expect(expectedMethods).toContain('create');
- runner.expect(expectedMethods).toContain('addButton');
- runner.expect(expectedMethods).toContain('setEventHandlers');
- });
-
- runner.it('should load extracted DocumentControls component', () => {
- // Load the extracted component
- delete require.cache[require.resolve('../components/document-controls.js')];
-
- try {
- const module = require('../components/document-controls.js');
- runner.expect(module.DocumentControls).toBeTruthy();
-
- // Set global for other tests
- global.ExtractedDocumentControls = module.DocumentControls;
- } catch (error) {
- throw new Error(`Failed to load extracted DocumentControls: ${error.message}`);
- }
- });
-
- runner.it('should preserve constructor functionality', () => {
- const DocumentControls = global.ExtractedDocumentControls;
-
- const controls = new DocumentControls();
- runner.expect(controls).toBeInstanceOf(DocumentControls);
- runner.expect(controls.controlPanel).toBeFalsy(); // Initially null
- runner.expect(controls.buttons).toBeInstanceOf(Map);
- });
-
- runner.it('should preserve control panel creation functionality', () => {
- const DocumentControls = global.ExtractedDocumentControls;
-
- const controls = new DocumentControls();
- controls.create();
-
- const panel = controls.getControlPanel();
- runner.expect(panel).toBeTruthy();
- runner.expect(panel.id).toBe('markitect-global-controls');
-
- // Check that panel is added to DOM
- const domPanel = document.getElementById('markitect-global-controls');
- runner.expect(domPanel).toBeTruthy();
-
- // Cleanup
- controls.destroy();
- });
-
- runner.it('should preserve button creation functionality', () => {
- const DocumentControls = global.ExtractedDocumentControls;
-
- const controls = new DocumentControls();
- controls.create();
-
- // Default buttons should be created
- runner.expect(controls.buttons.has('save-document')).toBeTruthy();
- runner.expect(controls.buttons.has('reset-all')).toBeTruthy();
- runner.expect(controls.buttons.has('show-status')).toBeTruthy();
- runner.expect(controls.buttons.has('toggle-debug')).toBeTruthy();
-
- // Check DOM elements exist
- runner.expect(document.getElementById('save-document')).toBeTruthy();
- runner.expect(document.getElementById('reset-all')).toBeTruthy();
- runner.expect(document.getElementById('show-status')).toBeTruthy();
- runner.expect(document.getElementById('toggle-debug')).toBeTruthy();
-
- // Cleanup
- controls.destroy();
- });
-
- runner.it('should support custom button addition', () => {
- const DocumentControls = global.ExtractedDocumentControls;
-
- const controls = new DocumentControls();
- controls.create();
-
- // Add custom button
- const customButton = controls.addButton('custom-test', '๐ฏ Test', '#ff6600');
- runner.expect(customButton).toBeTruthy();
- runner.expect(customButton.id).toBe('custom-test');
- runner.expect(customButton.textContent).toBe('๐ฏ Test');
-
- // Check button is in map and DOM
- runner.expect(controls.buttons.has('custom-test')).toBeTruthy();
- runner.expect(document.getElementById('custom-test')).toBeTruthy();
-
- // Cleanup
- controls.destroy();
- });
-
- runner.it('should support event handler configuration', () => {
- const DocumentControls = global.ExtractedDocumentControls;
-
- const controls = new DocumentControls();
- controls.create();
-
- let saveClicked = false;
- let resetClicked = false;
-
- const handlers = {
- 'save-document': () => { saveClicked = true; },
- 'reset-all': () => { resetClicked = true; }
- };
-
- controls.setEventHandlers(handlers);
-
- // Simulate button clicks
- const saveBtn = document.getElementById('save-document');
- const resetBtn = document.getElementById('reset-all');
-
- saveBtn.click();
- resetBtn.click();
-
- runner.expect(saveClicked).toBeTruthy();
- runner.expect(resetClicked).toBeTruthy();
-
- // Cleanup
- controls.destroy();
- });
-
- runner.it('should support show/hide functionality', () => {
- const DocumentControls = global.ExtractedDocumentControls;
-
- const controls = new DocumentControls();
- controls.create();
-
- const panel = controls.getControlPanel();
-
- // Test hiding
- controls.hide();
- runner.expect(panel.style.display).toBe('none');
-
- // Test showing
- controls.show();
- runner.expect(panel.style.display).toBe('block');
-
- // Cleanup
- controls.destroy();
- });
-
- runner.it('should preserve destroy functionality', () => {
- const DocumentControls = global.ExtractedDocumentControls;
-
- const controls = new DocumentControls();
- controls.create();
-
- // Verify panel exists
- runner.expect(document.getElementById('markitect-global-controls')).toBeTruthy();
-
- // Destroy
- controls.destroy();
-
- // Verify panel is removed
- runner.expect(document.getElementById('markitect-global-controls')).toBeFalsy();
- runner.expect(controls.controlPanel).toBeFalsy();
- });
-
- runner.it('should support status updates', () => {
- const DocumentControls = global.ExtractedDocumentControls;
-
- const controls = new DocumentControls();
- controls.create();
-
- // Test status update
- controls.updateStatus({ totalSections: 5, editingSections: 2 });
-
- // The status should be reflected in the panel (implementation specific)
- const panel = controls.getControlPanel();
- runner.expect(panel).toBeTruthy();
-
- // Cleanup
- controls.destroy();
- });
-});
-
-module.exports = {
- runner,
- EXPECTED_DOCUMENTCONTROLS_API
-};
-
-// Run tests if called directly
-if (require.main === module) {
- console.log('๐งช Testing DocumentControls Component Extraction');
- runner.run().then(() => {
- console.log('โ
DocumentControls extraction tests completed');
- });
-}
\ No newline at end of file
diff --git a/markitect/static/js/tests/test-domrenderer-extraction.js b/markitect/static/js/tests/test-domrenderer-extraction.js
deleted file mode 100644
index e8aadc04..00000000
--- a/markitect/static/js/tests/test-domrenderer-extraction.js
+++ /dev/null
@@ -1,212 +0,0 @@
-#!/usr/bin/env node
-
-/**
- * TDD Test for DOMRenderer Component Extraction
- *
- * Tests the extraction of DOMRenderer from the monolithic editor.js
- * DOMRenderer handles all DOM interactions and UI rendering.
- */
-
-const RefactorTestRunner = require('./refactor-test-runner.js');
-
-const runner = new RefactorTestRunner();
-
-// Define expected DOMRenderer API
-const EXPECTED_DOMRENDERER_API = [
- 'constructor',
- 'renderAllSections',
- 'renderSection',
- 'showEditor',
- 'hideCurrentEditor',
- 'showImageEditor',
- 'findSectionElement',
- 'handleSectionClick',
- 'setupSectionElement',
- 'trackEvent',
- 'getEventStats'
- // Note: addGlobalControls and debug methods are on MarkitectCleanEditor, not DOMRenderer
-];
-
-runner.describe('DOMRenderer Component Extraction', () => {
-
- runner.it('should define expected API methods', () => {
- const expectedMethods = EXPECTED_DOMRENDERER_API;
- runner.expect(expectedMethods.length).toBe(11);
- runner.expect(expectedMethods).toContain('renderAllSections');
- runner.expect(expectedMethods).toContain('showEditor');
- runner.expect(expectedMethods).toContain('handleSectionClick');
- });
-
- runner.it('should extract from monolithic editor.js', () => {
- // Load the monolithic editor.js to extract DOMRenderer
- delete require.cache[require.resolve('/home/worsch/markitect_project/markitect/static/editor.js')];
-
- try {
- const editorModule = require('/home/worsch/markitect_project/markitect/static/editor.js');
- runner.expect(editorModule.DOMRenderer).toBeTruthy();
- // Set global for other tests
- global.DOMRenderer = editorModule.DOMRenderer;
- global.SectionManager = editorModule.SectionManager;
- } catch (error) {
- throw new Error(`Failed to load monolithic editor.js: ${error.message}`);
- }
- });
-
- runner.it('should preserve DOMRenderer constructor functionality', () => {
- const DOMRenderer = global.DOMRenderer;
- const SectionManager = global.SectionManager;
-
- const container = document.createElement('div');
- const sectionManager = new SectionManager();
-
- const renderer = new DOMRenderer(sectionManager, container);
- runner.expect(renderer).toBeInstanceOf(DOMRenderer);
- runner.expect(renderer.sectionManager).toBe(sectionManager);
- runner.expect(renderer.container).toBe(container);
- });
-
- runner.it('should preserve section rendering functionality', () => {
- const DOMRenderer = global.DOMRenderer;
- const SectionManager = global.SectionManager;
-
- const container = document.createElement('div');
- container.innerHTML = '
';
-
- const sectionManager = new SectionManager();
- const renderer = new DOMRenderer(sectionManager, container);
-
- const testMarkdown = '# Test Heading\nTest content';
- const sections = sectionManager.createSectionsFromMarkdown(testMarkdown);
-
- // This should not throw an error
- renderer.renderAllSections(sections);
-
- // Check that some content was rendered
- runner.expect(container.innerHTML.length).toBe(container.innerHTML.length); // Basic sanity check
- });
-
- runner.it('should preserve findSectionElement functionality', () => {
- const DOMRenderer = global.DOMRenderer;
- const SectionManager = global.SectionManager;
-
- const container = document.createElement('div');
- container.innerHTML = '
';
-
- const sectionManager = new SectionManager();
- const renderer = new DOMRenderer(sectionManager, container);
-
- const testMarkdown = '# Test Heading\nTest content';
- const sections = sectionManager.createSectionsFromMarkdown(testMarkdown);
- renderer.renderAllSections(sections);
-
- const sectionId = sections[0].id;
- const element = renderer.findSectionElement(sectionId);
-
- // Should find an element or return null (not throw error)
- runner.expect(typeof element === 'object').toBeTruthy();
- });
-
- runner.it('should preserve event tracking functionality', () => {
- const DOMRenderer = global.DOMRenderer;
- const SectionManager = global.SectionManager;
-
- const container = document.createElement('div');
- const sectionManager = new SectionManager();
- const renderer = new DOMRenderer(sectionManager, container);
-
- // Should have trackEvent method
- runner.expect(typeof renderer.trackEvent === 'function').toBeTruthy();
-
- // Should be able to track an event
- renderer.trackEvent('test-event', { data: 'test' });
-
- // Should have getEventStats method
- runner.expect(typeof renderer.getEventStats === 'function').toBeTruthy();
-
- const stats = renderer.getEventStats();
- runner.expect(typeof stats === 'object').toBeTruthy();
- });
-
- runner.it('should preserve editor showing functionality', () => {
- const DOMRenderer = global.DOMRenderer;
- const SectionManager = global.SectionManager;
-
- const container = document.createElement('div');
- container.innerHTML = '
';
-
- const sectionManager = new SectionManager();
- const renderer = new DOMRenderer(sectionManager, container);
-
- const testMarkdown = '# Test Heading\nTest content';
- const sections = sectionManager.createSectionsFromMarkdown(testMarkdown);
- renderer.renderAllSections(sections);
-
- const sectionId = sections[0].id;
-
- // showEditor should not throw error
- try {
- renderer.showEditor(sectionId, 'test content');
- runner.expect(true).toBeTruthy(); // If we get here, no error was thrown
- } catch (error) {
- // Some errors are expected if DOM structure isn't complete
- runner.expect(typeof error.message === 'string').toBeTruthy();
- }
- });
-
- runner.it('should have core DOM rendering methods', () => {
- const DOMRenderer = global.DOMRenderer;
- const SectionManager = global.SectionManager;
-
- const container = document.createElement('div');
- const sectionManager = new SectionManager();
- const renderer = new DOMRenderer(sectionManager, container);
-
- // Should have core methods
- runner.expect(typeof renderer.renderAllSections === 'function').toBeTruthy();
- runner.expect(typeof renderer.showEditor === 'function').toBeTruthy();
- runner.expect(typeof renderer.findSectionElement === 'function').toBeTruthy();
- runner.expect(typeof renderer.trackEvent === 'function').toBeTruthy();
- });
-});
-
-// Export API tests for use during extraction
-const DOMRENDERER_API_TESTS = [
- (DOMRenderer, SectionManager) => {
- const container = document.createElement('div');
- const sectionManager = new SectionManager();
- const renderer = new DOMRenderer(sectionManager, container);
- if (!renderer.sectionManager) {
- throw new Error('sectionManager property missing');
- }
- },
- (DOMRenderer, SectionManager) => {
- const container = document.createElement('div');
- const sectionManager = new SectionManager();
- const renderer = new DOMRenderer(sectionManager, container);
- if (typeof renderer.renderAllSections !== 'function') {
- throw new Error('renderAllSections method missing');
- }
- },
- (DOMRenderer, SectionManager) => {
- const container = document.createElement('div');
- const sectionManager = new SectionManager();
- const renderer = new DOMRenderer(sectionManager, container);
- if (typeof renderer.showEditor !== 'function') {
- throw new Error('showEditor method missing');
- }
- }
-];
-
-module.exports = {
- runner,
- EXPECTED_DOMRENDERER_API,
- DOMRENDERER_API_TESTS
-};
-
-// Run tests if called directly
-if (require.main === module) {
- console.log('๐งช Testing DOMRenderer Component Extraction');
- runner.run().then(() => {
- console.log('โ
DOMRenderer extraction tests completed');
- });
-}
\ No newline at end of file
diff --git a/markitect/static/js/tests/test-extracted-domrenderer.js b/markitect/static/js/tests/test-extracted-domrenderer.js
deleted file mode 100644
index d0a8990a..00000000
--- a/markitect/static/js/tests/test-extracted-domrenderer.js
+++ /dev/null
@@ -1,271 +0,0 @@
-#!/usr/bin/env node
-
-/**
- * TDD Test for Extracted DOMRenderer Component
- *
- * Tests the extracted DOMRenderer component independently from the monolith.
- * Verifies that core functionality is preserved after extraction.
- */
-
-const RefactorTestRunner = require('./refactor-test-runner.js');
-
-const runner = new RefactorTestRunner();
-
-runner.describe('Extracted DOMRenderer Component', () => {
-
- runner.it('should load extracted DOMRenderer component', () => {
- // Load the extracted component
- delete require.cache[require.resolve('../components/dom-renderer.js')];
-
- try {
- const module = require('../components/dom-renderer.js');
- runner.expect(module.DOMRenderer).toBeTruthy();
- runner.expect(module.FloatingMenu).toBeTruthy();
-
- // Set globals for other tests
- global.ExtractedDOMRenderer = module.DOMRenderer;
- global.ExtractedFloatingMenu = module.FloatingMenu;
- } catch (error) {
- throw new Error(`Failed to load extracted DOMRenderer: ${error.message}`);
- }
- });
-
- runner.it('should preserve constructor functionality', () => {
- const DOMRenderer = global.ExtractedDOMRenderer;
-
- // Load SectionManager from our extracted core
- const sectionModule = require('../core/section-manager.js');
- const SectionManager = sectionModule.SectionManager;
-
- const container = document.createElement('div');
- const sectionManager = new SectionManager();
-
- const renderer = new DOMRenderer(sectionManager, container);
- runner.expect(renderer).toBeInstanceOf(DOMRenderer);
- runner.expect(renderer.sectionManager).toBe(sectionManager);
- runner.expect(renderer.container).toBe(container);
- runner.expect(renderer.editingSections).toBeInstanceOf(Set);
- });
-
- runner.it('should preserve section rendering functionality', () => {
- const DOMRenderer = global.ExtractedDOMRenderer;
- const sectionModule = require('../core/section-manager.js');
- const SectionManager = sectionModule.SectionManager;
-
- const container = document.createElement('div');
- container.innerHTML = '
';
-
- const sectionManager = new SectionManager();
- const renderer = new DOMRenderer(sectionManager, container);
-
- const testMarkdown = '# Test Heading\nTest content';
- const sections = sectionManager.createSectionsFromMarkdown(testMarkdown);
-
- // This should not throw an error
- renderer.renderAllSections(sections);
-
- // Check that content was rendered
- runner.expect(container.innerHTML.length > 100).toBeTruthy();
- runner.expect(container.innerHTML).toContain('Test Heading');
- });
-
- runner.it('should preserve findSectionElement functionality', () => {
- const DOMRenderer = global.ExtractedDOMRenderer;
- const sectionModule = require('../core/section-manager.js');
- const SectionManager = sectionModule.SectionManager;
-
- const container = document.createElement('div');
- container.innerHTML = '
';
-
- const sectionManager = new SectionManager();
- const renderer = new DOMRenderer(sectionManager, container);
-
- const testMarkdown = '# Test Heading\nTest content';
- const sections = sectionManager.createSectionsFromMarkdown(testMarkdown);
- renderer.renderAllSections(sections);
-
- const sectionId = sections[0].id;
- const element = renderer.findSectionElement(sectionId);
-
- runner.expect(element).toBeTruthy();
- runner.expect(element.getAttribute('data-section-id')).toBe(sectionId);
- });
-
- runner.it('should preserve event tracking functionality', () => {
- const DOMRenderer = global.ExtractedDOMRenderer;
- const sectionModule = require('../core/section-manager.js');
- const SectionManager = sectionModule.SectionManager;
-
- const container = document.createElement('div');
- const sectionManager = new SectionManager();
- const renderer = new DOMRenderer(sectionManager, container);
-
- // Should have trackEvent method
- runner.expect(typeof renderer.trackEvent === 'function').toBeTruthy();
-
- // Should be able to track an event
- renderer.trackEvent('test-event', { data: 'test' });
-
- // Should have getEventStats method
- runner.expect(typeof renderer.getEventStats === 'function').toBeTruthy();
-
- const stats = renderer.getEventStats();
- runner.expect(typeof stats === 'object').toBeTruthy();
- runner.expect(stats).toHaveProperty('stats');
- runner.expect(stats).toHaveProperty('totalEvents');
- runner.expect(stats).toHaveProperty('recentEvents');
- });
-
- runner.it('should preserve editor showing functionality', () => {
- const DOMRenderer = global.ExtractedDOMRenderer;
- const sectionModule = require('../core/section-manager.js');
- const SectionManager = sectionModule.SectionManager;
-
- const container = document.createElement('div');
- container.innerHTML = '
';
-
- const sectionManager = new SectionManager();
- const renderer = new DOMRenderer(sectionManager, container);
-
- const testMarkdown = '# Test Heading\nTest content';
- const sections = sectionManager.createSectionsFromMarkdown(testMarkdown);
- renderer.renderAllSections(sections);
-
- const sectionId = sections[0].id;
-
- // showEditor should not throw error
- try {
- renderer.showEditor(sectionId, 'test content');
- runner.expect(true).toBeTruthy(); // If we get here, no error was thrown
-
- // Check that editing state was set
- runner.expect(renderer.editingSections.has(sectionId)).toBeTruthy();
- } catch (error) {
- throw new Error(`showEditor failed: ${error.message}`);
- }
- });
-
- runner.it('should preserve FloatingMenu functionality', () => {
- const FloatingMenu = global.ExtractedFloatingMenu;
- const DOMRenderer = global.ExtractedDOMRenderer;
- const sectionModule = require('../core/section-manager.js');
- const SectionManager = sectionModule.SectionManager;
-
- const container = document.createElement('div');
- container.innerHTML = '
';
-
- const sectionManager = new SectionManager();
- const renderer = new DOMRenderer(sectionManager, container);
-
- const testMarkdown = '# Test Heading\nTest content';
- const sections = sectionManager.createSectionsFromMarkdown(testMarkdown);
- renderer.renderAllSections(sections);
-
- const sectionId = sections[0].id;
- const floatingMenu = new FloatingMenu(sectionId, 'text', renderer);
-
- runner.expect(floatingMenu.sectionId).toBe(sectionId);
- runner.expect(floatingMenu.type).toBe('text');
- runner.expect(floatingMenu.renderer).toBe(renderer);
- runner.expect(floatingMenu.isVisible).toBeFalsy();
-
- // Test show/hide functionality
- const content = document.createElement('div');
- content.textContent = 'Test content';
-
- floatingMenu.show(content);
- runner.expect(floatingMenu.isVisible).toBeTruthy();
-
- floatingMenu.hide();
- runner.expect(floatingMenu.isVisible).toBeFalsy();
- });
-
- runner.it('should handle section click events', () => {
- const DOMRenderer = global.ExtractedDOMRenderer;
- const sectionModule = require('../core/section-manager.js');
- const SectionManager = sectionModule.SectionManager;
-
- const container = document.createElement('div');
- container.innerHTML = '
';
-
- const sectionManager = new SectionManager();
- const renderer = new DOMRenderer(sectionManager, container);
-
- const testMarkdown = '# Test Heading\nTest content';
- const sections = sectionManager.createSectionsFromMarkdown(testMarkdown);
- renderer.renderAllSections(sections);
-
- const sectionId = sections[0].id;
- const element = renderer.findSectionElement(sectionId);
-
- // Simulate a click event
- const clickEvent = new Event('click', { bubbles: true });
- Object.defineProperty(clickEvent, 'target', { value: element });
-
- // Should not throw error
- try {
- renderer.handleSectionClick(clickEvent);
- runner.expect(true).toBeTruthy();
- } catch (error) {
- throw new Error(`handleSectionClick failed: ${error.message}`);
- }
- });
-
- // Comparative test - verify extracted component behaves similarly to original
- runner.it('should behave similarly to original monolithic component', () => {
- // Load both components
- const originalModule = require('/home/worsch/markitect_project/markitect/static/editor.js');
- const extractedModule = require('../components/dom-renderer.js');
- const sectionModule = require('../core/section-manager.js');
-
- const originalSectionManager = new originalModule.SectionManager();
- const extractedSectionManager = new sectionModule.SectionManager();
-
- const originalContainer = document.createElement('div');
- originalContainer.innerHTML = '
';
-
- const extractedContainer = document.createElement('div');
- extractedContainer.innerHTML = '
';
-
- const originalRenderer = new originalModule.DOMRenderer(originalSectionManager, originalContainer);
- const extractedRenderer = new extractedModule.DOMRenderer(extractedSectionManager, extractedContainer);
-
- const testMarkdown = '# Test\nContent\n\n## Subheading\nMore content';
-
- // Create sections with both
- const originalSections = originalSectionManager.createSectionsFromMarkdown(testMarkdown);
- const extractedSections = extractedSectionManager.createSectionsFromMarkdown(testMarkdown);
-
- // Render with both
- originalRenderer.renderAllSections(originalSections);
- extractedRenderer.renderAllSections(extractedSections);
-
- // Should have rendered content
- runner.expect(originalContainer.innerHTML.length > 100).toBeTruthy();
- runner.expect(extractedContainer.innerHTML.length > 100).toBeTruthy();
-
- // Should have same number of section elements
- const originalSectionElements = originalContainer.querySelectorAll('.ui-edit-section');
- const extractedSectionElements = extractedContainer.querySelectorAll('.ui-edit-section');
-
- runner.expect(extractedSectionElements.length).toBe(originalSectionElements.length);
-
- // Should have similar event stats structure
- const originalStats = originalRenderer.getEventStats();
- const extractedStats = extractedRenderer.getEventStats();
-
- runner.expect(extractedStats).toHaveProperty('stats');
- runner.expect(extractedStats).toHaveProperty('totalEvents');
- runner.expect(extractedStats).toHaveProperty('recentEvents');
- });
-});
-
-module.exports = runner;
-
-// Run tests if called directly
-if (require.main === module) {
- console.log('๐งช Testing Extracted DOMRenderer Component');
- runner.run().then(() => {
- console.log('โ
Extracted DOMRenderer tests completed');
- });
-}
\ No newline at end of file
diff --git a/markitect/static/js/tests/test-extracted-section-manager.js b/markitect/static/js/tests/test-extracted-section-manager.js
deleted file mode 100644
index 0eb51d01..00000000
--- a/markitect/static/js/tests/test-extracted-section-manager.js
+++ /dev/null
@@ -1,226 +0,0 @@
-#!/usr/bin/env node
-
-/**
- * TDD Test for Extracted SectionManager Component
- *
- * Tests the extracted SectionManager component independently from the monolith.
- * Verifies that all functionality is preserved after extraction.
- */
-
-const RefactorTestRunner = require('./refactor-test-runner.js');
-
-const runner = new RefactorTestRunner();
-
-runner.describe('Extracted SectionManager Component', () => {
-
- runner.it('should load extracted SectionManager component', () => {
- // Load the extracted component
- delete require.cache[require.resolve('../core/section-manager.js')];
-
- try {
- const module = require('../core/section-manager.js');
- runner.expect(module.SectionManager).toBeTruthy();
- runner.expect(module.Section).toBeTruthy();
- runner.expect(module.EditState).toBeTruthy();
- runner.expect(module.SectionType).toBeTruthy();
-
- // Set globals for other tests
- global.ExtractedSectionManager = module.SectionManager;
- global.ExtractedSection = module.Section;
- global.ExtractedEditState = module.EditState;
- global.ExtractedSectionType = module.SectionType;
- } catch (error) {
- throw new Error(`Failed to load extracted SectionManager: ${error.message}`);
- }
- });
-
- runner.it('should preserve constructor functionality', () => {
- const SectionManager = global.ExtractedSectionManager;
-
- const manager = new SectionManager();
- runner.expect(manager).toBeInstanceOf(SectionManager);
- runner.expect(manager.sections).toBeInstanceOf(Map);
- runner.expect(manager.listeners).toBeInstanceOf(Map);
- });
-
- runner.it('should preserve section creation functionality', () => {
- const SectionManager = global.ExtractedSectionManager;
- const manager = new SectionManager();
-
- const testMarkdown = `# Heading 1\nContent 1\n\n## Heading 2\nContent 2`;
- const sections = manager.createSectionsFromMarkdown(testMarkdown);
-
- runner.expect(Array.isArray(sections)).toBeTruthy();
- runner.expect(sections.length).toBe(2);
- runner.expect(sections[0].currentMarkdown).toContain('Heading 1');
- runner.expect(sections[1].currentMarkdown).toContain('Heading 2');
- });
-
- runner.it('should preserve section editing functionality', () => {
- const SectionManager = global.ExtractedSectionManager;
- const manager = new SectionManager();
-
- const sections = manager.createSectionsFromMarkdown('# Test\nContent');
- const sectionId = sections[0].id;
-
- // Test start editing
- const content = manager.startEditing(sectionId);
- runner.expect(content).toContain('Test');
-
- const section = manager.sections.get(sectionId);
- runner.expect(section.isEditing()).toBeTruthy();
-
- // Test stop editing
- section.stopEditing();
- runner.expect(section.isEditing()).toBeFalsy();
- });
-
- runner.it('should preserve event system functionality', () => {
- const SectionManager = global.ExtractedSectionManager;
- const manager = new SectionManager();
-
- let eventFired = false;
- let eventData = null;
-
- manager.on('test-event', (data) => {
- eventFired = true;
- eventData = data;
- });
-
- manager.emit('test-event', { test: 'data' });
-
- runner.expect(eventFired).toBeTruthy();
- runner.expect(eventData).toEqual({ test: 'data' });
- });
-
- runner.it('should preserve document status functionality', () => {
- const SectionManager = global.ExtractedSectionManager;
- const manager = new SectionManager();
-
- manager.createSectionsFromMarkdown('# Test\nContent');
- const status = manager.getDocumentStatus();
-
- runner.expect(status).toHaveProperty('totalSections');
- runner.expect(status).toHaveProperty('editingSections');
- runner.expect(status.totalSections).toBe(1);
- });
-
- runner.it('should preserve getAllSections functionality', () => {
- const SectionManager = global.ExtractedSectionManager;
- const manager = new SectionManager();
-
- const testMarkdown = '# One\nContent\n\n# Two\nMore content';
- manager.createSectionsFromMarkdown(testMarkdown);
-
- const allSections = manager.getAllSections();
- runner.expect(Array.isArray(allSections)).toBeTruthy();
- runner.expect(allSections.length).toBe(2);
- });
-
- runner.it('should preserve section splitting functionality', () => {
- const SectionManager = global.ExtractedSectionManager;
- const manager = new SectionManager();
-
- const sections = manager.createSectionsFromMarkdown('# Original\nContent');
- const sectionId = sections[0].id;
-
- const newContent = '# Split 1\nContent 1\n\n# Split 2\nContent 2';
- const newSections = manager.handleSectionSplit(sectionId, newContent);
-
- runner.expect(Array.isArray(newSections)).toBeTruthy();
- runner.expect(newSections.length).toBe(2);
- runner.expect(manager.sections.has(sectionId)).toBeFalsy(); // Original removed
- });
-
- runner.it('should preserve Section class functionality', () => {
- const Section = global.ExtractedSection;
- const EditState = global.ExtractedEditState;
-
- const section = new Section('test-id', '# Test Content', 'heading');
-
- runner.expect(section.id).toBe('test-id');
- runner.expect(section.currentMarkdown).toBe('# Test Content');
- runner.expect(section.type).toBe('heading');
- runner.expect(section.state).toBe(EditState.ORIGINAL);
- });
-
- runner.it('should preserve Section ID generation', () => {
- const Section = global.ExtractedSection;
-
- const id1 = Section.generateId('# Test Heading', 0);
- const id2 = Section.generateId('# Different Heading', 1);
-
- runner.expect(typeof id1 === 'string').toBeTruthy();
- runner.expect(typeof id2 === 'string').toBeTruthy();
- runner.expect(id1).toContain('section-');
- runner.expect(id2).toContain('section-');
- runner.expect(id1 !== id2).toBeTruthy(); // Should be unique
- });
-
- runner.it('should preserve Section type detection', () => {
- const Section = global.ExtractedSection;
- const SectionType = global.ExtractedSectionType;
-
- runner.expect(Section.detectType('# Heading')).toBe(SectionType.HEADING);
- runner.expect(Section.detectType('')).toBe(SectionType.IMAGE);
- runner.expect(Section.detectType('```code```')).toBe(SectionType.CODE);
- runner.expect(Section.detectType('Regular paragraph')).toBe(SectionType.PARAGRAPH);
- });
-
- // Comparative test - verify extracted component behaves identically to original
- runner.it('should behave identically to original monolithic component', () => {
- // Load both components
- const originalModule = require('/home/worsch/markitect_project/markitect/static/editor.js');
- const extractedModule = require('../core/section-manager.js');
-
- const originalManager = new originalModule.SectionManager();
- const extractedManager = new extractedModule.SectionManager();
-
- const testMarkdown = '# Test\nContent\n\n## Subheading\nMore content';
-
- // Debug: Check what each component produces
- console.log('Creating sections with original component...');
- const originalSections = originalManager.createSectionsFromMarkdown(testMarkdown);
- console.log(`Original produced ${originalSections.length} sections`);
-
- console.log('Creating sections with extracted component...');
- const extractedSections = extractedManager.createSectionsFromMarkdown(testMarkdown);
- console.log(`Extracted produced ${extractedSections.length} sections`);
-
- if (originalSections.length > 0) {
- console.log('Original first section:', originalSections[0].currentMarkdown);
- }
- if (extractedSections.length > 0) {
- console.log('Extracted first section:', extractedSections[0].currentMarkdown);
- }
-
- // Should have same number of sections
- runner.expect(extractedSections.length).toBe(originalSections.length);
-
- // Should have same content
- for (let i = 0; i < originalSections.length; i++) {
- runner.expect(extractedSections[i].currentMarkdown).toBe(originalSections[i].currentMarkdown);
- runner.expect(extractedSections[i].type).toBe(originalSections[i].type);
- }
-
- // Should have same document status structure
- const originalStatus = originalManager.getDocumentStatus();
- const extractedStatus = extractedManager.getDocumentStatus();
-
- console.log('Original status:', originalStatus);
- console.log('Extracted status:', extractedStatus);
-
- runner.expect(extractedStatus.totalSections).toBe(originalStatus.totalSections);
- runner.expect(extractedStatus.editingSections).toBe(originalStatus.editingSections);
- });
-});
-
-module.exports = runner;
-
-// Run tests if called directly
-if (require.main === module) {
- console.log('๐งช Testing Extracted SectionManager Component');
- runner.run().then(() => {
- console.log('โ
Extracted SectionManager tests completed');
- });
-}
\ No newline at end of file
diff --git a/markitect/static/js/tests/test-full-integration.js b/markitect/static/js/tests/test-full-integration.js
deleted file mode 100644
index 3edb0ced..00000000
--- a/markitect/static/js/tests/test-full-integration.js
+++ /dev/null
@@ -1,305 +0,0 @@
-#!/usr/bin/env node
-
-/**
- * Full Integration Test
- *
- * Tests that all extracted components (SectionManager, DOMRenderer,
- * DebugPanel, DocumentControls) work together as a complete system.
- */
-
-const RefactorTestRunner = require('./refactor-test-runner.js');
-
-const runner = new RefactorTestRunner();
-
-runner.describe('Full Component Integration Tests', () => {
-
- runner.it('should load all extracted components', () => {
- try {
- // Load all extracted components
- const sectionModule = require('../core/section-manager.js');
- const domModule = require('../components/dom-renderer.js');
- const debugModule = require('../components/debug-panel.js');
- const controlsModule = require('../components/document-controls.js');
-
- runner.expect(sectionModule.SectionManager).toBeTruthy();
- runner.expect(domModule.DOMRenderer).toBeTruthy();
- runner.expect(debugModule.DebugPanel).toBeTruthy();
- runner.expect(controlsModule.DocumentControls).toBeTruthy();
-
- // Set globals for other tests
- global.ExtractedSectionManager = sectionModule.SectionManager;
- global.ExtractedDOMRenderer = domModule.DOMRenderer;
- global.ExtractedDebugPanel = debugModule.DebugPanel;
- global.ExtractedDocumentControls = controlsModule.DocumentControls;
-
- } catch (error) {
- throw new Error(`Failed to load extracted components: ${error.message}`);
- }
- });
-
- runner.it('should support complete document editing workflow with all components', () => {
- const SectionManager = global.ExtractedSectionManager;
- const DOMRenderer = global.ExtractedDOMRenderer;
- const DebugPanel = global.ExtractedDebugPanel;
- const DocumentControls = global.ExtractedDocumentControls;
-
- // Setup DOM container
- const container = document.createElement('div');
- container.innerHTML = '
';
- document.body.appendChild(container);
-
- // Create all components
- const sectionManager = new SectionManager();
- const domRenderer = new DOMRenderer(sectionManager, container);
- const debugPanel = new DebugPanel();
- const documentControls = new DocumentControls();
-
- // Setup document controls
- documentControls.create();
-
- // Wire up event handlers for debugging
- sectionManager.on('sections-created', (data) => {
- debugPanel.addMessage(`Created ${data.count} sections`, 'INFO');
- });
-
- sectionManager.on('edit-started', (data) => {
- debugPanel.addMessage(`Edit started for section: ${data.sectionId}`, 'DEBUG');
- });
-
- // Test workflow: Create document
- const testMarkdown = `# Document Title
-Introduction paragraph with some content.
-
-## Section A
-Content for section A with details.
-
-
-
-### Subsection A.1
-More detailed content here.`;
-
- // Create sections
- const sections = sectionManager.createSectionsFromMarkdown(testMarkdown);
- runner.expect(sections.length).toBe(4);
-
- // Render sections
- domRenderer.renderAllSections(sections);
- const renderedElements = container.querySelectorAll('.ui-edit-section');
- runner.expect(renderedElements.length).toBe(sections.length);
-
- // Test editing workflow
- const firstSection = sections[0];
- sectionManager.startEditing(firstSection.id);
- runner.expect(firstSection.isEditing()).toBeTruthy();
-
- // Check debug messages were created
- runner.expect(debugPanel.getMessageCount()).toBe(2); // sections-created + edit-started
-
- // Test document controls functionality
- const controlPanel = documentControls.getControlPanel();
- runner.expect(controlPanel).toBeTruthy();
- runner.expect(document.getElementById('save-document')).toBeTruthy();
- runner.expect(document.getElementById('toggle-debug')).toBeTruthy();
-
- // Cleanup
- document.body.removeChild(container);
- documentControls.destroy();
- });
-
- runner.it('should support debug panel integration with document controls', () => {
- const DebugPanel = global.ExtractedDebugPanel;
- const DocumentControls = global.ExtractedDocumentControls;
-
- // Create components
- const debugPanel = new DebugPanel();
- const documentControls = new DocumentControls();
-
- // Setup document controls
- documentControls.create();
-
- // Setup debug panel toggle handler
- const handlers = {
- 'toggle-debug': () => debugPanel.toggle()
- };
- documentControls.setEventHandlers(handlers);
-
- // Test debug toggle functionality
- const debugButton = documentControls.getButton('toggle-debug');
- runner.expect(debugButton).toBeTruthy();
-
- // Add some debug messages
- debugPanel.addMessage('Test message 1', 'INFO');
- debugPanel.addMessage('Test message 2', 'ERROR');
-
- // Simulate button click to show debug panel
- debugButton.click();
- runner.expect(debugPanel.isActive).toBeTruthy();
-
- // Simulate button click to hide debug panel
- debugButton.click();
- runner.expect(debugPanel.isActive).toBeFalsy();
-
- // Cleanup
- documentControls.destroy();
- });
-
- runner.it('should support event-driven communication between all components', () => {
- const SectionManager = global.ExtractedSectionManager;
- const DOMRenderer = global.ExtractedDOMRenderer;
- const DebugPanel = global.ExtractedDebugPanel;
- const DocumentControls = global.ExtractedDocumentControls;
-
- // Setup container
- const container = document.createElement('div');
- container.innerHTML = '
';
- document.body.appendChild(container);
-
- // Create components
- const sectionManager = new SectionManager();
- const domRenderer = new DOMRenderer(sectionManager, container);
- const debugPanel = new DebugPanel();
- const documentControls = new DocumentControls();
-
- documentControls.create();
-
- // Setup comprehensive event handling
- let eventLog = [];
-
- sectionManager.on('sections-created', (data) => {
- eventLog.push(`sections-created: ${data.count} sections`);
- debugPanel.addMessage(`Sections created: ${data.count}`, 'INFO');
- });
-
- sectionManager.on('edit-started', (data) => {
- eventLog.push(`edit-started: ${data.sectionId}`);
- debugPanel.addMessage(`Edit started: ${data.sectionId}`, 'DEBUG');
- });
-
- sectionManager.on('changes-accepted', (data) => {
- eventLog.push(`changes-accepted: ${data.sectionId}`);
- debugPanel.addMessage(`Changes accepted: ${data.sectionId}`, 'SUCCESS');
- });
-
- // Test complete workflow
- const testMarkdown = '# Test\nContent for testing';
- const sections = sectionManager.createSectionsFromMarkdown(testMarkdown);
- domRenderer.renderAllSections(sections);
-
- // Start editing
- sectionManager.startEditing(sections[0].id);
- sectionManager.updateContent(sections[0].id, '# Updated Test\nUpdated content');
- sectionManager.acceptChanges(sections[0].id);
-
- // Verify events were logged
- runner.expect(eventLog.length).toBe(3);
- runner.expect(eventLog[0]).toContain('sections-created');
- runner.expect(eventLog[1]).toContain('edit-started');
- runner.expect(eventLog[2]).toContain('changes-accepted');
-
- // Verify debug messages were created
- runner.expect(debugPanel.getMessageCount()).toBe(3);
-
- // Test document controls status update
- const status = sectionManager.getDocumentStatus();
- documentControls.updateStatus(status);
- runner.expect(documentControls.lastStatus).toBeTruthy();
-
- // Cleanup
- document.body.removeChild(container);
- documentControls.destroy();
- });
-
- runner.it('should handle error scenarios gracefully across components', () => {
- const SectionManager = global.ExtractedSectionManager;
- const DOMRenderer = global.ExtractedDOMRenderer;
- const DebugPanel = global.ExtractedDebugPanel;
- const DocumentControls = global.ExtractedDocumentControls;
-
- // Test component creation without proper DOM setup
- const debugPanel = new DebugPanel();
- const documentControls = new DocumentControls();
-
- // These should not throw errors
- try {
- debugPanel.toggle(); // No DOM elements
- debugPanel.update(); // No DOM elements
- documentControls.show(); // No control panel created yet
- documentControls.hide(); // No control panel created yet
-
- runner.expect(true).toBeTruthy(); // If we get here, no errors were thrown
- } catch (error) {
- throw new Error(`Components should handle missing DOM gracefully: ${error.message}`);
- }
-
- // Test section manager with invalid input
- const sectionManager = new SectionManager();
- const sections = sectionManager.createSectionsFromMarkdown('');
- runner.expect(sections.length).toBe(0);
-
- // Test DOM renderer with invalid container
- try {
- const invalidRenderer = new DOMRenderer(sectionManager, null);
- runner.expect(invalidRenderer.container).toBeFalsy();
- } catch (error) {
- // This is acceptable - constructor might validate input
- runner.expect(typeof error.message === 'string').toBeTruthy();
- }
- });
-
- runner.it('should support scalable architecture with component lifecycle', () => {
- const SectionManager = global.ExtractedSectionManager;
- const DOMRenderer = global.ExtractedDOMRenderer;
- const DebugPanel = global.ExtractedDebugPanel;
- const DocumentControls = global.ExtractedDocumentControls;
-
- // Test multiple instances
- const sectionManager1 = new SectionManager();
- const sectionManager2 = new SectionManager();
- const debugPanel1 = new DebugPanel();
- const debugPanel2 = new DebugPanel();
-
- // Each should be independent
- debugPanel1.addMessage('Message from panel 1', 'INFO');
- debugPanel2.addMessage('Message from panel 2', 'ERROR');
-
- runner.expect(debugPanel1.getMessageCount()).toBe(1);
- runner.expect(debugPanel2.getMessageCount()).toBe(1);
-
- // Test section managers are independent
- const sections1 = sectionManager1.createSectionsFromMarkdown('# Document 1');
- const sections2 = sectionManager2.createSectionsFromMarkdown('# Document 2');
-
- runner.expect(sections1.length).toBe(1);
- runner.expect(sections2.length).toBe(1);
- runner.expect(sections1[0]).toBeTruthy();
- runner.expect(sections2[0]).toBeTruthy();
-
- // IDs should be different (each section gets unique ID)
- const id1 = sections1[0].id;
- const id2 = sections2[0].id;
- runner.expect(id1 !== id2).toBeTruthy();
-
- // Test document controls lifecycle
- const controls1 = new DocumentControls();
- const controls2 = new DocumentControls();
-
- controls1.create();
- runner.expect(document.getElementById('markitect-global-controls')).toBeTruthy();
-
- controls2.create(); // Should replace the first one
- runner.expect(document.getElementById('markitect-global-controls')).toBeTruthy();
-
- controls2.destroy();
- runner.expect(document.getElementById('markitect-global-controls')).toBeFalsy();
- });
-});
-
-module.exports = runner;
-
-// Run tests if called directly
-if (require.main === module) {
- console.log('๐งช Running Full Component Integration Tests');
- runner.run().then(() => {
- console.log('โ
Full integration tests completed');
- });
-}
\ No newline at end of file
diff --git a/markitect/static/js/tests/test-navigator-demo.html b/markitect/static/js/tests/test-navigator-demo.html
deleted file mode 100644
index 020178b1..00000000
--- a/markitect/static/js/tests/test-navigator-demo.html
+++ /dev/null
@@ -1,342 +0,0 @@
-
-
-
-
-
- DocumentNavigator Live Demo
-
-
-
-
-
-
-
1. Introduction to MarkiTect
-
-
MarkiTect is an advanced markdown processing engine that provides sophisticated document management capabilities. This demo showcases the DocumentNavigator widget, which provides Substack-style navigation for long-form documents.
-
-
The navigator automatically extracts headings from your content and builds a hierarchical table of contents that floats elegantly on the side of your document.
-
-
-
1.1 Core Features
-
-
The DocumentNavigator widget includes numerous advanced features designed for optimal user experience:
-
-
- Automatic Heading Detection : Scans document for H1, H2, H3 elements
- Hierarchical Structure : Maintains proper heading hierarchy with indentation
- Scroll Spy : Highlights current section as you scroll
- Smooth Navigation : Animated scrolling to clicked sections
- Responsive Design : Auto-hides on mobile devices
-
-
-
-
1.1.1 Responsive Behavior
-
-
The navigator intelligently adapts to different screen sizes. On desktop computers, it remains visible as a floating panel. On mobile devices, it automatically hides to preserve screen real estate for content.
-
-
Try resizing your browser window to see this behavior in action. The navigator will disappear when the viewport becomes narrow (under 768px wide).
-
-
-
1.1.2 Accessibility Features
-
-
The DocumentNavigator is built with accessibility in mind:
-
-
- Full keyboard navigation support
- ARIA labels and proper semantic markup
- Screen reader compatibility
- High contrast hover states
- Focus management
-
-
-
-
1.2 Implementation Details
-
-
The DocumentNavigator is implemented as a modular ES6 class that extends our base UIWidget class. This follows the planned plugin architecture for MarkiTect widgets.
-
-
Key implementation highlights include:
-
-
- extractHeadings() - Scans DOM for heading elements
- buildNavigationTree() - Creates hierarchical structure
- handleScroll() - Manages scroll spy functionality
- navigateToHeading() - Handles smooth scrolling
-
-
-
-
2. Widget Architecture
-
-
The DocumentNavigator follows a clean architectural pattern that separates concerns and provides maximum flexibility for customization and extension.
-
-
The widget is designed as part of a larger plugin ecosystem that will allow developers to create custom UI components that can be loaded dynamically and configured independently.
-
-
-
2.1 Base Class Hierarchy
-
-
Our widget system is built on a foundation of base classes that provide common functionality:
-
-
- Widget : Core functionality (events, state, lifecycle)
- UIWidget : DOM manipulation and visual behavior
- InteractiveWidget : Event handling and user interaction
-
-
-
DocumentNavigator extends UIWidget directly since it doesn't require complex interaction handling beyond basic click and keyboard events.
-
-
-
2.1.1 Event System
-
-
The widget uses a custom event system built on the native EventTarget API. This allows for clean separation of concerns and easy integration with other components.
-
-
Key events emitted by DocumentNavigator:
-
-
- rendered - Widget has been rendered to DOM
- navigate - User navigated to a heading
- toggle - Widget was expanded or collapsed
- theme-changed - Theme was changed
- destroyed - Widget was destroyed
-
-
-
-
2.1.2 State Management
-
-
State management is handled through a simple Map-based system that provides reactive updates and event emission when state changes occur.
-
-
This approach is lightweight but powerful enough for most widget use cases while remaining debuggable and predictable.
-
-
-
2.2 Plugin System Integration
-
-
While the current implementation works standalone, it's designed to integrate seamlessly with our planned plugin system. The plugin definition includes:
-
-
- Metadata and versioning information
- Dependency declarations
- Default configuration options
- Lifecycle hooks
- Theme variants
- Development helpers
-
-
-
-
3. Usage Examples
-
-
The DocumentNavigator can be used in several ways, from simple instantiation to advanced configuration with custom themes and behavior.
-
-
-
3.1 Basic Usage
-
-
The simplest way to use DocumentNavigator is with default settings:
-
-
const navigator = new DocumentNavigator();
-await navigator.initialize();
-await navigator.render();
-
-
This creates a navigator with default settings that will scan the entire document for headings and display them in a collapsible panel on the left side.
-
-
-
3.2 Advanced Configuration
-
-
For more control, you can specify detailed configuration options:
-
-
const navigator = new DocumentNavigator({
- position: 'right',
- collapsed: false,
- theme: 'dark',
- maxHeadingLevel: 4,
- enableScrollSpy: true,
- smoothScroll: true
-});
-
-
This creates a navigator on the right side that starts expanded, includes H4 headings, and uses the dark theme.
-
-
-
3.2.1 Custom Theming
-
-
The navigator supports multiple built-in themes and can be extended with custom themes. The theming system integrates with MarkiTect's document themes for consistent styling.
-
-
Available themes include default, dark, and minimal, each optimized for different use cases and aesthetics.
-
-
-
4. Testing and Quality
-
-
The DocumentNavigator implementation follows Test-Driven Development (TDD) methodology with comprehensive test coverage ensuring reliability and maintainability.
-
-
-
4.1 Test Coverage
-
-
Our test suite covers all major functionality:
-
-
- Widget instantiation and configuration
- DOM rendering and element creation
- Heading extraction and hierarchy building
- Navigation and smooth scrolling
- Expand/collapse animations
- Scroll spy functionality
- Responsive behavior
- Keyboard navigation
- Event emission
- Edge cases and error handling
-
-
-
-
-
-
The navigator is optimized for performance with several key strategies:
-
-
- Throttled Scroll Events : Scroll spy updates are throttled to 100ms intervals
- Efficient DOM Queries : Heading extraction is done once and cached
- Conditional Rendering : Navigator only renders if minimum heading count is met
- Memory Management : Proper cleanup prevents memory leaks
- Responsive Loading : Navigator automatically hides on mobile to save resources
-
-
-
-
5. Conclusion
-
-
The DocumentNavigator widget successfully brings Substack-style navigation to MarkiTect documents. It provides an intuitive, accessible, and performant way for users to navigate long-form content.
-
-
The implementation demonstrates the power of our widget architecture approach, with clean separation of concerns, comprehensive testing, and excellent extensibility for future enhancements.
-
-
Scroll back to the top and try the navigation features! The hamburger menu should be visible on the left side of your screen.
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/markitect/static/js/tests/test-real-user-functionality.js b/markitect/static/js/tests/test-real-user-functionality.js
deleted file mode 100644
index 3d7fddef..00000000
--- a/markitect/static/js/tests/test-real-user-functionality.js
+++ /dev/null
@@ -1,285 +0,0 @@
-#!/usr/bin/env node
-
-/**
- * Real User Functionality Tests
- *
- * This test file validates the actual functionality that users experience,
- * not just internal API calls. It tests the complete user workflow.
- */
-
-const RefactorTestRunner = require('./refactor-test-runner.js');
-
-const runner = new RefactorTestRunner();
-
-runner.describe('Real User Functionality Tests', () => {
-
- runner.it('should allow users to edit content and see changes in DOM', () => {
- // Load all extracted components
- const sectionModule = require('../core/section-manager.js');
- const domModule = require('../components/dom-renderer.js');
- const debugModule = require('../components/debug-panel.js');
- const controlsModule = require('../components/document-controls.js');
-
- const { SectionManager } = sectionModule;
- const { DOMRenderer } = domModule;
- const { DebugPanel } = debugModule;
- const { DocumentControls } = controlsModule;
-
- // Setup DOM container
- const container = document.createElement('div');
- container.innerHTML = '
';
- document.body.appendChild(container);
-
- // Create components
- const sectionManager = new SectionManager();
- const domRenderer = new DOMRenderer(sectionManager, container);
- const debugPanel = new DebugPanel();
- const documentControls = new DocumentControls();
-
- // Setup document controls
- documentControls.create();
-
- // Create sections from test markdown
- const testMarkdown = `# Original Title\nOriginal content that should be editable.`;
- const sections = sectionManager.createSectionsFromMarkdown(testMarkdown);
- domRenderer.renderAllSections(sections);
-
- const firstSection = sections[0];
- const sectionElement = container.querySelector(`[data-section-id="${firstSection.id}"]`);
-
- // Verify original content is rendered
- runner.expect(sectionElement.innerHTML).toContain('Original Title');
-
- // Simulate user clicking on section
- const clickEvent = new Event('click', { bubbles: true });
- sectionElement.dispatchEvent(clickEvent);
-
- // Verify editing state is active
- runner.expect(firstSection.isEditing()).toBeTruthy();
-
- // Find the floating menu and edit controls
- const floatingMenu = document.querySelector('.ui-edit-floating-menu');
- runner.expect(floatingMenu).toBeTruthy();
-
- const textarea = floatingMenu.querySelector('textarea');
- const acceptButton = Array.from(floatingMenu.querySelectorAll('button')).find(btn => btn.textContent.includes('Accept'));
-
- runner.expect(textarea).toBeTruthy();
- runner.expect(acceptButton).toBeTruthy();
-
- // Simulate user editing content
- const newContent = '# Updated Title\nCompletely new content added by user.';
- textarea.value = newContent;
-
- // Simulate user clicking accept
- acceptButton.click();
-
- // Verify section is no longer editing
- runner.expect(firstSection.isEditing()).toBeFalsy();
-
- // Verify floating menu is gone
- const menuAfterAccept = document.querySelector('.ui-edit-floating-menu');
- runner.expect(menuAfterAccept).toBeFalsy();
-
- // CRITICAL TEST: Verify DOM was actually updated with new content
- const updatedElement = container.querySelector(`[data-section-id="${firstSection.id}"]`);
- runner.expect(updatedElement.innerHTML).toContain('Updated Title');
- runner.expect(updatedElement.innerHTML).toContain('Completely new content');
- runner.expect(updatedElement.innerHTML).not.toContain('Original Title');
-
- // Cleanup
- document.body.removeChild(container);
- documentControls.destroy();
- });
-
- runner.it('should allow users to reset all changes', () => {
- // Setup similar to above
- const sectionModule = require('../core/section-manager.js');
- const domModule = require('../components/dom-renderer.js');
- const controlsModule = require('../components/document-controls.js');
-
- const { SectionManager } = sectionModule;
- const { DOMRenderer } = domModule;
- const { DocumentControls } = controlsModule;
-
- const container = document.createElement('div');
- container.innerHTML = '
';
- document.body.appendChild(container);
-
- const sectionManager = new SectionManager();
- const domRenderer = new DOMRenderer(sectionManager, container);
- const documentControls = new DocumentControls();
-
- documentControls.create();
-
- // Create and modify content
- const testMarkdown = `# Test Section\nOriginal content for reset test.`;
- const sections = sectionManager.createSectionsFromMarkdown(testMarkdown);
- domRenderer.renderAllSections(sections);
-
- const firstSection = sections[0];
-
- // Make changes to the section
- sectionManager.startEditing(firstSection.id);
- sectionManager.updateContent(firstSection.id, '# Modified Title\nModified content.');
- sectionManager.acceptChanges(firstSection.id);
-
- // Verify changes are applied
- let sectionElement = container.querySelector(`[data-section-id="${firstSection.id}"]`);
- runner.expect(sectionElement.innerHTML).toContain('Modified Title');
- runner.expect(firstSection.hasChanges()).toBeTruthy();
-
- // Test reset functionality
- const resetButton = documentControls.getButton('reset-all');
- runner.expect(resetButton).toBeTruthy();
-
- // Click reset button
- resetButton.click();
-
- // Verify content is reset
- sectionElement = container.querySelector(`[data-section-id="${firstSection.id}"]`);
- runner.expect(sectionElement.innerHTML).toContain('Test Section');
- runner.expect(sectionElement.innerHTML).not.toContain('Modified Title');
- runner.expect(firstSection.hasChanges()).toBeFalsy();
-
- // Cleanup
- document.body.removeChild(container);
- documentControls.destroy();
- });
-
- runner.it('should handle cancel operations correctly', () => {
- const sectionModule = require('../core/section-manager.js');
- const domModule = require('../components/dom-renderer.js');
-
- const { SectionManager } = sectionModule;
- const { DOMRenderer } = domModule;
-
- const container = document.createElement('div');
- container.innerHTML = '
';
- document.body.appendChild(container);
-
- const sectionManager = new SectionManager();
- const domRenderer = new DOMRenderer(sectionManager, container);
-
- const testMarkdown = `# Cancel Test\nContent that should remain unchanged.`;
- const sections = sectionManager.createSectionsFromMarkdown(testMarkdown);
- domRenderer.renderAllSections(sections);
-
- const firstSection = sections[0];
- const originalContent = firstSection.currentMarkdown;
-
- // Start editing
- const sectionElement = container.querySelector(`[data-section-id="${firstSection.id}"]`);
- sectionElement.click();
-
- // Make changes but cancel them
- const floatingMenu = document.querySelector('.ui-edit-floating-menu');
- const textarea = floatingMenu.querySelector('textarea');
- const cancelButton = Array.from(floatingMenu.querySelectorAll('button')).find(btn => btn.textContent.includes('Cancel'));
-
- textarea.value = '# This should be cancelled\nThis content should not appear.';
- cancelButton.click();
-
- // Verify content is unchanged
- const unchangedElement = container.querySelector(`[data-section-id="${firstSection.id}"]`);
- runner.expect(unchangedElement.innerHTML).toContain('Cancel Test');
- runner.expect(unchangedElement.innerHTML).not.toContain('This should be cancelled');
- runner.expect(firstSection.currentMarkdown).toBe(originalContent);
-
- // Cleanup
- document.body.removeChild(container);
- });
-
- runner.it('should validate the complete editing workflow', () => {
- // This test validates the entire user experience end-to-end
- const sectionModule = require('../core/section-manager.js');
- const domModule = require('../components/dom-renderer.js');
- const debugModule = require('../components/debug-panel.js');
- const controlsModule = require('../components/document-controls.js');
-
- const { SectionManager } = sectionModule;
- const { DOMRenderer } = domModule;
- const { DebugPanel } = debugModule;
- const { DocumentControls } = controlsModule;
-
- const container = document.createElement('div');
- container.innerHTML = '
';
- document.body.appendChild(container);
-
- const sectionManager = new SectionManager();
- const domRenderer = new DOMRenderer(sectionManager, container);
- const debugPanel = new DebugPanel();
- const documentControls = new DocumentControls();
-
- documentControls.create();
-
- // Multi-section document
- const testMarkdown = `# Document Title
-Introduction paragraph.
-
-## Section A
-Content for section A.
-
-## Section B
-Content for section B.`;
-
- const sections = sectionManager.createSectionsFromMarkdown(testMarkdown);
- domRenderer.renderAllSections(sections);
-
- // Verify all sections are rendered
- const renderedSections = container.querySelectorAll('.ui-edit-section');
- runner.expect(renderedSections.length).toBe(sections.length);
-
- // Test editing multiple sections
- const firstSection = sections[0];
- const secondSection = sections[2]; // Section A
-
- // Edit first section
- renderedSections[0].click();
- let floatingMenu = document.querySelector('.ui-edit-floating-menu');
- let textarea = floatingMenu.querySelector('textarea');
- let acceptButton = Array.from(floatingMenu.querySelectorAll('button')).find(btn => btn.textContent.includes('Accept'));
-
- textarea.value = '# Updated Document Title\nUpdated introduction.';
- acceptButton.click();
-
- // Edit second section
- renderedSections[2].click();
- floatingMenu = document.querySelector('.ui-edit-floating-menu');
- textarea = floatingMenu.querySelector('textarea');
- acceptButton = Array.from(floatingMenu.querySelectorAll('button')).find(btn => btn.textContent.includes('Accept'));
-
- textarea.value = '## Updated Section A\nCompletely new content for section A.';
- acceptButton.click();
-
- // Verify both sections were updated
- const updatedSections = container.querySelectorAll('.ui-edit-section');
- runner.expect(updatedSections[0].innerHTML).toContain('Updated Document Title');
- runner.expect(updatedSections[2].innerHTML).toContain('Updated Section A');
-
- // Test reset restores all sections
- const resetButton = documentControls.getButton('reset-all');
- resetButton.click();
-
- const resetSections = container.querySelectorAll('.ui-edit-section');
- runner.expect(resetSections[0].innerHTML).toContain('Document Title');
- runner.expect(resetSections[0].innerHTML).not.toContain('Updated Document Title');
- runner.expect(resetSections[2].innerHTML).toContain('Section A');
- runner.expect(resetSections[2].innerHTML).not.toContain('Updated Section A');
-
- // Cleanup
- document.body.removeChild(container);
- documentControls.destroy();
- });
-});
-
-module.exports = runner;
-
-// Run tests if called directly
-if (require.main === module) {
- console.log('๐งช Running Real User Functionality Tests');
- runner.run().then(() => {
- console.log('โ
Real user functionality tests completed');
- console.log('These tests validate what users actually experience, not just internal APIs');
- });
-}
\ No newline at end of file
diff --git a/markitect/static/js/tests/test-section-manager-extraction.js b/markitect/static/js/tests/test-section-manager-extraction.js
deleted file mode 100644
index 1eecce5d..00000000
--- a/markitect/static/js/tests/test-section-manager-extraction.js
+++ /dev/null
@@ -1,196 +0,0 @@
-#!/usr/bin/env node
-
-/**
- * TDD Test for SectionManager Component Extraction
- *
- * Tests the extraction of SectionManager from the monolithic editor.js
- * Ensures all functionality is preserved during refactoring.
- */
-
-const RefactorTestRunner = require('./refactor-test-runner.js');
-
-const runner = new RefactorTestRunner();
-
-// First, let's define what the SectionManager API should look like
-const EXPECTED_SECTION_MANAGER_API = [
- 'constructor',
- 'createSectionsFromMarkdown',
- 'startEditing',
- 'stopEditing',
- 'getAllSections',
- 'sections', // Map property, not method
- 'getDocumentStatus',
- 'getDocumentMarkdown',
- 'on', // event system
- 'emit', // event system
- 'handleSectionSplit',
- 'updateContent',
- 'acceptChanges',
- 'cancelChanges',
- 'resetSection'
-];
-
-runner.describe('SectionManager Component Extraction', () => {
-
- runner.it('should define expected API methods', () => {
- // This test defines what we expect from the extracted SectionManager
- const expectedMethods = EXPECTED_SECTION_MANAGER_API;
- runner.expect(expectedMethods.length).toBe(15);
- runner.expect(expectedMethods).toContain('createSectionsFromMarkdown');
- runner.expect(expectedMethods).toContain('startEditing');
- runner.expect(expectedMethods).toContain('stopEditing');
- });
-
- runner.it('should extract from monolithic editor.js', () => {
- // Load the monolithic editor.js to extract SectionManager
- delete require.cache[require.resolve('/home/worsch/markitect_project/markitect/static/editor.js')];
-
- try {
- const editorModule = require('/home/worsch/markitect_project/markitect/static/editor.js');
- runner.expect(editorModule.SectionManager).toBeTruthy();
- // Set global for other tests
- global.SectionManager = editorModule.SectionManager;
- global.Section = editorModule.Section;
- global.EditState = editorModule.EditState;
- } catch (error) {
- throw new Error(`Failed to load monolithic editor.js: ${error.message}`);
- }
- });
-
- runner.it('should preserve SectionManager constructor functionality', () => {
- const SectionManager = global.SectionManager;
-
- const manager = new SectionManager();
- runner.expect(manager).toBeInstanceOf(SectionManager);
- runner.expect(manager.sections).toBeInstanceOf(Map);
- });
-
- runner.it('should preserve createSectionsFromMarkdown functionality', () => {
- const SectionManager = global.SectionManager;
- const manager = new SectionManager();
-
- const testMarkdown = `# Heading 1\nContent 1\n\n## Heading 2\nContent 2`;
- const sections = manager.createSectionsFromMarkdown(testMarkdown);
-
- runner.expect(Array.isArray(sections)).toBeTruthy();
- runner.expect(sections.length).toBe(2);
- runner.expect(sections[0].currentMarkdown).toContain('Heading 1');
- runner.expect(sections[1].currentMarkdown).toContain('Heading 2');
- });
-
- runner.it('should preserve section editing state management', () => {
- const SectionManager = global.SectionManager;
- const manager = new SectionManager();
-
- const sections = manager.createSectionsFromMarkdown('# Test\nContent');
- const sectionId = sections[0].id;
-
- // Test start editing
- runner.expect(manager.startEditing(sectionId)).toBeTruthy();
- const section = manager.sections.get(sectionId);
- runner.expect(section.isEditing()).toBeTruthy();
-
- // Test stop editing
- section.stopEditing();
- runner.expect(section.isEditing()).toBeFalsy();
- });
-
- runner.it('should preserve event system functionality', () => {
- const SectionManager = global.SectionManager;
- const manager = new SectionManager();
-
- let eventFired = false;
- let eventData = null;
-
- manager.on('test-event', (data) => {
- eventFired = true;
- eventData = data;
- });
-
- manager.emit('test-event', { test: 'data' });
-
- runner.expect(eventFired).toBeTruthy();
- runner.expect(eventData).toEqual({ test: 'data' });
- });
-
- runner.it('should preserve document status functionality', () => {
- const SectionManager = global.SectionManager;
- const manager = new SectionManager();
-
- manager.createSectionsFromMarkdown('# Test\nContent');
- const status = manager.getDocumentStatus();
-
- runner.expect(status).toHaveProperty('totalSections');
- runner.expect(status).toHaveProperty('editingSections');
- runner.expect(status.totalSections).toBe(1);
- });
-
- runner.it('should preserve getAllSections functionality', () => {
- const SectionManager = global.SectionManager;
- const manager = new SectionManager();
-
- const testMarkdown = '# One\nContent\n\n# Two\nMore content';
- manager.createSectionsFromMarkdown(testMarkdown);
-
- const allSections = manager.getAllSections();
- runner.expect(Array.isArray(allSections)).toBeTruthy();
- runner.expect(allSections.length).toBe(2);
- });
-
- runner.it('should preserve section splitting functionality', () => {
- const SectionManager = global.SectionManager;
- const manager = new SectionManager();
-
- const sections = manager.createSectionsFromMarkdown('# Original\nContent');
- const sectionId = sections[0].id;
-
- const newContent = '# Split 1\nContent 1\n\n# Split 2\nContent 2';
- const newSections = manager.handleSectionSplit(sectionId, newContent);
-
- runner.expect(Array.isArray(newSections)).toBeTruthy();
- runner.expect(newSections.length).toBe(2);
- runner.expect(manager.sections.has(sectionId)).toBeFalsy(); // Original removed
- });
-});
-
-// Export API tests for use during extraction
-const SECTION_MANAGER_API_TESTS = [
- (SectionManager) => {
- const manager = new SectionManager();
- if (!manager.sections || !(manager.sections instanceof Map)) {
- throw new Error('sections property missing or not a Map');
- }
- },
- (SectionManager) => {
- const manager = new SectionManager();
- if (typeof manager.createSectionsFromMarkdown !== 'function') {
- throw new Error('createSectionsFromMarkdown method missing');
- }
- },
- (SectionManager) => {
- const manager = new SectionManager();
- if (typeof manager.startEditing !== 'function') {
- throw new Error('startEditing method missing');
- }
- },
- (SectionManager) => {
- const manager = new SectionManager();
- if (typeof manager.stopEditing !== 'function') {
- throw new Error('stopEditing method missing');
- }
- }
-];
-
-module.exports = {
- runner,
- EXPECTED_SECTION_MANAGER_API,
- SECTION_MANAGER_API_TESTS
-};
-
-// Run tests if called directly
-if (require.main === module) {
- console.log('๐งช Testing SectionManager Component Extraction');
- runner.run().then(() => {
- console.log('โ
SectionManager extraction tests completed');
- });
-}
\ No newline at end of file
diff --git a/markitect/static/js/tests/test.md b/markitect/static/js/tests/test.md
deleted file mode 100644
index 239c58bf..00000000
--- a/markitect/static/js/tests/test.md
+++ /dev/null
@@ -1,6 +0,0 @@
-# Test Document
-
-This is a test document to check if UI controls appear in edit mode.
-
-## Section 1
-Some content here.
diff --git a/markitect/static/js/tests/test_edit.html b/markitect/static/js/tests/test_edit.html
deleted file mode 100644
index 813b65bf..00000000
--- a/markitect/static/js/tests/test_edit.html
+++ /dev/null
@@ -1,149 +0,0 @@
-
-
-
-
-
-
- Test Document
-
-
-
-
-
-
-
-
-
-
-
-
-
Test Document
-
This is a test document to check if UI controls appear in edit mode.
-
Section 1
-
Some content here.
-
-
-- html from markdown by MarkiTect on 2025-11-11 23:42:23 by worsch
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/markitect/static/js/widgets/base/UIWidget.js b/markitect/static/js/widgets/base/UIWidget.js
deleted file mode 100644
index c889d0d0..00000000
--- a/markitect/static/js/widgets/base/UIWidget.js
+++ /dev/null
@@ -1,215 +0,0 @@
-/**
- * UI Widget Base Class
- *
- * Extends Widget with DOM manipulation and visual functionality.
- * Base for all widgets that render UI elements.
- */
-import { Widget } from './Widget.js';
-
-export class UIWidget extends Widget {
- constructor(options = {}) {
- super(options);
-
- // UI properties
- this.element = null;
- this.isVisible = false;
- this.isRendered = false;
- this.theme = options.theme || 'default';
- this.cssClasses = new Set(['markitect-widget']);
-
- // Animation support
- this.animationDuration = options.animationDuration || 300;
- this.enableAnimations = options.enableAnimations !== false;
- }
-
- /**
- * Render the widget to DOM (abstract method)
- */
- async render() {
- throw new Error('render() method must be implemented by subclass');
- }
-
- /**
- * Show the widget
- */
- async show(options = {}) {
- if (!this.isRendered) {
- await this.render();
- }
-
- if (this.isVisible) {
- return this;
- }
-
- this.isVisible = true;
-
- if (this.element) {
- if (this.enableAnimations && !options.immediate) {
- await this.animateShow();
- } else {
- this.element.style.display = '';
- }
- }
-
- this.emit('shown');
- return this;
- }
-
- /**
- * Hide the widget
- */
- async hide(options = {}) {
- if (!this.isVisible) {
- return this;
- }
-
- this.isVisible = false;
-
- if (this.element) {
- if (this.enableAnimations && !options.immediate) {
- await this.animateHide();
- } else {
- this.element.style.display = 'none';
- }
- }
-
- this.emit('hidden');
- return this;
- }
-
- /**
- * Toggle visibility
- */
- async toggle(options = {}) {
- return this.isVisible ? this.hide(options) : this.show(options);
- }
-
- /**
- * Show animation (override for custom animations)
- */
- async animateShow() {
- if (!this.element) return;
-
- return new Promise(resolve => {
- this.element.style.transition = `opacity ${this.animationDuration}ms ease-in-out`;
- this.element.style.opacity = '0';
- this.element.style.display = '';
-
- // Force reflow
- this.element.offsetHeight;
-
- this.element.style.opacity = '1';
-
- setTimeout(() => {
- this.element.style.transition = '';
- resolve();
- }, this.animationDuration);
- });
- }
-
- /**
- * Hide animation (override for custom animations)
- */
- async animateHide() {
- if (!this.element) return;
-
- return new Promise(resolve => {
- this.element.style.transition = `opacity ${this.animationDuration}ms ease-in-out`;
- this.element.style.opacity = '0';
-
- setTimeout(() => {
- this.element.style.display = 'none';
- this.element.style.transition = '';
- this.element.style.opacity = '';
- resolve();
- }, this.animationDuration);
- });
- }
-
- /**
- * CSS class management
- */
- addClass(className) {
- this.cssClasses.add(className);
- if (this.element) {
- this.element.classList.add(className);
- }
- return this;
- }
-
- removeClass(className) {
- this.cssClasses.delete(className);
- if (this.element) {
- this.element.classList.remove(className);
- }
- return this;
- }
-
- hasClass(className) {
- return this.cssClasses.has(className);
- }
-
- /**
- * Apply theme styling
- */
- applyTheme(themeName) {
- const oldTheme = this.theme;
- this.theme = themeName;
-
- this.removeClass(`theme-${oldTheme}`);
- this.addClass(`theme-${themeName}`);
-
- this.emit('theme-changed', { oldTheme, newTheme: themeName });
- return this;
- }
-
- /**
- * Find child element by selector
- */
- findElement(selector) {
- return this.element ? this.element.querySelector(selector) : null;
- }
-
- /**
- * Find all child elements by selector
- */
- findElements(selector) {
- return this.element ? this.element.querySelectorAll(selector) : [];
- }
-
- /**
- * Override destroy to clean up DOM
- */
- async destroy() {
- if (this.element && this.element.parentNode) {
- this.element.parentNode.removeChild(this.element);
- }
-
- this.element = null;
- this.isRendered = false;
- this.isVisible = false;
-
- await super.destroy();
- }
-
- /**
- * Apply all CSS classes to element
- */
- applyCSSClasses(element = this.element) {
- if (element) {
- element.className = Array.from(this.cssClasses).join(' ');
- }
- }
-
- /**
- * Default configuration for UI widgets
- */
- getDefaultConfig() {
- return {
- ...super.getDefaultConfig(),
- theme: 'default',
- animationDuration: 300,
- enableAnimations: true
- };
- }
-}
\ No newline at end of file
diff --git a/markitect/static/js/widgets/base/Widget.js b/markitect/static/js/widgets/base/Widget.js
deleted file mode 100644
index 1c284cf6..00000000
--- a/markitect/static/js/widgets/base/Widget.js
+++ /dev/null
@@ -1,141 +0,0 @@
-/**
- * Base Widget Class
- *
- * Foundation class for all Markitect UI widgets following the plugin architecture.
- * Provides core functionality for event handling, state management, and lifecycle.
- */
-export class Widget extends EventTarget {
- constructor(options = {}) {
- super();
-
- // Core properties
- this.id = options.id || `widget-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
- this.container = options.container || document.body;
- this.config = { ...this.getDefaultConfig(), ...options };
-
- // State management
- this.state = new Map();
- this.isInitialized = false;
- this.isDestroyed = false;
-
- // Mixin support
- this.mixins = [];
-
- // Lifecycle hooks
- this.onInitialize = options.onInitialize || (() => {});
- this.onDestroy = options.onDestroy || (() => {});
- }
-
- /**
- * Initialize the widget
- */
- async initialize() {
- if (this.isInitialized || this.isDestroyed) {
- return this;
- }
-
- try {
- await this.onInitialize(this);
- this.isInitialized = true;
- this.emit('initialized');
- return this;
- } catch (error) {
- this.emit('error', { phase: 'initialize', error });
- throw error;
- }
- }
-
- /**
- * Destroy the widget and clean up resources
- */
- async destroy() {
- if (this.isDestroyed) {
- return;
- }
-
- try {
- await this.onDestroy(this);
- this.isDestroyed = true;
- this.emit('destroyed');
- } catch (error) {
- this.emit('error', { phase: 'destroy', error });
- throw error;
- }
- }
-
- /**
- * State management
- */
- setState(key, value) {
- const oldValue = this.state.get(key);
- this.state.set(key, value);
- this.emit('state-changed', { key, value, oldValue });
- }
-
- getState(key, defaultValue = null) {
- return this.state.get(key) ?? defaultValue;
- }
-
- /**
- * Event emission wrapper
- */
- emit(eventType, data = {}) {
- const event = new CustomEvent(eventType, {
- detail: { widget: this, ...data }
- });
- this.dispatchEvent(event);
- }
-
- /**
- * Apply mixin functionality
- */
- applyMixin(mixin) {
- if (typeof mixin === 'object') {
- Object.assign(this, mixin);
- this.mixins.push(mixin);
- }
- return this;
- }
-
- /**
- * Default configuration (override in subclasses)
- */
- getDefaultConfig() {
- return {};
- }
-
- /**
- * Utility method for creating DOM elements with styling
- */
- createElement(tag, options = {}) {
- const element = document.createElement(tag);
-
- if (options.className) {
- element.className = options.className;
- }
-
- if (options.textContent) {
- element.textContent = options.textContent;
- }
-
- if (options.innerHTML) {
- element.innerHTML = options.innerHTML;
- }
-
- if (options.style) {
- if (typeof options.style === 'string') {
- element.style.cssText = options.style;
- } else {
- Object.assign(element.style, options.style);
- }
- }
-
- if (options.attributes) {
- Object.entries(options.attributes).forEach(([key, value]) => {
- element.setAttribute(key, value);
- });
- }
-
- return element;
- }
-}
\ No newline at end of file
diff --git a/markitect/static/js/widgets/navigation/DocumentNavigator.js b/markitect/static/js/widgets/navigation/DocumentNavigator.js
deleted file mode 100644
index d25e058e..00000000
--- a/markitect/static/js/widgets/navigation/DocumentNavigator.js
+++ /dev/null
@@ -1,625 +0,0 @@
-/**
- * DocumentNavigator Widget
- *
- * Substack-style floating document navigation widget that displays a hierarchical
- * table of contents based on document headings. Supports smooth scrolling,
- * scroll spy, expand/collapse, and responsive behavior.
- */
-import { UIWidget } from '../base/UIWidget.js';
-
-export class DocumentNavigator extends UIWidget {
- constructor(options = {}) {
- super(options);
-
- // Navigation state
- this.isCollapsed = this.config.collapsed;
- this.currentSection = null;
- this.headings = [];
- this.navigationTree = [];
-
- // Scroll spy state
- this.scrollSpyEnabled = this.config.enableScrollSpy;
- this.scrollThrottle = null;
-
- // Event bindings
- this.boundScrollHandler = this.handleScroll.bind(this);
- this.boundResizeHandler = this.handleResize.bind(this);
-
- // Initialize responsive behavior
- this.mediaQuery = window.matchMedia('(max-width: 768px)');
- }
-
- getDefaultConfig() {
- return {
- ...super.getDefaultConfig(),
- position: 'left', // 'left' or 'right'
- collapsed: true, // Start collapsed
- autoHide: true, // Hide on mobile
- maxHeadingLevel: 3, // H1, H2, H3
- enableScrollSpy: true, // Highlight current section
- smoothScroll: true, // Smooth scroll behavior
- animationDuration: 300, // Animation timing
- minHeadings: 2, // Min headings to show navigator
- theme: 'default', // Theme support
-
- // Styling options
- width: '280px',
- collapsedWidth: '40px',
- offset: { top: '80px', side: '20px' },
-
- // Accessibility
- enableKeyboard: true,
- ariaLabel: 'Document Navigation'
- };
- }
-
- async initialize() {
- await super.initialize();
-
- // Extract headings from container
- this.extractHeadings();
- this.buildNavigationTree();
-
- // Set up event listeners
- if (this.scrollSpyEnabled) {
- window.addEventListener('scroll', this.boundScrollHandler, { passive: true });
- }
-
- if (this.config.autoHide) {
- window.addEventListener('resize', this.boundResizeHandler);
- this.handleResize(); // Initial check
- }
-
- return this;
- }
-
- async render() {
- if (this.isRendered) {
- return this.element;
- }
-
- // Check if we have enough headings
- if (this.headings.length < this.config.minHeadings) {
- this.isRendered = true;
- return null; // Don't render if too few headings
- }
-
- // Create main container
- this.element = this.createElement('nav', {
- className: 'document-navigator markitect-widget',
- attributes: {
- 'aria-label': this.config.ariaLabel,
- 'role': 'navigation'
- },
- style: this.getNavigatorStyle()
- });
-
- // Apply CSS classes
- this.applyCSSClasses();
- this.addClass('theme-' + this.theme);
- this.addClass('position-' + this.config.position);
-
- // Create toggle button (always visible)
- this.createToggleButton();
-
- // Create navigation list (hidden when collapsed)
- this.createNavigationList();
-
- // Set initial visibility state
- if (this.isCollapsed) {
- await this.collapse({ immediate: true });
- } else {
- await this.expand({ immediate: true });
- }
-
- // Append to container
- this.container.appendChild(this.element);
-
- // Initialize scroll spy
- if (this.scrollSpyEnabled) {
- this.updateCurrentSection();
- }
-
- this.isRendered = true;
- this.emit('rendered');
-
- return this.element;
- }
-
- createToggleButton() {
- this.toggleButton = this.createElement('button', {
- className: 'navigator-toggle',
- attributes: {
- 'type': 'button',
- 'aria-label': this.isCollapsed ? 'Expand navigation' : 'Collapse navigation',
- 'aria-expanded': !this.isCollapsed
- },
- innerHTML: this.getToggleIcon(),
- style: this.getToggleStyle()
- });
-
- // Toggle on click
- this.toggleButton.addEventListener('click', async () => {
- await this.toggle();
- });
-
- // Keyboard support
- if (this.config.enableKeyboard) {
- this.toggleButton.addEventListener('keydown', this.handleKeyboard.bind(this));
- }
-
- this.element.appendChild(this.toggleButton);
- }
-
- createNavigationList() {
- this.navigationList = this.createElement('div', {
- className: 'navigator-list',
- style: this.getListStyle()
- });
-
- if (this.headings.length === 0) {
- this.createEmptyState();
- } else {
- this.populateNavigationList();
- }
-
- this.element.appendChild(this.navigationList);
- }
-
- createEmptyState() {
- const emptyMessage = this.createElement('div', {
- className: 'navigator-empty',
- textContent: 'No headings found',
- style: {
- padding: '1rem',
- textAlign: 'center',
- color: '#666',
- fontStyle: 'italic'
- }
- });
-
- this.navigationList.appendChild(emptyMessage);
- }
-
- populateNavigationList() {
- // Create header
- const header = this.createElement('div', {
- className: 'navigator-header',
- innerHTML: `
- Contents
- โ
- `,
- style: {
- display: 'flex',
- justifyContent: 'space-between',
- alignItems: 'center',
- padding: '1rem 1rem 0.5rem',
- borderBottom: '1px solid #eee',
- marginBottom: '0.5rem'
- }
- });
-
- // Close button functionality
- const closeButton = header.querySelector('.navigator-close');
- closeButton.addEventListener('click', async () => {
- await this.collapse();
- });
-
- this.navigationList.appendChild(header);
-
- // Create navigation items
- const navContainer = this.createElement('div', {
- className: 'navigator-items',
- style: {
- maxHeight: '70vh',
- overflowY: 'auto',
- padding: '0 0.5rem 1rem'
- }
- });
-
- this.renderNavigationTree(navContainer, this.navigationTree);
- this.navigationList.appendChild(navContainer);
- }
-
- renderNavigationTree(container, items, level = 0) {
- items.forEach(item => {
- const navItem = this.createElement('div', {
- className: `navigator-item level-${level}`,
- style: {
- marginLeft: `${level * 1}rem`,
- marginBottom: '0.25rem'
- }
- });
-
- // Create clickable link
- const link = this.createElement('a', {
- className: 'navigator-link',
- textContent: item.text,
- attributes: {
- 'href': `#${item.id}`,
- 'data-target': item.id,
- 'data-level': item.level,
- 'role': 'button',
- 'tabindex': '0'
- },
- style: {
- display: 'block',
- padding: '0.5rem 0.75rem',
- textDecoration: 'none',
- color: '#333',
- borderRadius: '4px',
- fontSize: level === 0 ? '0.9rem' : '0.8rem',
- fontWeight: level === 0 ? '600' : '400',
- transition: 'all 0.2s ease',
- cursor: 'pointer'
- }
- });
-
- // Hover effects
- link.addEventListener('mouseenter', () => {
- link.style.backgroundColor = '#f0f0f0';
- });
-
- link.addEventListener('mouseleave', () => {
- if (!link.classList.contains('active')) {
- link.style.backgroundColor = '';
- }
- });
-
- // Click navigation
- link.addEventListener('click', (e) => {
- e.preventDefault();
- this.navigateToHeading(item.id);
- });
-
- navItem.appendChild(link);
-
- // Render children recursively
- if (item.children && item.children.length > 0) {
- this.renderNavigationTree(navItem, item.children, level + 1);
- }
-
- container.appendChild(navItem);
- });
- }
-
- extractHeadings() {
- const headingSelectors = [];
- for (let i = 1; i <= this.config.maxHeadingLevel; i++) {
- headingSelectors.push(`h${i}`);
- }
-
- const headingElements = this.container.querySelectorAll(headingSelectors.join(', '));
-
- this.headings = Array.from(headingElements).map((heading, index) => {
- // Ensure heading has an ID
- if (!heading.id) {
- heading.id = `heading-${index + 1}`;
- }
-
- return {
- element: heading,
- id: heading.id,
- text: heading.textContent.trim(),
- level: parseInt(heading.tagName.substring(1)),
- offset: heading.offsetTop
- };
- });
-
- return this.headings;
- }
-
- buildNavigationTree() {
- this.navigationTree = [];
- const stack = [];
-
- this.headings.forEach(heading => {
- const item = {
- ...heading,
- children: []
- };
-
- // Find correct parent based on heading level
- while (stack.length > 0 && stack[stack.length - 1].level >= heading.level) {
- stack.pop();
- }
-
- if (stack.length === 0) {
- // Top level item
- this.navigationTree.push(item);
- } else {
- // Child item
- stack[stack.length - 1].children.push(item);
- }
-
- stack.push(item);
- });
-
- return this.navigationTree;
- }
-
- async toggle(options = {}) {
- return this.isCollapsed ? this.expand(options) : this.collapse(options);
- }
-
- async expand(options = {}) {
- if (!this.isCollapsed) {
- return this;
- }
-
- this.isCollapsed = false;
-
- if (this.toggleButton) {
- this.toggleButton.setAttribute('aria-expanded', 'true');
- this.toggleButton.setAttribute('aria-label', 'Collapse navigation');
- this.toggleButton.innerHTML = this.getToggleIcon();
- }
-
- if (this.navigationList) {
- if (this.enableAnimations && !options.immediate) {
- await this.animateExpand();
- } else {
- this.navigationList.style.display = '';
- this.element.style.width = this.config.width;
- }
- }
-
- this.emit('toggle', { expanded: true });
- return this;
- }
-
- async collapse(options = {}) {
- if (this.isCollapsed) {
- return this;
- }
-
- this.isCollapsed = true;
-
- if (this.toggleButton) {
- this.toggleButton.setAttribute('aria-expanded', 'false');
- this.toggleButton.setAttribute('aria-label', 'Expand navigation');
- this.toggleButton.innerHTML = this.getToggleIcon();
- }
-
- if (this.navigationList) {
- if (this.enableAnimations && !options.immediate) {
- await this.animateCollapse();
- } else {
- this.navigationList.style.display = 'none';
- this.element.style.width = this.config.collapsedWidth;
- }
- }
-
- this.emit('toggle', { expanded: false });
- return this;
- }
-
- async animateExpand() {
- return new Promise(resolve => {
- this.navigationList.style.opacity = '0';
- this.navigationList.style.display = '';
-
- // Animate width and opacity
- this.element.style.transition = `width ${this.animationDuration}ms ease-in-out`;
- this.navigationList.style.transition = `opacity ${this.animationDuration}ms ease-in-out`;
-
- // Force reflow
- this.element.offsetWidth;
-
- this.element.style.width = this.config.width;
- this.navigationList.style.opacity = '1';
-
- setTimeout(() => {
- this.element.style.transition = '';
- this.navigationList.style.transition = '';
- resolve();
- }, this.animationDuration);
- });
- }
-
- async animateCollapse() {
- return new Promise(resolve => {
- this.element.style.transition = `width ${this.animationDuration}ms ease-in-out`;
- this.navigationList.style.transition = `opacity ${this.animationDuration}ms ease-in-out`;
-
- this.navigationList.style.opacity = '0';
- this.element.style.width = this.config.collapsedWidth;
-
- setTimeout(() => {
- this.navigationList.style.display = 'none';
- this.element.style.transition = '';
- this.navigationList.style.transition = '';
- resolve();
- }, this.animationDuration);
- });
- }
-
- navigateToHeading(headingId) {
- const targetElement = document.getElementById(headingId);
- if (!targetElement) {
- console.warn(`Heading with ID '${headingId}' not found`);
- return;
- }
-
- // Update active navigation item
- this.setActiveItem(headingId);
-
- // Scroll to target
- if (this.config.smoothScroll) {
- targetElement.scrollIntoView({
- behavior: 'smooth',
- block: 'start',
- inline: 'nearest'
- });
- } else {
- targetElement.scrollIntoView();
- }
-
- // Emit navigation event
- this.emit('navigate', { target: headingId, element: targetElement });
-
- // Optionally collapse after navigation on mobile
- if (this.mediaQuery.matches && this.config.autoHide) {
- setTimeout(() => this.collapse(), 500);
- }
- }
-
- setActiveItem(headingId) {
- // Remove previous active state
- const previousActive = this.findElement('.navigator-link.active');
- if (previousActive) {
- previousActive.classList.remove('active');
- previousActive.style.backgroundColor = '';
- }
-
- // Set new active state
- const newActive = this.findElement(`[data-target="${headingId}"]`);
- if (newActive) {
- newActive.classList.add('active');
- newActive.style.backgroundColor = '#e3f2fd';
- newActive.style.color = '#1976d2';
- }
-
- this.currentSection = headingId;
- }
-
- handleScroll() {
- if (!this.scrollSpyEnabled || !this.isRendered) {
- return;
- }
-
- // Throttle scroll events
- if (this.scrollThrottle) {
- return;
- }
-
- this.scrollThrottle = setTimeout(() => {
- this.updateCurrentSection();
- this.scrollThrottle = null;
- }, 100);
- }
-
- updateCurrentSection() {
- const scrollPosition = window.pageYOffset + 100; // Offset for header
- let currentHeading = null;
-
- // Find the current heading based on scroll position
- for (let i = this.headings.length - 1; i >= 0; i--) {
- const heading = this.headings[i];
- if (heading.element.offsetTop <= scrollPosition) {
- currentHeading = heading;
- break;
- }
- }
-
- if (currentHeading && currentHeading.id !== this.currentSection) {
- this.setActiveItem(currentHeading.id);
- }
- }
-
- getCurrentSection() {
- return this.currentSection;
- }
-
- handleResize() {
- if (!this.config.autoHide) {
- return;
- }
-
- if (this.mediaQuery.matches) {
- // Mobile: hide navigator
- if (this.element) {
- this.element.style.display = 'none';
- }
- } else {
- // Desktop: show navigator
- if (this.element) {
- this.element.style.display = '';
- }
- }
- }
-
- handleKeyboard(event) {
- switch (event.key) {
- case 'Enter':
- case ' ':
- event.preventDefault();
- this.toggle();
- break;
- case 'Escape':
- event.preventDefault();
- this.collapse();
- break;
- }
- }
-
- getNavigatorStyle() {
- const baseStyle = {
- position: 'fixed',
- top: this.config.offset.top,
- zIndex: '1000',
- backgroundColor: 'rgba(255, 255, 255, 0.95)',
- border: '1px solid #e1e5e9',
- borderRadius: '8px',
- boxShadow: '0 2px 8px rgba(0, 0, 0, 0.15)',
- backdropFilter: 'blur(8px)',
- width: this.isCollapsed ? this.config.collapsedWidth : this.config.width,
- maxHeight: '80vh',
- overflow: 'hidden',
- transition: 'width 0.3s ease-in-out'
- };
-
- // Position-specific styling
- if (this.config.position === 'left') {
- baseStyle.left = this.config.offset.side;
- } else {
- baseStyle.right = this.config.offset.side;
- }
-
- return baseStyle;
- }
-
- getToggleStyle() {
- return {
- width: '100%',
- height: this.config.collapsedWidth,
- border: 'none',
- backgroundColor: 'transparent',
- cursor: 'pointer',
- display: 'flex',
- alignItems: 'center',
- justifyContent: 'center',
- fontSize: '16px',
- color: '#666',
- transition: 'color 0.2s ease'
- };
- }
-
- getListStyle() {
- return {
- display: this.isCollapsed ? 'none' : '',
- opacity: this.isCollapsed ? '0' : '1'
- };
- }
-
- getToggleIcon() {
- if (this.isCollapsed) {
- return this.config.position === 'left' ? 'โฐ' : 'โฐ';
- } else {
- return 'โ';
- }
- }
-
- async destroy() {
- // Remove event listeners
- window.removeEventListener('scroll', this.boundScrollHandler);
- window.removeEventListener('resize', this.boundResizeHandler);
-
- // Clear throttle
- if (this.scrollThrottle) {
- clearTimeout(this.scrollThrottle);
- }
-
- await super.destroy();
- }
-}
\ No newline at end of file