markitect-main/markitect/static/editor.js

4385 lines
153 KiB
JavaScript
Raw Normal View History

// 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();
feat: implement advanced image editing with drop zone and staging workflow Completely redesigned image editing experience with professional workflow: ## 🎨 New Drop Zone Interface - **Drag & Drop Support**: Users can drag image files directly onto preview area - **Visual Feedback**: Border changes to green on dragover, overlay shows drop instruction - **Click to Select**: Alternative file selection by clicking the preview area - **File Type Validation**: Supports JPG, PNG, GIF, WebP with proper validation ## 📝 Staging System (Non-Destructive Editing) - **No Immediate Changes**: Image replacement and alt text edits are staged, not applied immediately - **Change Tracking**: Visual indicator shows when user has unsaved changes - **Preview Updates**: Users see staged changes in real-time preview without affecting document - **Staging State**: Maintains separate staged vs. current state for both image source and alt text ## 🎯 Enhanced Button Workflow - **Accept**: Applies all staged changes (image + alt text) to document content - **Cancel**: Discards all staged changes and closes editor - **Reset**: Clears staged changes and returns preview to original state (keeps editor open) ## 🚀 User Experience Improvements - **Professional Interface**: Clean, modern design with clear visual hierarchy - **Immediate Feedback**: Real-time preview of changes without document modification - **Non-Destructive**: No accidental overwrites - changes must be explicitly accepted - **Intuitive Controls**: Standard edit/cancel/reset pattern familiar to users ## 🔧 Technical Enhancements - **Memory Efficient**: Removed redundant replaceImage method, integrated into main editor - **Event-Driven**: Proper drag/drop event handling with prevent default - **State Management**: Comprehensive staging state tracking with change detection - **Error Prevention**: File type validation and graceful error handling Added comprehensive test suite with 7 tests covering all new functionality. All image editing workflows now provide professional, non-destructive editing experience. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-02 16:57:30 +01:00
// 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;
`;
feat: implement advanced image editing with drop zone and staging workflow Completely redesigned image editing experience with professional workflow: ## 🎨 New Drop Zone Interface - **Drag & Drop Support**: Users can drag image files directly onto preview area - **Visual Feedback**: Border changes to green on dragover, overlay shows drop instruction - **Click to Select**: Alternative file selection by clicking the preview area - **File Type Validation**: Supports JPG, PNG, GIF, WebP with proper validation ## 📝 Staging System (Non-Destructive Editing) - **No Immediate Changes**: Image replacement and alt text edits are staged, not applied immediately - **Change Tracking**: Visual indicator shows when user has unsaved changes - **Preview Updates**: Users see staged changes in real-time preview without affecting document - **Staging State**: Maintains separate staged vs. current state for both image source and alt text ## 🎯 Enhanced Button Workflow - **Accept**: Applies all staged changes (image + alt text) to document content - **Cancel**: Discards all staged changes and closes editor - **Reset**: Clears staged changes and returns preview to original state (keeps editor open) ## 🚀 User Experience Improvements - **Professional Interface**: Clean, modern design with clear visual hierarchy - **Immediate Feedback**: Real-time preview of changes without document modification - **Non-Destructive**: No accidental overwrites - changes must be explicitly accepted - **Intuitive Controls**: Standard edit/cancel/reset pattern familiar to users ## 🔧 Technical Enhancements - **Memory Efficient**: Removed redundant replaceImage method, integrated into main editor - **Event-Driven**: Proper drag/drop event handling with prevent default - **State Management**: Comprehensive staging state tracking with change detection - **Error Prevention**: File type validation and graceful error handling Added comprehensive test suite with 7 tests covering all new functionality. All image editing workflows now provide professional, non-destructive editing experience. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-02 16:57:30 +01:00
// 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;
feat: implement advanced image editing with drop zone and staging workflow Completely redesigned image editing experience with professional workflow: ## 🎨 New Drop Zone Interface - **Drag & Drop Support**: Users can drag image files directly onto preview area - **Visual Feedback**: Border changes to green on dragover, overlay shows drop instruction - **Click to Select**: Alternative file selection by clicking the preview area - **File Type Validation**: Supports JPG, PNG, GIF, WebP with proper validation ## 📝 Staging System (Non-Destructive Editing) - **No Immediate Changes**: Image replacement and alt text edits are staged, not applied immediately - **Change Tracking**: Visual indicator shows when user has unsaved changes - **Preview Updates**: Users see staged changes in real-time preview without affecting document - **Staging State**: Maintains separate staged vs. current state for both image source and alt text ## 🎯 Enhanced Button Workflow - **Accept**: Applies all staged changes (image + alt text) to document content - **Cancel**: Discards all staged changes and closes editor - **Reset**: Clears staged changes and returns preview to original state (keeps editor open) ## 🚀 User Experience Improvements - **Professional Interface**: Clean, modern design with clear visual hierarchy - **Immediate Feedback**: Real-time preview of changes without document modification - **Non-Destructive**: No accidental overwrites - changes must be explicitly accepted - **Intuitive Controls**: Standard edit/cancel/reset pattern familiar to users ## 🔧 Technical Enhancements - **Memory Efficient**: Removed redundant replaceImage method, integrated into main editor - **Event-Driven**: Proper drag/drop event handling with prevent default - **State Management**: Comprehensive staging state tracking with change detection - **Error Prevention**: File type validation and graceful error handling Added comprehensive test suite with 7 tests covering all new functionality. All image editing workflows now provide professional, non-destructive editing experience. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-02 16:57:30 +01:00
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;
`;
feat: implement advanced image editing with drop zone and staging workflow Completely redesigned image editing experience with professional workflow: ## 🎨 New Drop Zone Interface - **Drag & Drop Support**: Users can drag image files directly onto preview area - **Visual Feedback**: Border changes to green on dragover, overlay shows drop instruction - **Click to Select**: Alternative file selection by clicking the preview area - **File Type Validation**: Supports JPG, PNG, GIF, WebP with proper validation ## 📝 Staging System (Non-Destructive Editing) - **No Immediate Changes**: Image replacement and alt text edits are staged, not applied immediately - **Change Tracking**: Visual indicator shows when user has unsaved changes - **Preview Updates**: Users see staged changes in real-time preview without affecting document - **Staging State**: Maintains separate staged vs. current state for both image source and alt text ## 🎯 Enhanced Button Workflow - **Accept**: Applies all staged changes (image + alt text) to document content - **Cancel**: Discards all staged changes and closes editor - **Reset**: Clears staged changes and returns preview to original state (keeps editor open) ## 🚀 User Experience Improvements - **Professional Interface**: Clean, modern design with clear visual hierarchy - **Immediate Feedback**: Real-time preview of changes without document modification - **Non-Destructive**: No accidental overwrites - changes must be explicitly accepted - **Intuitive Controls**: Standard edit/cancel/reset pattern familiar to users ## 🔧 Technical Enhancements - **Memory Efficient**: Removed redundant replaceImage method, integrated into main editor - **Event-Driven**: Proper drag/drop event handling with prevent default - **State Management**: Comprehensive staging state tracking with change detection - **Error Prevention**: File type validation and graceful error handling Added comprehensive test suite with 7 tests covering all new functionality. All image editing workflows now provide professional, non-destructive editing experience. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-02 16:57:30 +01:00
// 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');
feat: implement advanced image editing with drop zone and staging workflow Completely redesigned image editing experience with professional workflow: ## 🎨 New Drop Zone Interface - **Drag & Drop Support**: Users can drag image files directly onto preview area - **Visual Feedback**: Border changes to green on dragover, overlay shows drop instruction - **Click to Select**: Alternative file selection by clicking the preview area - **File Type Validation**: Supports JPG, PNG, GIF, WebP with proper validation ## 📝 Staging System (Non-Destructive Editing) - **No Immediate Changes**: Image replacement and alt text edits are staged, not applied immediately - **Change Tracking**: Visual indicator shows when user has unsaved changes - **Preview Updates**: Users see staged changes in real-time preview without affecting document - **Staging State**: Maintains separate staged vs. current state for both image source and alt text ## 🎯 Enhanced Button Workflow - **Accept**: Applies all staged changes (image + alt text) to document content - **Cancel**: Discards all staged changes and closes editor - **Reset**: Clears staged changes and returns preview to original state (keeps editor open) ## 🚀 User Experience Improvements - **Professional Interface**: Clean, modern design with clear visual hierarchy - **Immediate Feedback**: Real-time preview of changes without document modification - **Non-Destructive**: No accidental overwrites - changes must be explicitly accepted - **Intuitive Controls**: Standard edit/cancel/reset pattern familiar to users ## 🔧 Technical Enhancements - **Memory Efficient**: Removed redundant replaceImage method, integrated into main editor - **Event-Driven**: Proper drag/drop event handling with prevent default - **State Management**: Comprehensive staging state tracking with change detection - **Error Prevention**: File type validation and graceful error handling Added comprehensive test suite with 7 tests covering all new functionality. All image editing workflows now provide professional, non-destructive editing experience. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-02 16:57:30 +01:00
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';
feat: implement advanced image editing with drop zone and staging workflow Completely redesigned image editing experience with professional workflow: ## 🎨 New Drop Zone Interface - **Drag & Drop Support**: Users can drag image files directly onto preview area - **Visual Feedback**: Border changes to green on dragover, overlay shows drop instruction - **Click to Select**: Alternative file selection by clicking the preview area - **File Type Validation**: Supports JPG, PNG, GIF, WebP with proper validation ## 📝 Staging System (Non-Destructive Editing) - **No Immediate Changes**: Image replacement and alt text edits are staged, not applied immediately - **Change Tracking**: Visual indicator shows when user has unsaved changes - **Preview Updates**: Users see staged changes in real-time preview without affecting document - **Staging State**: Maintains separate staged vs. current state for both image source and alt text ## 🎯 Enhanced Button Workflow - **Accept**: Applies all staged changes (image + alt text) to document content - **Cancel**: Discards all staged changes and closes editor - **Reset**: Clears staged changes and returns preview to original state (keeps editor open) ## 🚀 User Experience Improvements - **Professional Interface**: Clean, modern design with clear visual hierarchy - **Immediate Feedback**: Real-time preview of changes without document modification - **Non-Destructive**: No accidental overwrites - changes must be explicitly accepted - **Intuitive Controls**: Standard edit/cancel/reset pattern familiar to users ## 🔧 Technical Enhancements - **Memory Efficient**: Removed redundant replaceImage method, integrated into main editor - **Event-Driven**: Proper drag/drop event handling with prevent default - **State Management**: Comprehensive staging state tracking with change detection - **Error Prevention**: File type validation and graceful error handling Added comprehensive test suite with 7 tests covering all new functionality. All image editing workflows now provide professional, non-destructive editing experience. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-02 16:57:30 +01:00
altTextInput.value = stagingState.currentAltText;
altTextInput.style.cssText = `
width: 100%;
padding: 8px;
border: 1px solid #ccc;
border-radius: 4px;
font-size: 14px;
`;
feat: implement advanced image editing with drop zone and staging workflow Completely redesigned image editing experience with professional workflow: ## 🎨 New Drop Zone Interface - **Drag & Drop Support**: Users can drag image files directly onto preview area - **Visual Feedback**: Border changes to green on dragover, overlay shows drop instruction - **Click to Select**: Alternative file selection by clicking the preview area - **File Type Validation**: Supports JPG, PNG, GIF, WebP with proper validation ## 📝 Staging System (Non-Destructive Editing) - **No Immediate Changes**: Image replacement and alt text edits are staged, not applied immediately - **Change Tracking**: Visual indicator shows when user has unsaved changes - **Preview Updates**: Users see staged changes in real-time preview without affecting document - **Staging State**: Maintains separate staged vs. current state for both image source and alt text ## 🎯 Enhanced Button Workflow - **Accept**: Applies all staged changes (image + alt text) to document content - **Cancel**: Discards all staged changes and closes editor - **Reset**: Clears staged changes and returns preview to original state (keeps editor open) ## 🚀 User Experience Improvements - **Professional Interface**: Clean, modern design with clear visual hierarchy - **Immediate Feedback**: Real-time preview of changes without document modification - **Non-Destructive**: No accidental overwrites - changes must be explicitly accepted - **Intuitive Controls**: Standard edit/cancel/reset pattern familiar to users ## 🔧 Technical Enhancements - **Memory Efficient**: Removed redundant replaceImage method, integrated into main editor - **Event-Driven**: Proper drag/drop event handling with prevent default - **State Management**: Comprehensive staging state tracking with change detection - **Error Prevention**: File type validation and graceful error handling Added comprehensive test suite with 7 tests covering all new functionality. All image editing workflows now provide professional, non-destructive editing experience. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-02 16:57:30 +01:00
// 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);
feat: implement advanced image editing with drop zone and staging workflow Completely redesigned image editing experience with professional workflow: ## 🎨 New Drop Zone Interface - **Drag & Drop Support**: Users can drag image files directly onto preview area - **Visual Feedback**: Border changes to green on dragover, overlay shows drop instruction - **Click to Select**: Alternative file selection by clicking the preview area - **File Type Validation**: Supports JPG, PNG, GIF, WebP with proper validation ## 📝 Staging System (Non-Destructive Editing) - **No Immediate Changes**: Image replacement and alt text edits are staged, not applied immediately - **Change Tracking**: Visual indicator shows when user has unsaved changes - **Preview Updates**: Users see staged changes in real-time preview without affecting document - **Staging State**: Maintains separate staged vs. current state for both image source and alt text ## 🎯 Enhanced Button Workflow - **Accept**: Applies all staged changes (image + alt text) to document content - **Cancel**: Discards all staged changes and closes editor - **Reset**: Clears staged changes and returns preview to original state (keeps editor open) ## 🚀 User Experience Improvements - **Professional Interface**: Clean, modern design with clear visual hierarchy - **Immediate Feedback**: Real-time preview of changes without document modification - **Non-Destructive**: No accidental overwrites - changes must be explicitly accepted - **Intuitive Controls**: Standard edit/cancel/reset pattern familiar to users ## 🔧 Technical Enhancements - **Memory Efficient**: Removed redundant replaceImage method, integrated into main editor - **Event-Driven**: Proper drag/drop event handling with prevent default - **State Management**: Comprehensive staging state tracking with change detection - **Error Prevention**: File type validation and graceful error handling Added comprehensive test suite with 7 tests covering all new functionality. All image editing workflows now provide professional, non-destructive editing experience. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-02 16:57:30 +01:00
// 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';
feat: implement advanced image editing with drop zone and staging workflow Completely redesigned image editing experience with professional workflow: ## 🎨 New Drop Zone Interface - **Drag & Drop Support**: Users can drag image files directly onto preview area - **Visual Feedback**: Border changes to green on dragover, overlay shows drop instruction - **Click to Select**: Alternative file selection by clicking the preview area - **File Type Validation**: Supports JPG, PNG, GIF, WebP with proper validation ## 📝 Staging System (Non-Destructive Editing) - **No Immediate Changes**: Image replacement and alt text edits are staged, not applied immediately - **Change Tracking**: Visual indicator shows when user has unsaved changes - **Preview Updates**: Users see staged changes in real-time preview without affecting document - **Staging State**: Maintains separate staged vs. current state for both image source and alt text ## 🎯 Enhanced Button Workflow - **Accept**: Applies all staged changes (image + alt text) to document content - **Cancel**: Discards all staged changes and closes editor - **Reset**: Clears staged changes and returns preview to original state (keeps editor open) ## 🚀 User Experience Improvements - **Professional Interface**: Clean, modern design with clear visual hierarchy - **Immediate Feedback**: Real-time preview of changes without document modification - **Non-Destructive**: No accidental overwrites - changes must be explicitly accepted - **Intuitive Controls**: Standard edit/cancel/reset pattern familiar to users ## 🔧 Technical Enhancements - **Memory Efficient**: Removed redundant replaceImage method, integrated into main editor - **Event-Driven**: Proper drag/drop event handling with prevent default - **State Management**: Comprehensive staging state tracking with change detection - **Error Prevention**: File type validation and graceful error handling Added comprehensive test suite with 7 tests covering all new functionality. All image editing workflows now provide professional, non-destructive editing experience. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-02 16:57:30 +01:00
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) => {
feat: implement advanced image editing with drop zone and staging workflow Completely redesigned image editing experience with professional workflow: ## 🎨 New Drop Zone Interface - **Drag & Drop Support**: Users can drag image files directly onto preview area - **Visual Feedback**: Border changes to green on dragover, overlay shows drop instruction - **Click to Select**: Alternative file selection by clicking the preview area - **File Type Validation**: Supports JPG, PNG, GIF, WebP with proper validation ## 📝 Staging System (Non-Destructive Editing) - **No Immediate Changes**: Image replacement and alt text edits are staged, not applied immediately - **Change Tracking**: Visual indicator shows when user has unsaved changes - **Preview Updates**: Users see staged changes in real-time preview without affecting document - **Staging State**: Maintains separate staged vs. current state for both image source and alt text ## 🎯 Enhanced Button Workflow - **Accept**: Applies all staged changes (image + alt text) to document content - **Cancel**: Discards all staged changes and closes editor - **Reset**: Clears staged changes and returns preview to original state (keeps editor open) ## 🚀 User Experience Improvements - **Professional Interface**: Clean, modern design with clear visual hierarchy - **Immediate Feedback**: Real-time preview of changes without document modification - **Non-Destructive**: No accidental overwrites - changes must be explicitly accepted - **Intuitive Controls**: Standard edit/cancel/reset pattern familiar to users ## 🔧 Technical Enhancements - **Memory Efficient**: Removed redundant replaceImage method, integrated into main editor - **Event-Driven**: Proper drag/drop event handling with prevent default - **State Management**: Comprehensive staging state tracking with change detection - **Error Prevention**: File type validation and graceful error handling Added comprehensive test suite with 7 tests covering all new functionality. All image editing workflows now provide professional, non-destructive editing experience. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-02 16:57:30 +01:00
// 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);
});
feat: implement advanced image editing with drop zone and staging workflow Completely redesigned image editing experience with professional workflow: ## 🎨 New Drop Zone Interface - **Drag & Drop Support**: Users can drag image files directly onto preview area - **Visual Feedback**: Border changes to green on dragover, overlay shows drop instruction - **Click to Select**: Alternative file selection by clicking the preview area - **File Type Validation**: Supports JPG, PNG, GIF, WebP with proper validation ## 📝 Staging System (Non-Destructive Editing) - **No Immediate Changes**: Image replacement and alt text edits are staged, not applied immediately - **Change Tracking**: Visual indicator shows when user has unsaved changes - **Preview Updates**: Users see staged changes in real-time preview without affecting document - **Staging State**: Maintains separate staged vs. current state for both image source and alt text ## 🎯 Enhanced Button Workflow - **Accept**: Applies all staged changes (image + alt text) to document content - **Cancel**: Discards all staged changes and closes editor - **Reset**: Clears staged changes and returns preview to original state (keeps editor open) ## 🚀 User Experience Improvements - **Professional Interface**: Clean, modern design with clear visual hierarchy - **Immediate Feedback**: Real-time preview of changes without document modification - **Non-Destructive**: No accidental overwrites - changes must be explicitly accepted - **Intuitive Controls**: Standard edit/cancel/reset pattern familiar to users ## 🔧 Technical Enhancements - **Memory Efficient**: Removed redundant replaceImage method, integrated into main editor - **Event-Driven**: Proper drag/drop event handling with prevent default - **State Management**: Comprehensive staging state tracking with change detection - **Error Prevention**: File type validation and graceful error handling Added comprehensive test suite with 7 tests covering all new functionality. All image editing workflows now provide professional, non-destructive editing experience. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-02 16:57:30 +01:00
const cancelBtn = this.createButton('✗ Cancel', 'ui-edit-cancel', (e) => {
feat: implement advanced image editing with drop zone and staging workflow Completely redesigned image editing experience with professional workflow: ## 🎨 New Drop Zone Interface - **Drag & Drop Support**: Users can drag image files directly onto preview area - **Visual Feedback**: Border changes to green on dragover, overlay shows drop instruction - **Click to Select**: Alternative file selection by clicking the preview area - **File Type Validation**: Supports JPG, PNG, GIF, WebP with proper validation ## 📝 Staging System (Non-Destructive Editing) - **No Immediate Changes**: Image replacement and alt text edits are staged, not applied immediately - **Change Tracking**: Visual indicator shows when user has unsaved changes - **Preview Updates**: Users see staged changes in real-time preview without affecting document - **Staging State**: Maintains separate staged vs. current state for both image source and alt text ## 🎯 Enhanced Button Workflow - **Accept**: Applies all staged changes (image + alt text) to document content - **Cancel**: Discards all staged changes and closes editor - **Reset**: Clears staged changes and returns preview to original state (keeps editor open) ## 🚀 User Experience Improvements - **Professional Interface**: Clean, modern design with clear visual hierarchy - **Immediate Feedback**: Real-time preview of changes without document modification - **Non-Destructive**: No accidental overwrites - changes must be explicitly accepted - **Intuitive Controls**: Standard edit/cancel/reset pattern familiar to users ## 🔧 Technical Enhancements - **Memory Efficient**: Removed redundant replaceImage method, integrated into main editor - **Event-Driven**: Proper drag/drop event handling with prevent default - **State Management**: Comprehensive staging state tracking with change detection - **Error Prevention**: File type validation and graceful error handling Added comprehensive test suite with 7 tests covering all new functionality. All image editing workflows now provide professional, non-destructive editing experience. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-02 16:57:30 +01:00
// Discard all staged changes and hide editor
this.sectionManager.cancelChanges(sectionId);
this.hideEditor(sectionId);
});
feat: implement advanced image editing with drop zone and staging workflow Completely redesigned image editing experience with professional workflow: ## 🎨 New Drop Zone Interface - **Drag & Drop Support**: Users can drag image files directly onto preview area - **Visual Feedback**: Border changes to green on dragover, overlay shows drop instruction - **Click to Select**: Alternative file selection by clicking the preview area - **File Type Validation**: Supports JPG, PNG, GIF, WebP with proper validation ## 📝 Staging System (Non-Destructive Editing) - **No Immediate Changes**: Image replacement and alt text edits are staged, not applied immediately - **Change Tracking**: Visual indicator shows when user has unsaved changes - **Preview Updates**: Users see staged changes in real-time preview without affecting document - **Staging State**: Maintains separate staged vs. current state for both image source and alt text ## 🎯 Enhanced Button Workflow - **Accept**: Applies all staged changes (image + alt text) to document content - **Cancel**: Discards all staged changes and closes editor - **Reset**: Clears staged changes and returns preview to original state (keeps editor open) ## 🚀 User Experience Improvements - **Professional Interface**: Clean, modern design with clear visual hierarchy - **Immediate Feedback**: Real-time preview of changes without document modification - **Non-Destructive**: No accidental overwrites - changes must be explicitly accepted - **Intuitive Controls**: Standard edit/cancel/reset pattern familiar to users ## 🔧 Technical Enhancements - **Memory Efficient**: Removed redundant replaceImage method, integrated into main editor - **Event-Driven**: Proper drag/drop event handling with prevent default - **State Management**: Comprehensive staging state tracking with change detection - **Error Prevention**: File type validation and graceful error handling Added comprehensive test suite with 7 tests covering all new functionality. All image editing workflows now provide professional, non-destructive editing experience. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-02 16:57:30 +01:00
const resetBtn = this.createButton('↺ Reset', 'ui-edit-reset', (e) => {
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
// 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
feat: implement advanced image editing with drop zone and staging workflow Completely redesigned image editing experience with professional workflow: ## 🎨 New Drop Zone Interface - **Drag & Drop Support**: Users can drag image files directly onto preview area - **Visual Feedback**: Border changes to green on dragover, overlay shows drop instruction - **Click to Select**: Alternative file selection by clicking the preview area - **File Type Validation**: Supports JPG, PNG, GIF, WebP with proper validation ## 📝 Staging System (Non-Destructive Editing) - **No Immediate Changes**: Image replacement and alt text edits are staged, not applied immediately - **Change Tracking**: Visual indicator shows when user has unsaved changes - **Preview Updates**: Users see staged changes in real-time preview without affecting document - **Staging State**: Maintains separate staged vs. current state for both image source and alt text ## 🎯 Enhanced Button Workflow - **Accept**: Applies all staged changes (image + alt text) to document content - **Cancel**: Discards all staged changes and closes editor - **Reset**: Clears staged changes and returns preview to original state (keeps editor open) ## 🚀 User Experience Improvements - **Professional Interface**: Clean, modern design with clear visual hierarchy - **Immediate Feedback**: Real-time preview of changes without document modification - **Non-Destructive**: No accidental overwrites - changes must be explicitly accepted - **Intuitive Controls**: Standard edit/cancel/reset pattern familiar to users ## 🔧 Technical Enhancements - **Memory Efficient**: Removed redundant replaceImage method, integrated into main editor - **Event-Driven**: Proper drag/drop event handling with prevent default - **State Management**: Comprehensive staging state tracking with change detection - **Error Prevention**: File type validation and graceful error handling Added comprehensive test suite with 7 tests covering all new functionality. All image editing workflows now provide professional, non-destructive editing experience. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-02 16:57:30 +01:00
stagingState.stagedImageSrc = null;
stagingState.stagedAltText = null;
stagingState.hasChanges = false;
feat: implement advanced image editing with drop zone and staging workflow Completely redesigned image editing experience with professional workflow: ## 🎨 New Drop Zone Interface - **Drag & Drop Support**: Users can drag image files directly onto preview area - **Visual Feedback**: Border changes to green on dragover, overlay shows drop instruction - **Click to Select**: Alternative file selection by clicking the preview area - **File Type Validation**: Supports JPG, PNG, GIF, WebP with proper validation ## 📝 Staging System (Non-Destructive Editing) - **No Immediate Changes**: Image replacement and alt text edits are staged, not applied immediately - **Change Tracking**: Visual indicator shows when user has unsaved changes - **Preview Updates**: Users see staged changes in real-time preview without affecting document - **Staging State**: Maintains separate staged vs. current state for both image source and alt text ## 🎯 Enhanced Button Workflow - **Accept**: Applies all staged changes (image + alt text) to document content - **Cancel**: Discards all staged changes and closes editor - **Reset**: Clears staged changes and returns preview to original state (keeps editor open) ## 🚀 User Experience Improvements - **Professional Interface**: Clean, modern design with clear visual hierarchy - **Immediate Feedback**: Real-time preview of changes without document modification - **Non-Destructive**: No accidental overwrites - changes must be explicitly accepted - **Intuitive Controls**: Standard edit/cancel/reset pattern familiar to users ## 🔧 Technical Enhancements - **Memory Efficient**: Removed redundant replaceImage method, integrated into main editor - **Event-Driven**: Proper drag/drop event handling with prevent default - **State Management**: Comprehensive staging state tracking with change detection - **Error Prevention**: File type validation and graceful error handling Added comprehensive test suite with 7 tests covering all new functionality. All image editing workflows now provide professional, non-destructive editing experience. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-02 16:57:30 +01:00
// 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);
feat: implement advanced image editing with drop zone and staging workflow Completely redesigned image editing experience with professional workflow: ## 🎨 New Drop Zone Interface - **Drag & Drop Support**: Users can drag image files directly onto preview area - **Visual Feedback**: Border changes to green on dragover, overlay shows drop instruction - **Click to Select**: Alternative file selection by clicking the preview area - **File Type Validation**: Supports JPG, PNG, GIF, WebP with proper validation ## 📝 Staging System (Non-Destructive Editing) - **No Immediate Changes**: Image replacement and alt text edits are staged, not applied immediately - **Change Tracking**: Visual indicator shows when user has unsaved changes - **Preview Updates**: Users see staged changes in real-time preview without affecting document - **Staging State**: Maintains separate staged vs. current state for both image source and alt text ## 🎯 Enhanced Button Workflow - **Accept**: Applies all staged changes (image + alt text) to document content - **Cancel**: Discards all staged changes and closes editor - **Reset**: Clears staged changes and returns preview to original state (keeps editor open) ## 🚀 User Experience Improvements - **Professional Interface**: Clean, modern design with clear visual hierarchy - **Immediate Feedback**: Real-time preview of changes without document modification - **Non-Destructive**: No accidental overwrites - changes must be explicitly accepted - **Intuitive Controls**: Standard edit/cancel/reset pattern familiar to users ## 🔧 Technical Enhancements - **Memory Efficient**: Removed redundant replaceImage method, integrated into main editor - **Event-Driven**: Proper drag/drop event handling with prevent default - **State Management**: Comprehensive staging state tracking with change detection - **Error Prevention**: File type validation and graceful error handling Added comprehensive test suite with 7 tests covering all new functionality. All image editing workflows now provide professional, non-destructive editing experience. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-02 16:57:30 +01:00
editorContainer.appendChild(changeIndicator);
editorContainer.appendChild(editorControls);
feat: implement advanced image editing with drop zone and staging workflow Completely redesigned image editing experience with professional workflow: ## 🎨 New Drop Zone Interface - **Drag & Drop Support**: Users can drag image files directly onto preview area - **Visual Feedback**: Border changes to green on dragover, overlay shows drop instruction - **Click to Select**: Alternative file selection by clicking the preview area - **File Type Validation**: Supports JPG, PNG, GIF, WebP with proper validation ## 📝 Staging System (Non-Destructive Editing) - **No Immediate Changes**: Image replacement and alt text edits are staged, not applied immediately - **Change Tracking**: Visual indicator shows when user has unsaved changes - **Preview Updates**: Users see staged changes in real-time preview without affecting document - **Staging State**: Maintains separate staged vs. current state for both image source and alt text ## 🎯 Enhanced Button Workflow - **Accept**: Applies all staged changes (image + alt text) to document content - **Cancel**: Discards all staged changes and closes editor - **Reset**: Clears staged changes and returns preview to original state (keeps editor open) ## 🚀 User Experience Improvements - **Professional Interface**: Clean, modern design with clear visual hierarchy - **Immediate Feedback**: Real-time preview of changes without document modification - **Non-Destructive**: No accidental overwrites - changes must be explicitly accepted - **Intuitive Controls**: Standard edit/cancel/reset pattern familiar to users ## 🔧 Technical Enhancements - **Memory Efficient**: Removed redundant replaceImage method, integrated into main editor - **Event-Driven**: Proper drag/drop event handling with prevent default - **State Management**: Comprehensive staging state tracking with change detection - **Error Prevention**: File type validation and graceful error handling Added comprehensive test suite with 7 tests covering all new functionality. All image editing workflows now provide professional, non-destructive editing experience. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-02 16:57:30 +01:00
editorContainer.appendChild(fileInput);
element.appendChild(editorContainer);
altTextInput.focus();
this.editingSections.add(sectionId);
}
/**
* Image manipulation methods
feat: implement advanced image editing with drop zone and staging workflow Completely redesigned image editing experience with professional workflow: ## 🎨 New Drop Zone Interface - **Drag & Drop Support**: Users can drag image files directly onto preview area - **Visual Feedback**: Border changes to green on dragover, overlay shows drop instruction - **Click to Select**: Alternative file selection by clicking the preview area - **File Type Validation**: Supports JPG, PNG, GIF, WebP with proper validation ## 📝 Staging System (Non-Destructive Editing) - **No Immediate Changes**: Image replacement and alt text edits are staged, not applied immediately - **Change Tracking**: Visual indicator shows when user has unsaved changes - **Preview Updates**: Users see staged changes in real-time preview without affecting document - **Staging State**: Maintains separate staged vs. current state for both image source and alt text ## 🎯 Enhanced Button Workflow - **Accept**: Applies all staged changes (image + alt text) to document content - **Cancel**: Discards all staged changes and closes editor - **Reset**: Clears staged changes and returns preview to original state (keeps editor open) ## 🚀 User Experience Improvements - **Professional Interface**: Clean, modern design with clear visual hierarchy - **Immediate Feedback**: Real-time preview of changes without document modification - **Non-Destructive**: No accidental overwrites - changes must be explicitly accepted - **Intuitive Controls**: Standard edit/cancel/reset pattern familiar to users ## 🔧 Technical Enhancements - **Memory Efficient**: Removed redundant replaceImage method, integrated into main editor - **Event-Driven**: Proper drag/drop event handling with prevent default - **State Management**: Comprehensive staging state tracking with change detection - **Error Prevention**: File type validation and graceful error handling Added comprehensive test suite with 7 tests covering all new functionality. All image editing workflows now provide professional, non-destructive editing experience. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-02 16:57:30 +01:00
* 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
*/
remove: eliminate floating status panel above editor menu Removed redundant floating status panel that appeared above the editor menu: ## 🗑️ Floating Status Panel Removal - **Issue**: Floating status panel in top-right corner duplicated information already in menu - **Solution**: Disabled `updateStatusDisplay()` method and removed `createStatusPanel()` - **Result**: Cleaner UI with status information only shown in integrated menu ## 🎯 Changes Made - **updateStatusDisplay()**: Now returns early without creating floating panel - **createStatusPanel()**: Method removed since no longer needed - **Status Integration**: Status information remains available in control panel menu - **UI Cleanup**: Eliminates visual clutter and redundant information display ## 🚀 User Experience Improvements - **Cleaner Interface**: No floating overlay competing with menu - **Single Source**: Status information consolidated in menu only - **Reduced Clutter**: Simpler, more focused editing experience - **Better Performance**: No unnecessary DOM element creation/updates ## 🔧 Technical Benefits - **Code Simplification**: Removed ~40 lines of floating panel code - **Performance**: No periodic floating panel updates (every 2 seconds) - **Memory Efficiency**: No floating DOM elements consuming resources - **Maintainability**: Single status display location to maintain ## ✅ Backward Compatibility - **Control Panel**: Status information still available in menu - **Status Tracking**: Real-time tracking continues to work - **Menu Integration**: All status features remain functional - **No Functionality Loss**: Only redundant display removed Added comprehensive test suite with 5 tests verifying floating panel removal. Interface is now cleaner with status information properly integrated into menu. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-02 21:19:16 +01:00
// createStatusPanel method removed - floating status panel no longer needed
// Status information is displayed in the control panel menu instead
/**
* Update status display with current status information
* @param {Object} status - Status object from SectionManager
*/
updateStatusDisplay(status) {
remove: eliminate floating status panel above editor menu Removed redundant floating status panel that appeared above the editor menu: ## 🗑️ Floating Status Panel Removal - **Issue**: Floating status panel in top-right corner duplicated information already in menu - **Solution**: Disabled `updateStatusDisplay()` method and removed `createStatusPanel()` - **Result**: Cleaner UI with status information only shown in integrated menu ## 🎯 Changes Made - **updateStatusDisplay()**: Now returns early without creating floating panel - **createStatusPanel()**: Method removed since no longer needed - **Status Integration**: Status information remains available in control panel menu - **UI Cleanup**: Eliminates visual clutter and redundant information display ## 🚀 User Experience Improvements - **Cleaner Interface**: No floating overlay competing with menu - **Single Source**: Status information consolidated in menu only - **Reduced Clutter**: Simpler, more focused editing experience - **Better Performance**: No unnecessary DOM element creation/updates ## 🔧 Technical Benefits - **Code Simplification**: Removed ~40 lines of floating panel code - **Performance**: No periodic floating panel updates (every 2 seconds) - **Memory Efficiency**: No floating DOM elements consuming resources - **Maintainability**: Single status display location to maintain ## ✅ Backward Compatibility - **Control Panel**: Status information still available in menu - **Status Tracking**: Real-time tracking continues to work - **Menu Integration**: All status features remain functional - **No Functionality Loss**: Only redundant display removed Added comprehensive test suite with 5 tests verifying floating panel removal. Interface is now cleaner with status information properly integrated into menu. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-02 21:19:16 +01:00
// Status information is now only displayed in the control panel menu
// No floating status panel needed since status is integrated into the menu
return;
}
}
/**
* Main Editor Integration
*/
class MarkitectCleanEditor {
constructor(markdownContent, containerElement, options = {}) {
debug('10: MarkitectCleanEditor constructor called', 'EDITOR');
this.options = {
theme: 'github',
keyboardShortcuts: true,
autosave: false,
originalFilename: null,
...options
};
debug('11: Creating SectionManager', 'EDITOR');
this.sectionManager = new SectionManager();
debug('12: Creating DOMRenderer', 'EDITOR');
this.domRenderer = new DOMRenderer(this.sectionManager, containerElement);
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;
}