Fixed image section editing button functionality: 1. **getCurrentEditingSectionId fix**: Updated to recognize both text editor containers (.ui-edit-editor-container) and image editor containers (.ui-edit-image-editor-container) 2. **Accept button fix**: - Properly handles alt text updates with immediate DOM reflection - Calls acceptChanges() and hideEditor() directly instead of generic handler - Ensures updateSectionContent() is called for immediate visual feedback 3. **Cancel button fix**: - Directly calls cancelChanges() and hideEditor() for proper flow - Removes dependency on generic handler that couldn't identify image containers 4. **Reset button fix**: - Calls resetSection() and refreshes image editor with reset content - Provides immediate visual feedback by reopening editor with original content Added comprehensive test suite with 7 tests covering all button interactions. All image section editing buttons now work correctly. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
4307 lines
150 KiB
JavaScript
4307 lines
150 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.hideEditor(data.sectionId);
|
||
this.updateSectionContent(data.sectionId, data.content);
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Track and log DOM events for analytics and debugging
|
||
* @param {string} eventType - Type of event
|
||
* @param {Object} eventData - Event data
|
||
*/
|
||
trackEvent(eventType, eventData) {
|
||
const timestamp = new Date().toISOString();
|
||
const eventRecord = {
|
||
type: eventType,
|
||
timestamp,
|
||
data: eventData
|
||
};
|
||
|
||
// Add to history (keep last 1000 events)
|
||
this.eventHistory.push(eventRecord);
|
||
if (this.eventHistory.length > 1000) {
|
||
this.eventHistory.shift();
|
||
}
|
||
|
||
// Update stats
|
||
if (this.eventStats.hasOwnProperty(eventType)) {
|
||
this.eventStats[eventType]++;
|
||
}
|
||
|
||
// Emit to section manager for broader handling
|
||
this.sectionManager.emit(eventType, eventData);
|
||
}
|
||
|
||
/**
|
||
* Get event statistics for debugging
|
||
* @returns {Object} Event statistics
|
||
*/
|
||
getEventStats() {
|
||
return {
|
||
stats: { ...this.eventStats },
|
||
recentEvents: this.eventHistory.slice(-10),
|
||
totalEvents: this.eventHistory.length
|
||
};
|
||
}
|
||
|
||
renderAllSections(sections) {
|
||
debug('21: renderAllSections called with ' + sections.length + ' sections', 'RENDER');
|
||
|
||
this.container.innerHTML = '';
|
||
debug('22: Container cleared', 'RENDER');
|
||
|
||
sections.forEach((section, index) => {
|
||
debug('23: Creating element for section ' + (index + 1) + ': ' + section.id, 'RENDER');
|
||
const element = this.createSectionElement(section);
|
||
section.domElement = element;
|
||
this.container.appendChild(element);
|
||
});
|
||
|
||
debug('24: All section elements added to container', 'RENDER');
|
||
|
||
// Enhanced DOM Event System - Setup all 6 event types with delegation
|
||
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.updateSectionContent(sectionId, newMarkdown);
|
||
}
|
||
|
||
// Accept changes and hide editor
|
||
this.sectionManager.acceptChanges(sectionId);
|
||
this.hideEditor(sectionId);
|
||
});
|
||
const cancelBtn = this.createButton('✗ Cancel', 'ui-edit-cancel', (e) => {
|
||
// Cancel changes and hide editor
|
||
this.sectionManager.cancelChanges(sectionId);
|
||
this.hideEditor(sectionId);
|
||
});
|
||
const resetBtn = this.createButton('↺ Reset', 'ui-edit-reset', (e) => {
|
||
// Reset section to original content and refresh image editor
|
||
this.sectionManager.resetSection(sectionId);
|
||
this.hideEditor(sectionId);
|
||
|
||
// Refresh the image editor with reset content
|
||
setTimeout(() => {
|
||
const resetSection = this.sectionManager.sections.get(sectionId);
|
||
if (resetSection) {
|
||
this.showImageEditor(sectionId, resetSection);
|
||
}
|
||
}, 100);
|
||
});
|
||
|
||
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);
|
||
this.updateSectionContent(sectionId, newMarkdown);
|
||
// Wait for DOM update before showing image editor
|
||
setTimeout(() => {
|
||
const updatedSection = this.sectionManager.sections.get(sectionId);
|
||
if (updatedSection) {
|
||
this.showImageEditor(sectionId, updatedSection);
|
||
}
|
||
}, 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, .ui-edit-image-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;
|
||
} |