Files
markitect-main/markitect/static/editor.js
tegwick 4f41b22335 fix: reset button now resets to original content like reset all function
Fixed reset button behavior to match reset all functionality:

## 🔄 Reset Button Enhancement
- **Before**: Only cleared staged changes, kept current modified content
- **After**: Resets section to original content like "Reset All" function does

## 🎯 Consistent Behavior
- **Reset Button**: Now calls `sectionManager.resetSection()` for complete reset
- **Reset All**: Already used `resetSection()` for each section
- **Result**: Both reset functions now have identical behavior

## 🚀 Implementation Details
- **Section Reset**: Calls `resetSection()` to restore original markdown content
- **DOM Update**: Immediately updates display with `updateSectionContent()`
- **Staging State**: Updates staging state to reflect original content values
- **Preview Update**: Resets image preview and alt text input to original values
- **Change Indicator**: Clears "unsaved changes" warning

## 📝 Reset Button Workflow (New)
1. **Reset Section**: Restore section to original content and state
2. **Update Display**: Show original content immediately in document
3. **Parse Original**: Extract original image source and alt text
4. **Update Staging**: Set staging state to reflect original values
5. **Clear Changes**: Remove any staged modifications
6. **Update UI**: Reset preview and form inputs to original values

##  User Experience
- **Consistent**: Reset button behavior now matches user expectations
- **Complete**: Resets everything back to original (not just current changes)
- **Immediate**: Users see original content restored right away
- **Reliable**: Works the same way as "Reset All" function

Added comprehensive test suite with 4 tests covering complete reset functionality.
Reset button now provides true "revert to original" behavior.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-02 17:07:42 +01:00

4461 lines
156 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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();
// Track staging state for this editor
const stagingState = {
originalMarkdown: section.currentMarkdown,
currentAltText: '',
currentImageSrc: '',
stagedImageSrc: null,
stagedAltText: null,
hasChanges: false
};
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;
`;
// Parse markdown to extract image info
const imageMatch = section.currentMarkdown.match(/!\[(.*?)\]\((.*?)\)/);
if (imageMatch) {
const [, altText, imageSrc] = imageMatch;
stagingState.currentAltText = altText;
stagingState.currentImageSrc = imageSrc;
}
// Image preview with drop zone
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: 2px dashed #007bff;
transition: all 0.3s ease;
cursor: pointer;
min-height: 200px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
position: relative;
`;
// Function to update image preview
const updateImagePreview = (imageSrc, altText) => {
imagePreview.innerHTML = '';
if (imageSrc) {
const img = document.createElement('img');
img.src = imageSrc;
img.alt = altText || '';
img.style.cssText = `
max-width: 100%;
max-height: 250px;
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
`;
imagePreview.appendChild(img);
// Add overlay for drop zone
const overlay = document.createElement('div');
overlay.className = 'drop-overlay';
overlay.style.cssText = `
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 123, 255, 0.1);
border-radius: 6px;
display: none;
align-items: center;
justify-content: center;
color: #007bff;
font-weight: bold;
font-size: 18px;
`;
overlay.textContent = '📁 Drop new image here';
imagePreview.appendChild(overlay);
} else {
// Show drop zone placeholder
const placeholder = document.createElement('div');
placeholder.style.cssText = `
text-align: center;
color: #6c757d;
font-size: 16px;
`;
placeholder.innerHTML = `
<div style="font-size: 48px; margin-bottom: 12px;">📁</div>
<div style="margin-bottom: 8px;"><strong>Drop image here or click to select</strong></div>
<div style="font-size: 14px;">Supports JPG, PNG, GIF, WebP</div>
`;
imagePreview.appendChild(placeholder);
}
};
// Initialize preview
updateImagePreview(stagingState.currentImageSrc, stagingState.currentAltText);
// File input for image selection
const fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.accept = 'image/*';
fileInput.style.display = 'none';
// Function to handle image file selection
const handleImageFile = (file) => {
if (file && file.type.startsWith('image/')) {
const reader = new FileReader();
reader.onload = (event) => {
stagingState.stagedImageSrc = event.target.result;
stagingState.hasChanges = true;
updateImagePreview(stagingState.stagedImageSrc, altTextInput.value);
updateChangeIndicator();
};
reader.readAsDataURL(file);
}
};
// Drag and drop functionality
imagePreview.addEventListener('dragover', (e) => {
e.preventDefault();
imagePreview.style.borderColor = '#28a745';
imagePreview.style.backgroundColor = '#f8fff8';
const overlay = imagePreview.querySelector('.drop-overlay');
if (overlay) overlay.style.display = 'flex';
});
imagePreview.addEventListener('dragleave', (e) => {
e.preventDefault();
imagePreview.style.borderColor = '#007bff';
imagePreview.style.backgroundColor = 'white';
const overlay = imagePreview.querySelector('.drop-overlay');
if (overlay) overlay.style.display = 'none';
});
imagePreview.addEventListener('drop', (e) => {
e.preventDefault();
imagePreview.style.borderColor = '#007bff';
imagePreview.style.backgroundColor = 'white';
const overlay = imagePreview.querySelector('.drop-overlay');
if (overlay) overlay.style.display = 'none';
const files = e.dataTransfer.files;
if (files.length > 0) {
handleImageFile(files[0]);
}
});
// Click to select file
imagePreview.addEventListener('click', () => {
fileInput.click();
});
fileInput.addEventListener('change', (e) => {
if (e.target.files.length > 0) {
handleImageFile(e.target.files[0]);
}
});
// Alt text editor
const altTextContainer = document.createElement('div');
altTextContainer.style.cssText = `margin-bottom: 16px;`;
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 = stagingState.currentAltText;
altTextInput.style.cssText = `
width: 100%;
padding: 8px;
border: 1px solid #ccc;
border-radius: 4px;
font-size: 14px;
`;
// Track alt text changes
altTextInput.addEventListener('input', () => {
stagingState.stagedAltText = altTextInput.value;
stagingState.hasChanges = altTextInput.value !== stagingState.currentAltText || stagingState.stagedImageSrc !== null;
updateChangeIndicator();
});
// Add keyboard shortcuts to alt text input
altTextInput.addEventListener('keydown', this.handleKeydown);
altTextContainer.appendChild(altTextLabel);
altTextContainer.appendChild(altTextInput);
// Change indicator
const changeIndicator = document.createElement('div');
changeIndicator.className = 'change-indicator';
changeIndicator.style.cssText = `
padding: 8px 12px;
background: #fff3cd;
border: 1px solid #ffeaa7;
border-radius: 4px;
color: #856404;
font-size: 14px;
text-align: center;
display: none;
`;
changeIndicator.textContent = '⚠️ You have unsaved changes';
const updateChangeIndicator = () => {
if (stagingState.hasChanges) {
changeIndicator.style.display = 'block';
} else {
changeIndicator.style.display = 'none';
}
};
// 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) => {
// Apply staged changes only when accept is clicked
if (stagingState.hasChanges) {
let newMarkdown = stagingState.originalMarkdown;
// Apply image source change if staged
if (stagingState.stagedImageSrc !== null) {
const currentImageMatch = newMarkdown.match(/!\[(.*?)\]\((.*?)\)/);
if (currentImageMatch) {
newMarkdown = newMarkdown.replace(
/!\[(.*?)\]\((.*?)\)/,
`![${currentImageMatch[1]}](${stagingState.stagedImageSrc})`
);
}
}
// Apply alt text change if staged
if (stagingState.stagedAltText !== null) {
newMarkdown = newMarkdown.replace(
/!\[(.*?)\]/,
`![${stagingState.stagedAltText}]`
);
}
// Update section with final changes
this.sectionManager.updateContent(sectionId, newMarkdown);
this.updateSectionContent(sectionId, newMarkdown);
}
// Accept changes and hide editor
this.sectionManager.acceptChanges(sectionId);
this.hideEditor(sectionId);
});
const cancelBtn = this.createButton('✗ Cancel', 'ui-edit-cancel', (e) => {
// Discard all staged changes and hide editor
this.sectionManager.cancelChanges(sectionId);
this.hideEditor(sectionId);
});
const resetBtn = this.createButton('↺ Reset', 'ui-edit-reset', (e) => {
// Reset section to original content (like reset all does)
this.sectionManager.resetSection(sectionId);
// Get the reset section to update staging state with original content
const resetSection = this.sectionManager.sections.get(sectionId);
if (resetSection) {
// Update DOM immediately to show reset content
this.updateSectionContent(sectionId, resetSection.currentMarkdown);
// Parse original image info from reset content
const originalImageMatch = resetSection.currentMarkdown.match(/!\[(.*?)\]\((.*?)\)/);
if (originalImageMatch) {
const [, originalAltText, originalImageSrc] = originalImageMatch;
// Update staging state to reflect original content
stagingState.originalMarkdown = resetSection.currentMarkdown;
stagingState.currentAltText = originalAltText;
stagingState.currentImageSrc = originalImageSrc;
}
}
// Clear any staged changes
stagingState.stagedImageSrc = null;
stagingState.stagedAltText = null;
stagingState.hasChanges = false;
// Reset alt text input to original
altTextInput.value = stagingState.currentAltText;
// Reset preview to original
updateImagePreview(stagingState.currentImageSrc, stagingState.currentAltText);
updateChangeIndicator();
});
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(changeIndicator);
editorContainer.appendChild(editorControls);
editorContainer.appendChild(fileInput);
element.appendChild(editorContainer);
altTextInput.focus();
this.editingSections.add(sectionId);
}
/**
* Image manipulation methods
* Note: Image replacement is now integrated into the main image editor with drag & drop
*/
resizeImage(sectionId) {
const section = this.sectionManager.sections.get(sectionId);
const size = prompt('Enter image width (e.g., 300px, 50%, or leave empty for auto):', '');
if (size !== null) {
const imageMatch = section.currentMarkdown.match(/!\[(.*?)\]\((.*?)\)/);
if (imageMatch) {
const style = size ? ` style="width: ${size};"` : '';
const newMarkdown = section.currentMarkdown.replace(
/!\[(.*?)\]\((.*?)\)/,
`<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;
// Remove any editor UI containers from the DOM
const textEditorContainer = element.querySelector('.ui-edit-editor-container');
if (textEditorContainer) {
textEditorContainer.remove();
}
const imageEditorContainer = element.querySelector('.ui-edit-image-editor-container');
if (imageEditorContainer) {
imageEditorContainer.remove();
}
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 (&lt;100 chars):</strong> ${sectionSizes.small}</div>
<div><strong>Medium (100-500 chars):</strong> ${sectionSizes.medium}</div>
<div><strong>Large (&gt;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;
}