diff --git a/markitect/static/editor.js b/markitect/static/editor.js index 36511a52..4dfd8707 100644 --- a/markitect/static/editor.js +++ b/markitect/static/editor.js @@ -45,6 +45,8 @@ const SectionType = Object.freeze({ CODE: 'code', QUOTE: 'quote', IMAGE: 'image', + TABLE: 'table', + HR: 'hr', OTHER: 'other' }); @@ -65,22 +67,591 @@ class Section { this.created = new Date(); } - static generateId(markdown, position) { - const cleanText = markdown.replace(/[^a-zA-Z0-9]/g, ''); - const hash = cleanText.substring(0, 8) + position; - return `section-${hash}`; + /** + * 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) { - const trimmed = markdown.trim(); - if (trimmed.startsWith('#')) return SectionType.HEADING; - if (trimmed.startsWith('```')) return SectionType.CODE; - if (trimmed.startsWith('>')) return SectionType.QUOTE; - if (trimmed.includes('![')) return SectionType.IMAGE; - if (trimmed.startsWith('-') || trimmed.startsWith('*') || trimmed.startsWith('1.')) return SectionType.LIST; + 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`); @@ -204,6 +775,34 @@ class Section { } 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); + } } /** @@ -289,8 +888,36 @@ class SectionManager { if (!section) { throw new Error(`Section ${sectionId} not found`); } + + // Store old type for comparison + const oldType = section.type; + + // Update content section.updateContent(markdown); - this.emit('content-updated', { sectionId, markdown, section: section.getStatus() }); + + // 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) { @@ -341,6 +968,16 @@ class SectionManager { 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, '\\\\$&'); } @@ -469,10 +1106,195 @@ class SectionManager { 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 + * DOM Renderer - Handles DOM interactions with Enhanced Event System */ class DOMRenderer { constructor(sectionManager, container) { @@ -480,11 +1302,33 @@ class DOMRenderer { this.container = container; this.editingSections = new Set(); + // 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(); } @@ -512,6 +1356,46 @@ class DOMRenderer { }); } + /** + * 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'); @@ -526,8 +1410,20 @@ class DOMRenderer { }); debug('24: All section elements added to container', 'RENDER'); + + // Enhanced DOM Event System - Setup all 6 event types with delegation this.container.addEventListener('click', this.handleSectionClick); - debug('25: Click listener attached - container content length: ' + this.container.innerHTML.length, 'RENDER'); + 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); + + debug('25: Enhanced event listeners attached - container content length: ' + this.container.innerHTML.length, 'RENDER'); } createSectionElement(section) { @@ -565,6 +1461,14 @@ class DOMRenderer { debug('CLICK: 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); if (section && section.isEditing()) { @@ -580,6 +1484,265 @@ class DOMRenderer { } } + /** + * 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) { const element = this.findSectionElement(sectionId); if (!element) return; @@ -895,7 +2058,7 @@ class DOMRenderer { handleKeydown(event) { // Enhanced keyboard shortcuts for section editing - const textarea = event.target.closest('textarea'); + const textarea = event.target.closest('textarea, input'); if (!textarea) return; // Find the section being edited @@ -906,28 +2069,58 @@ class DOMRenderer { 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) { @@ -980,6 +2173,8 @@ class DOMRenderer { 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; @@ -987,23 +2182,584 @@ class DOMRenderer { 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); } /** @@ -1328,7 +3084,411 @@ class MarkitectCleanEditor { // 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.showStatus()); + document.getElementById('show-status').addEventListener('click', () => this.showDocumentStatus()); + + // 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 = ` +
| Section | +Type | +State | +Length | +Changes | +
|---|---|---|---|---|
| + + ${section.type || 'paragraph'} + + | ++ + ${section.state || 'original'} + + | +${section.currentMarkdown.length} chars | ++ ${hasChanges ? '●' : '○'} + | +