Fixed JavaScript method call errors that were blocking content display: - Fix sectionManager.getSection() → sections.get() method calls - Fix section.isModified() → section.hasChanges() method calls - Add missing getDocumentStatus() method to SectionManager class Added comprehensive content rendering validation test to catch future issues. Enhanced section styling system with 17 advanced styling methods. All content now renders successfully with full JavaScript functionality. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
4279 lines
149 KiB
JavaScript
4279 lines
149 KiB
JavaScript
// 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'
|
||
});
|
||
|
||
function debug(message, category = 'INFO') {
|
||
const prefix = `DEBUG ${category}:`;
|
||
|
||
switch (DEBUG_MODE) {
|
||
case 'off':
|
||
// No debugging output
|
||
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');
|
||
}
|
||
}
|
||
|
||
// 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) {
|
||
const section = this.sections.get(sectionId);
|
||
if (!section) {
|
||
throw new Error(`Section ${sectionId} not found`);
|
||
}
|
||
|
||
if (section.isEditing()) {
|
||
console.log('Section already in editing state:', sectionId);
|
||
return section.editingMarkdown;
|
||
}
|
||
|
||
const content = section.startEdit();
|
||
this.emit('edit-started', { sectionId, content, section: section.getStatus() });
|
||
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();
|
||
|
||
// 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) => {
|
||
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.updateTextareaContent(data.content, data.sectionId);
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 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
|
||
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);
|
||
|
||
debug('25: Enhanced event listeners attached - 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(/<a href="([^"]*)"([^>]*)>/g, '<a href="$1" target="_blank"$2>');
|
||
element.innerHTML = htmlWithTargetBlank;
|
||
} else {
|
||
element.innerHTML = `<p>${section.currentMarkdown}</p>`;
|
||
}
|
||
|
||
this.setupSectionElement(element);
|
||
debug('SECTION: Section element setup complete for ' + section.id, 'SECTION');
|
||
return element;
|
||
}
|
||
|
||
handleSectionClick(event) {
|
||
debug('CLICK: 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('CLICK: Ignoring click on form element', 'CLICK');
|
||
return;
|
||
}
|
||
|
||
const sectionElement = event.target.closest('.ui-edit-section');
|
||
debug('CLICK: Found section element: ' + (sectionElement ? sectionElement.getAttribute('data-section-id') : 'null'), 'CLICK');
|
||
if (!sectionElement) return;
|
||
|
||
const sectionId = sectionElement.getAttribute('data-section-id');
|
||
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()) {
|
||
console.log('Section already being edited:', sectionId);
|
||
return;
|
||
}
|
||
|
||
try {
|
||
console.log('Starting edit for section:', sectionId);
|
||
this.sectionManager.startEditing(sectionId);
|
||
} catch (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) {
|
||
const element = this.findSectionElement(sectionId);
|
||
if (!element) return;
|
||
|
||
this.hideCurrentEditor();
|
||
|
||
const section = this.sectionManager.sections.get(sectionId);
|
||
const isImageSection = section && section.isImage();
|
||
|
||
if (isImageSection) {
|
||
this.showImageEditor(sectionId, section);
|
||
return;
|
||
}
|
||
|
||
const editorContainer = document.createElement('div');
|
||
editorContainer.className = 'ui-edit-editor-container';
|
||
|
||
const textarea = document.createElement('textarea');
|
||
textarea.className = 'ui-edit-textarea ui-edit-textarea-main';
|
||
textarea.value = content;
|
||
textarea.style.cssText = `
|
||
flex: 1;
|
||
min-height: 100px;
|
||
font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace;
|
||
font-size: 14px;
|
||
line-height: 1.5;
|
||
padding: 12px;
|
||
border: 2px solid #007bff;
|
||
border-radius: 6px;
|
||
resize: vertical;
|
||
outline: none;
|
||
background: white;
|
||
color: #333;
|
||
`;
|
||
|
||
// Setup auto-resize functionality
|
||
this.setupAutoResize(textarea);
|
||
|
||
const controls = document.createElement('div');
|
||
controls.className = 'ui-edit-controls';
|
||
controls.style.cssText = `
|
||
display: flex;
|
||
gap: 8px;
|
||
justify-content: flex-end;
|
||
margin-top: 8px;
|
||
`;
|
||
|
||
const acceptBtn = this.createButton('Accept', 'ui-edit-button-accept', this.handleAccept);
|
||
const cancelBtn = this.createButton('Cancel', 'ui-edit-button-cancel', this.handleCancel);
|
||
|
||
controls.appendChild(acceptBtn);
|
||
controls.appendChild(cancelBtn);
|
||
|
||
editorContainer.appendChild(textarea);
|
||
editorContainer.appendChild(controls);
|
||
|
||
element.innerHTML = '';
|
||
element.appendChild(editorContainer);
|
||
|
||
textarea.focus();
|
||
this.editingSections.add(sectionId);
|
||
|
||
textarea.addEventListener('input', () => {
|
||
this.sectionManager.updateContent(sectionId, textarea.value);
|
||
});
|
||
|
||
// Add keyboard shortcuts
|
||
textarea.addEventListener('keydown', this.handleKeydown);
|
||
}
|
||
|
||
/**
|
||
* 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();
|
||
|
||
const editorContainer = document.createElement('div');
|
||
editorContainer.className = 'ui-edit-image-editor-container';
|
||
editorContainer.style.cssText = `
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 12px;
|
||
margin-top: 12px;
|
||
padding: 16px;
|
||
background: #f8f9fa;
|
||
border-radius: 8px;
|
||
border: 2px solid #007bff;
|
||
`;
|
||
|
||
// Image preview
|
||
const imagePreview = document.createElement('div');
|
||
imagePreview.className = 'ui-edit-image-preview';
|
||
imagePreview.style.cssText = `
|
||
max-width: 100%;
|
||
text-align: center;
|
||
background: white;
|
||
padding: 16px;
|
||
border-radius: 6px;
|
||
border: 1px solid #dee2e6;
|
||
`;
|
||
|
||
// Parse markdown to extract image info
|
||
const imageMatch = section.currentMarkdown.match(/!\[(.*?)\]\((.*?)\)/);
|
||
if (imageMatch) {
|
||
const [, altText, imageSrc] = imageMatch;
|
||
const img = document.createElement('img');
|
||
img.src = imageSrc;
|
||
img.alt = altText;
|
||
img.style.cssText = `
|
||
max-width: 100%;
|
||
max-height: 300px;
|
||
border-radius: 4px;
|
||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||
`;
|
||
imagePreview.appendChild(img);
|
||
}
|
||
|
||
// Image controls
|
||
const controlPanel = document.createElement('div');
|
||
controlPanel.className = 'ui-edit-image-controls';
|
||
controlPanel.style.cssText = `
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
|
||
gap: 8px;
|
||
`;
|
||
|
||
// Alt text editor
|
||
const altTextContainer = document.createElement('div');
|
||
altTextContainer.style.cssText = `grid-column: 1 / -1; margin-bottom: 8px;`;
|
||
const altTextLabel = document.createElement('label');
|
||
altTextLabel.textContent = 'Alt Text:';
|
||
altTextLabel.style.cssText = `display: block; margin-bottom: 4px; font-weight: bold;`;
|
||
|
||
const altTextInput = document.createElement('input');
|
||
altTextInput.type = 'text';
|
||
altTextInput.value = imageMatch ? imageMatch[1] : '';
|
||
altTextInput.style.cssText = `
|
||
width: 100%;
|
||
padding: 8px;
|
||
border: 1px solid #ccc;
|
||
border-radius: 4px;
|
||
font-size: 14px;
|
||
`;
|
||
|
||
// Add keyboard shortcuts to alt text input
|
||
altTextInput.addEventListener('keydown', this.handleKeydown);
|
||
|
||
altTextContainer.appendChild(altTextLabel);
|
||
altTextContainer.appendChild(altTextInput);
|
||
|
||
// Image manipulation buttons
|
||
const buttons = [
|
||
{ text: 'Replace Image', action: () => this.replaceImage(sectionId) },
|
||
{ text: 'Resize', action: () => this.resizeImage(sectionId) },
|
||
{ text: 'Add Caption', action: () => this.addImageCaption(sectionId) },
|
||
{ text: 'Remove Image', action: () => this.removeImage(sectionId) }
|
||
];
|
||
|
||
buttons.forEach(({ text, action }) => {
|
||
const btn = this.createButton(text, 'ui-edit-image-btn', action);
|
||
btn.style.fontSize = '12px';
|
||
btn.style.padding = '6px 12px';
|
||
controlPanel.appendChild(btn);
|
||
});
|
||
|
||
// Standard editor controls
|
||
const editorControls = document.createElement('div');
|
||
editorControls.className = 'ui-edit-controls';
|
||
editorControls.style.cssText = `
|
||
display: flex;
|
||
gap: 8px;
|
||
justify-content: flex-end;
|
||
margin-top: 12px;
|
||
`;
|
||
|
||
const acceptBtn = this.createButton('✓ Accept', 'ui-edit-accept', (e) => {
|
||
// Update alt text if changed
|
||
if (imageMatch && altTextInput.value !== imageMatch[1]) {
|
||
const newMarkdown = section.currentMarkdown.replace(
|
||
/!\[(.*?)\]/,
|
||
`![${altTextInput.value}]`
|
||
);
|
||
this.sectionManager.updateContent(sectionId, newMarkdown);
|
||
}
|
||
this.handleAccept(e);
|
||
});
|
||
const cancelBtn = this.createButton('✗ Cancel', 'ui-edit-cancel', this.handleCancel);
|
||
const resetBtn = this.createButton('↺ Reset', 'ui-edit-reset', this.handleReset);
|
||
|
||
acceptBtn.style.background = '#28a745';
|
||
cancelBtn.style.background = '#dc3545';
|
||
resetBtn.style.background = '#fd7e14';
|
||
|
||
editorControls.appendChild(acceptBtn);
|
||
editorControls.appendChild(cancelBtn);
|
||
editorControls.appendChild(resetBtn);
|
||
|
||
// Assemble the editor
|
||
editorContainer.appendChild(imagePreview);
|
||
editorContainer.appendChild(altTextContainer);
|
||
editorContainer.appendChild(controlPanel);
|
||
editorContainer.appendChild(editorControls);
|
||
|
||
element.appendChild(editorContainer);
|
||
altTextInput.focus();
|
||
this.editingSections.add(sectionId);
|
||
}
|
||
|
||
/**
|
||
* Image manipulation methods
|
||
*/
|
||
replaceImage(sectionId) {
|
||
const input = document.createElement('input');
|
||
input.type = 'file';
|
||
input.accept = 'image/*';
|
||
input.addEventListener('change', (e) => {
|
||
const file = e.target.files[0];
|
||
if (file) {
|
||
const reader = new FileReader();
|
||
reader.onload = (event) => {
|
||
const section = this.sectionManager.sections.get(sectionId);
|
||
const imageMatch = section.currentMarkdown.match(/!\[(.*?)\]\((.*?)\)/);
|
||
if (imageMatch) {
|
||
const newMarkdown = section.currentMarkdown.replace(
|
||
/!\[(.*?)\]\((.*?)\)/,
|
||
`![${imageMatch[1]}](${event.target.result})`
|
||
);
|
||
this.sectionManager.updateContent(sectionId, newMarkdown);
|
||
this.hideEditor(sectionId);
|
||
setTimeout(() => this.showImageEditor(sectionId, this.sectionManager.sections.get(sectionId)), 100);
|
||
}
|
||
};
|
||
reader.readAsDataURL(file);
|
||
}
|
||
});
|
||
input.click();
|
||
}
|
||
|
||
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(
|
||
/!\[(.*?)\]\((.*?)\)/,
|
||
`<img src="${imageMatch[2]}" alt="${imageMatch[1]}"${style}>`
|
||
);
|
||
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) {
|
||
const sectionId = this.getCurrentEditingSectionId(event.target);
|
||
if (sectionId) {
|
||
this.sectionManager.acceptChanges(sectionId);
|
||
}
|
||
}
|
||
|
||
handleCancel(event) {
|
||
const sectionId = this.getCurrentEditingSectionId(event.target);
|
||
if (sectionId) {
|
||
this.sectionManager.cancelChanges(sectionId);
|
||
}
|
||
}
|
||
|
||
handleReset(event) {
|
||
const sectionId = this.getCurrentEditingSectionId(event.target);
|
||
if (sectionId) {
|
||
this.sectionManager.resetSection(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) {
|
||
const editorContainer = button.closest('.ui-edit-editor-container');
|
||
if (!editorContainer) return null;
|
||
|
||
const sectionElement = editorContainer.parentElement;
|
||
return sectionElement ? sectionElement.getAttribute('data-section-id') : null;
|
||
}
|
||
|
||
hideEditor(sectionId) {
|
||
const element = this.findSectionElement(sectionId);
|
||
if (!element) return;
|
||
|
||
const section = this.sectionManager.sections.get(sectionId);
|
||
if (section) {
|
||
this.updateSectionContent(sectionId, section.currentMarkdown);
|
||
}
|
||
|
||
this.editingSections.delete(sectionId);
|
||
}
|
||
|
||
hideCurrentEditor() {
|
||
this.editingSections.forEach(sectionId => {
|
||
const element = this.findSectionElement(sectionId);
|
||
if (element && element.querySelector('.ui-edit-editor-container')) {
|
||
this.hideEditor(sectionId);
|
||
}
|
||
});
|
||
}
|
||
|
||
updateSectionContent(sectionId, content) {
|
||
const element = this.findSectionElement(sectionId);
|
||
if (!element) return;
|
||
|
||
if (typeof marked !== 'undefined') {
|
||
const html = marked.parse(content);
|
||
const htmlWithTargetBlank = html.replace(/<a href="([^"]*)"([^>]*)>/g, '<a href="$1" target="_blank"$2>');
|
||
element.innerHTML = htmlWithTargetBlank;
|
||
} else {
|
||
element.innerHTML = `<p>${content}</p>`;
|
||
}
|
||
|
||
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() {
|
||
const panel = document.createElement('div');
|
||
panel.className = 'ui-edit-status-panel';
|
||
panel.style.cssText = `
|
||
position: fixed;
|
||
top: 20px;
|
||
right: 20px;
|
||
background: white;
|
||
border: 1px solid #ddd;
|
||
border-radius: 8px;
|
||
padding: 12px 16px;
|
||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||
font-size: 14px;
|
||
z-index: 1000;
|
||
min-width: 180px;
|
||
`;
|
||
|
||
const statusText = document.createElement('div');
|
||
statusText.className = 'ui-edit-status-text';
|
||
statusText.textContent = 'Ready';
|
||
|
||
const detailsText = document.createElement('div');
|
||
detailsText.className = 'ui-edit-status-details';
|
||
detailsText.style.cssText = `
|
||
font-size: 12px;
|
||
color: #666;
|
||
margin-top: 4px;
|
||
`;
|
||
|
||
panel.appendChild(statusText);
|
||
panel.appendChild(detailsText);
|
||
|
||
return panel;
|
||
}
|
||
|
||
/**
|
||
* Update status display with current status information
|
||
* @param {Object} status - Status object from SectionManager
|
||
*/
|
||
updateStatusDisplay(status) {
|
||
let statusPanel = document.querySelector('.ui-edit-status-panel');
|
||
|
||
if (!statusPanel) {
|
||
statusPanel = this.createStatusPanel();
|
||
document.body.appendChild(statusPanel);
|
||
}
|
||
|
||
const statusText = statusPanel.querySelector('.ui-edit-status-text');
|
||
const detailsText = statusPanel.querySelector('.ui-edit-status-details');
|
||
|
||
// Update status text and color based on state
|
||
switch (status.state) {
|
||
case 'ready':
|
||
statusText.textContent = '✓ Ready';
|
||
statusText.style.color = '#28a745';
|
||
break;
|
||
case 'editing':
|
||
statusText.textContent = '✏️ Editing';
|
||
statusText.style.color = '#007bff';
|
||
break;
|
||
case 'modified':
|
||
statusText.textContent = '⚠️ Modified';
|
||
statusText.style.color = '#ffc107';
|
||
break;
|
||
default:
|
||
statusText.textContent = 'Unknown';
|
||
statusText.style.color = '#6c757d';
|
||
}
|
||
|
||
// Update details
|
||
const details = [];
|
||
details.push(`${status.totalSections} sections`);
|
||
|
||
if (status.editingSections.length > 0) {
|
||
details.push(`${status.editingSections.length} editing`);
|
||
}
|
||
|
||
if (status.modifiedSections > 0) {
|
||
details.push(`${status.modifiedSections} modified`);
|
||
}
|
||
|
||
detailsText.textContent = details.join(' • ');
|
||
|
||
// Update timestamp
|
||
const now = new Date().toLocaleTimeString();
|
||
statusPanel.setAttribute('title', `Last update: ${now}`);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 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);
|
||
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;
|
||
`;
|
||
|
||
buttonContainer.appendChild(saveButton);
|
||
buttonContainer.appendChild(resetButton);
|
||
buttonContainer.appendChild(statusButton);
|
||
|
||
controlPanel.appendChild(title);
|
||
controlPanel.appendChild(buttonContainer);
|
||
|
||
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());
|
||
|
||
// 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 = `
|
||
<div class="stat-item"><strong>${status.totalSections}</strong> sections</div>
|
||
<div class="stat-item"><strong>${status.editingSections}</strong> editing</div>
|
||
<div class="stat-item"><strong>${eventStats.totalEvents}</strong> events</div>
|
||
`;
|
||
}
|
||
|
||
/**
|
||
* 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 = `
|
||
<h3>Document Status</h3>
|
||
<p><strong>Total Sections:</strong> ${totalSections}</p>
|
||
<p><strong>Modified Sections:</strong> ${editedSections}</p>
|
||
<p><strong>Currently Editing:</strong> ${currentlyEditing}</p>
|
||
<hr>
|
||
<h4>Section Details:</h4>
|
||
<ul>
|
||
${sections.map(s => `
|
||
<li>
|
||
<strong>${s.id}</strong> (${s.type})
|
||
- ${s.state}
|
||
${s.hasChanges ? ' ✏️' : ''}
|
||
${s.isEditing ? ' 🖊️' : ''}
|
||
</li>
|
||
`).join('')}
|
||
</ul>
|
||
`;
|
||
|
||
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 = `
|
||
<div style="display: flex; align-items: flex-start; gap: 12px;">
|
||
${iconHtml}
|
||
<div style="flex: 1; line-height: 1.4;">
|
||
${message}
|
||
</div>
|
||
${dismissible ? '<div style="margin-left: auto; opacity: 0.7; font-size: 16px; line-height: 1;">×</div>' : ''}
|
||
</div>
|
||
`;
|
||
|
||
// 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: '<div style="color: #28a745; font-size: 18px; font-weight: bold;">✓</div>',
|
||
error: '<div style="color: #dc3545; font-size: 18px; font-weight: bold;">✕</div>',
|
||
warning: '<div style="color: #ffc107; font-size: 18px; font-weight: bold;">⚠</div>',
|
||
info: '<div style="color: #17a2b8; font-size: 18px; font-weight: bold;">ℹ</div>',
|
||
debug: '<div style="color: #6c757d; font-size: 18px; font-weight: bold;">🐛</div>'
|
||
};
|
||
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 = `<h2>${title}</h2>${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 = `
|
||
<div style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; line-height: 1.6;">
|
||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin-bottom: 24px;">
|
||
<!-- Document Overview -->
|
||
<div style="background: #f8f9fa; padding: 16px; border-radius: 8px; border-left: 4px solid #007acc;">
|
||
<h3 style="margin: 0 0 12px 0; color: #007acc; font-size: 16px;">📄 Document Overview</h3>
|
||
<div style="display: grid; gap: 8px;">
|
||
<div><strong>Total Sections:</strong> ${status.totalSections}</div>
|
||
<div><strong>Total Characters:</strong> ${totalCharacters.toLocaleString()}</div>
|
||
<div><strong>Average Section Length:</strong> ${averageLength} chars</div>
|
||
<div><strong>Document Status:</strong>
|
||
<span style="color: ${status.hasUnsavedChanges ? '#f39c12' : '#27ae60'}; font-weight: bold;">
|
||
${status.hasUnsavedChanges ? '⚠️ Has Changes' : '✅ All Saved'}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Section States -->
|
||
<div style="background: #f8f9fa; padding: 16px; border-radius: 8px; border-left: 4px solid #27ae60;">
|
||
<h3 style="margin: 0 0 12px 0; color: #27ae60; font-size: 16px;">📝 Section States</h3>
|
||
<div style="display: grid; gap: 8px;">
|
||
<div><strong>Currently Editing:</strong> ${status.editingSections} sections</div>
|
||
<div><strong>Modified (Unsaved):</strong> ${status.modifiedSections} sections</div>
|
||
<div><strong>Saved:</strong> ${status.savedSections} sections</div>
|
||
<div><strong>Original:</strong> ${status.totalSections - status.modifiedSections - status.savedSections} sections</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin-bottom: 24px;">
|
||
<!-- Section Types -->
|
||
<div style="background: #f8f9fa; padding: 16px; border-radius: 8px; border-left: 4px solid #8e44ad;">
|
||
<h3 style="margin: 0 0 12px 0; color: #8e44ad; font-size: 16px;">🏷️ Section Types</h3>
|
||
<div style="display: grid; gap: 6px; font-size: 14px;">
|
||
${Object.entries(sectionTypes).map(([type, count]) =>
|
||
`<div><strong>${type.charAt(0).toUpperCase() + type.slice(1)}:</strong> ${count}</div>`
|
||
).join('')}
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Section Sizes -->
|
||
<div style="background: #f8f9fa; padding: 16px; border-radius: 8px; border-left: 4px solid #e67e22;">
|
||
<h3 style="margin: 0 0 12px 0; color: #e67e22; font-size: 16px;">📏 Section Sizes</h3>
|
||
<div style="display: grid; gap: 6px; font-size: 14px;">
|
||
<div><strong>Small (<100 chars):</strong> ${sectionSizes.small}</div>
|
||
<div><strong>Medium (100-500 chars):</strong> ${sectionSizes.medium}</div>
|
||
<div><strong>Large (>500 chars):</strong> ${sectionSizes.large}</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Event Statistics -->
|
||
<div style="background: #f8f9fa; padding: 16px; border-radius: 8px; border-left: 4px solid #3498db; margin-bottom: 20px;">
|
||
<h3 style="margin: 0 0 12px 0; color: #3498db; font-size: 16px;">⚡ Event Statistics</h3>
|
||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 12px; font-size: 14px;">
|
||
<div><strong>Total Events:</strong> ${eventStats.totalEvents}</div>
|
||
<div><strong>Section Clicks:</strong> ${eventStats.stats['section-click'] || 0}</div>
|
||
<div><strong>Hover Events:</strong> ${(eventStats.stats['section-hover-enter'] || 0) + (eventStats.stats['section-hover-leave'] || 0)}</div>
|
||
<div><strong>Keyboard Shortcuts:</strong> ${eventStats.stats['keyboard-shortcut'] || 0}</div>
|
||
<div><strong>Context Menus:</strong> ${eventStats.stats['section-context-menu'] || 0}</div>
|
||
<div><strong>Drag Events:</strong> ${(eventStats.stats['section-drag-start'] || 0) + (eventStats.stats['section-drag-over'] || 0)}</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Recent Activity -->
|
||
${eventStats.recentEvents.length > 0 ? `
|
||
<div style="background: #f8f9fa; padding: 16px; border-radius: 8px; border-left: 4px solid #9b59b6;">
|
||
<h3 style="margin: 0 0 12px 0; color: #9b59b6; font-size: 16px;">🕒 Recent Activity (Last 10 Events)</h3>
|
||
<div style="max-height: 200px; overflow-y: auto;">
|
||
${eventStats.recentEvents.slice(-10).reverse().map(event => `
|
||
<div style="padding: 6px; margin: 4px 0; background: white; border-radius: 4px; font-size: 13px; border-left: 3px solid #ddd;">
|
||
<strong>${event.type}</strong> - ${event.timestamp}
|
||
${event.data.sectionId ? `<br/><span style="color: #666;">Section: ${event.data.sectionId.substring(0, 12)}...</span>` : ''}
|
||
${event.data.shortcut ? `<br/><span style="color: #666;">Shortcut: ${event.data.shortcut}</span>` : ''}
|
||
</div>
|
||
`).join('')}
|
||
</div>
|
||
</div>
|
||
` : ''}
|
||
|
||
<!-- Section Details Table -->
|
||
<div style="margin-top: 20px;">
|
||
<h3 style="margin: 0 0 12px 0; color: #2c3e50; font-size: 16px;">📋 Section Details</h3>
|
||
<div style="max-height: 300px; overflow-y: auto; border: 1px solid #ddd; border-radius: 4px;">
|
||
<table style="width: 100%; border-collapse: collapse; font-size: 13px;">
|
||
<thead style="background: #34495e; color: white; position: sticky; top: 0;">
|
||
<tr>
|
||
<th style="padding: 8px; text-align: left; border-bottom: 1px solid #ddd;">Section</th>
|
||
<th style="padding: 8px; text-align: left; border-bottom: 1px solid #ddd;">Type</th>
|
||
<th style="padding: 8px; text-align: left; border-bottom: 1px solid #ddd;">State</th>
|
||
<th style="padding: 8px; text-align: left; border-bottom: 1px solid #ddd;">Length</th>
|
||
<th style="padding: 8px; text-align: left; border-bottom: 1px solid #ddd;">Changes</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
${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 `
|
||
<tr style="border-bottom: 1px solid #eee;">
|
||
<td style="padding: 8px; max-width: 200px; overflow: hidden; text-overflow: ellipsis;" title="${section.currentMarkdown.substring(0, 100)}">
|
||
${preview}
|
||
</td>
|
||
<td style="padding: 8px;">
|
||
<span style="background: #ecf0f1; padding: 2px 6px; border-radius: 3px; font-size: 11px;">
|
||
${section.type || 'paragraph'}
|
||
</span>
|
||
</td>
|
||
<td style="padding: 8px;">
|
||
<span style="color: ${stateColor}; font-weight: bold;">
|
||
${section.state || 'original'}
|
||
</span>
|
||
</td>
|
||
<td style="padding: 8px;">${section.currentMarkdown.length} chars</td>
|
||
<td style="padding: 8px; color: ${hasChanges ? '#e74c3c' : '#27ae60'};">
|
||
${hasChanges ? '●' : '○'}
|
||
</td>
|
||
</tr>
|
||
`;
|
||
}).join('')}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
this.showModal('📊 Comprehensive Document Status', statusHtml);
|
||
}
|
||
|
||
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;
|
||
} |