Files
markitect-main/relicts/AllControlsRudimentary.html

3846 lines
144 KiB
HTML
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Development Guardrails</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial, sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 2rem;
line-height: 1.6;
color: #333333;
background-color: #ffffff;
}
#markdown-content {
min-height: 200px;
}
h1, h2, h3, h4, h5, h6 {
color: #333333;
}
pre {
background-color: #f6f8fa;
color: #333333;
padding: 1rem;
border-radius: 6px;
overflow-x: auto;
border: 1px solid #d0d7de;
}
code {
background-color: #f6f8fa;
color: #333333;
padding: 0.2em 0.4em;
border-radius: 3px;
font-size: 0.9em;
}
pre code {
background: none;
padding: 0;
}
blockquote {
border-left: 4px solid #dfe2e5;
margin: 0;
padding-left: 1rem;
color: #6a737d;
}
table {
font-size: 0.85em;
border-collapse: collapse;
margin: 1rem 0;
width: 100%;
border: 1px solid #d0d7de;
}
th, td {
font-size: inherit;
border: 1px solid #d0d7de;
padding: 0.5rem;
text-align: left;
}
th {
background-color: #f6f8fa;
font-weight: 600;
}
img {
max-width: 12cm;
max-height: 20cm;
height: auto;
display: block;
margin: 1rem auto;
}</style>
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"
onload="window.markitectMarkedLoaded = true"
onerror="window.markitectMarkedError = true"></script>
</head>
<body class="markitect-edit-mode">
<div id="markdown-content"></div>
<script>
const markdownContent = "# Development Guardrails\n\n## JavaScript Code Principles\n\n### 1. No Inline JavaScript in Python\n**NEVER write JavaScript code directly from Python code**\n\n\u274c **Wrong:**\n```python\nscript = f\"\"\"\nfunction myFunction() {{\n console.log(\"Hello {name}\");\n}}\n\"\"\"\n```\n\n\u2705 **Correct:**\n```python\n# Load from external files only\ncomponents = [\n 'js/core/section-manager.js',\n 'js/components/debug-panel.js',\n 'js/components/document-controls.js'\n]\n```\n\n### 2. Why This Rule Exists\n- **Quoting Problems**: String escaping in Python corrupts JavaScript\n- **Syntax Errors**: Template literals and complex JS break when embedded\n- **Maintainability**: JS code should be in .js files for proper tooling\n- **Architecture**: Follows the established modular component system\n\n### 3. Proper Approach\n1. Create separate `.js` files in `markitect/static/js/components/`\n2. Load them via `_get_clean_editor_scripts()`\n3. Wire up components in the initialization script only\n\n## Testing and Validation\n\n### 1. Always Validate Generated HTML\n- Check that HTML files actually render content\n- Validate JavaScript syntax before deployment\n- Test both viewing and editing modes\n\n### 2. Detect JavaScript Errors Programmatically\n- Run syntax validation on generated JS\n- Check for common error patterns\n- Fail fast when JS is malformed\n\n### 3. Manual Testing Backup\n- If automated checks pass but functionality fails\n- Open generated HTML in browser\n- Check console for runtime errors\n- Report specific error messages\n\n## Architecture Principles\n\n### 1. Separation of Concerns\n- Python: File generation, template management\n- JavaScript: UI components, interaction logic\n- HTML: Structure and content only\n\n### 2. Modular Component System\n- Each UI component in separate file\n- Lazy loading where appropriate\n- Clear dependency management\n\n### 3. Error Handling\n- Graceful degradation when components fail\n- Clear error messages for debugging\n- Fallback modes when possible\n\n## Breaking These Rules\n\nIf you find yourself writing JavaScript in Python strings:\n1. **STOP** - Step back and reconsider\n2. Create a proper component file instead\n3. Use the existing component loading system\n4. Add validation to catch the issue early\n\nThese guardrails exist because we've seen the problems when they're violated.";
const markdownContentWithDogtag = "# Development Guardrails\n\n## JavaScript Code Principles\n\n### 1. No Inline JavaScript in Python\n**NEVER write JavaScript code directly from Python code**\n\n\u274c **Wrong:**\n```python\nscript = f\"\"\"\nfunction myFunction() {{\n console.log(\"Hello {name}\");\n}}\n\"\"\"\n```\n\n\u2705 **Correct:**\n```python\n# Load from external files only\ncomponents = [\n 'js/core/section-manager.js',\n 'js/components/debug-panel.js',\n 'js/components/document-controls.js'\n]\n```\n\n### 2. Why This Rule Exists\n- **Quoting Problems**: String escaping in Python corrupts JavaScript\n- **Syntax Errors**: Template literals and complex JS break when embedded\n- **Maintainability**: JS code should be in .js files for proper tooling\n- **Architecture**: Follows the established modular component system\n\n### 3. Proper Approach\n1. Create separate `.js` files in `markitect/static/js/components/`\n2. Load them via `_get_clean_editor_scripts()`\n3. Wire up components in the initialization script only\n\n## Testing and Validation\n\n### 1. Always Validate Generated HTML\n- Check that HTML files actually render content\n- Validate JavaScript syntax before deployment\n- Test both viewing and editing modes\n\n### 2. Detect JavaScript Errors Programmatically\n- Run syntax validation on generated JS\n- Check for common error patterns\n- Fail fast when JS is malformed\n\n### 3. Manual Testing Backup\n- If automated checks pass but functionality fails\n- Open generated HTML in browser\n- Check console for runtime errors\n- Report specific error messages\n\n## Architecture Principles\n\n### 1. Separation of Concerns\n- Python: File generation, template management\n- JavaScript: UI components, interaction logic\n- HTML: Structure and content only\n\n### 2. Modular Component System\n- Each UI component in separate file\n- Lazy loading where appropriate\n- Clear dependency management\n\n### 3. Error Handling\n- Graceful degradation when components fail\n- Clear error messages for debugging\n- Fallback modes when possible\n\n## Breaking These Rules\n\nIf you find yourself writing JavaScript in Python strings:\n1. **STOP** - Step back and reconsider\n2. Create a proper component file instead\n3. Use the existing component loading system\n4. Add validation to catch the issue early\n\nThese guardrails exist because we've seen the problems when they're violated.\n\n---\n*-- html from markdown by <a href=\"https://coulomb.social/open/MarkiTect\" target=\"_blank\">MarkiTect</a> on 2025-11-12 01:49:38 by <a href=\"https://coulomb.social/open/worsch\" target=\"_blank\">worsch</a>*";
const dogtagContent = "\n\n---\n*-- html from markdown by <a href=\"https://coulomb.social/open/MarkiTect\" target=\"_blank\">MarkiTect</a> on 2025-11-12 01:49:38 by <a href=\"https://coulomb.social/open/worsch\" target=\"_blank\">worsch</a>*";
window.markitectBase64References = {};
const MARKITECT_EDIT_MODE = true;
const MARKITECT_EDITOR_CONFIG = {
mode: 'edit',
theme: 'github',
keyboardShortcuts: true,
autosave: false,
sections: true,
originalFilename: 'GUARDRAILS',
version: 'Markitect v0.8.1.dev26+ge0bc5daee',
repoName: 'Markitect'
};
window.editorConfig = MARKITECT_EDITOR_CONFIG;
// === js/core/section-manager.js ===
/**
* SectionManager Component
*
* Extracted from monolithic editor.js as part of architecture refactoring.
* Manages the collection of sections and their state transitions.
*
* Dependencies:
* - EditState enum (imported)
* - SectionType enum (imported)
* - Section class (imported)
* - debug function (imported)
*/
// Import dependencies - these will be separate modules
const EditState = Object.freeze({
ORIGINAL: 'original',
EDITING: 'editing',
MODIFIED: 'modified',
SAVED: 'saved'
});
const SectionType = Object.freeze({
HEADING: 'heading',
PARAGRAPH: 'paragraph',
LIST: 'list',
CODE: 'code',
QUOTE: 'quote',
TABLE: 'table',
HR: 'hr',
IMAGE: 'image'
});
// Debug function (will be extracted to utils)
function debug(message, category = 'INFO') {
// Simple console debug for now - will be enhanced later
console.log(`DEBUG ${category}: ${message}`);
}
/**
* Section Class - manages individual section state and content
*/
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();
}
static generateId(markdown, position, strategy = 'hash', parentId = null) {
return this.generateIdWithStrategy(markdown, position, strategy, parentId);
}
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);
}
}
static generateAdvancedId(content, position, sectionType) {
const contentHash = this.generateCryptoHash(content);
const safeType = sectionType || 'paragraph';
const typePrefix = safeType.substring(0, 3);
const positionHex = position.toString(16).padStart(2, '0');
return `section-${typePrefix}-${contentHash}-${positionHex}`;
}
static generateCryptoHash(content) {
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;
}
const hexHash = Math.abs(hash).toString(16).padStart(8, '0');
return hexHash.substring(0, 8);
}
static normalizeContentForHashing(content) {
if (!content || typeof content !== 'string') {
return '';
}
return content
.trim()
.replace(/\s+/g, ' ')
.replace(/\r\n/g, '\n')
.toLowerCase();
}
static sanitizeContentForId(content) {
if (!content || typeof content !== 'string') {
return '';
}
return content
.replace(/<[^>]*>/g, '')
.replace(/javascript:/gi, '')
.replace(/[^\w\s\-_.#]/g, '')
.trim();
}
static generateTimestampId(content, position = 0, sectionType = 'paragraph') {
const timestamp = Date.now().toString(36);
const contentSnippet = this.generateCryptoHash(content || '').substring(0, 4);
const safeType = sectionType || 'paragraph';
const typePrefix = safeType.substring(0, 3);
return `section-${typePrefix}-${contentSnippet}-${timestamp}`;
}
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}`;
}
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}`;
}
}
static detectType(markdown) {
if (!markdown || typeof markdown !== 'string') {
return SectionType.PARAGRAPH;
}
const content = markdown.replace(/^\n+|\n+$/g, '');
if (!content) {
return SectionType.PARAGRAPH;
}
const trimmed = content.trim();
// Detection order matters - most specific first
if (this.isHeading(trimmed)) {
return SectionType.HEADING;
}
if (this.isImage(trimmed)) {
return SectionType.IMAGE;
}
if (this.isCodeBlock(trimmed)) {
return SectionType.CODE;
}
return SectionType.PARAGRAPH;
}
static isHeading(trimmed) {
const headingPattern = /^#{1,6}\s+.+/;
return headingPattern.test(trimmed);
}
static isImage(trimmed) {
const imagePattern = /!\[.*?\]\([^)]+\)/;
return imagePattern.test(trimmed);
}
static isCodeBlock(trimmed) {
if (trimmed.startsWith('```') || trimmed.startsWith('~~~')) {
return true;
}
if (trimmed.includes('```') || trimmed.includes('~~~')) {
const codeBlockPattern = /```[\s\S]*?```|~~~[\s\S]*?~~~/;
if (codeBlockPattern.test(trimmed)) {
return true;
}
}
return false;
}
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 (this.editingMarkdown && this.editingMarkdown !== this.currentMarkdown) {
this.pendingMarkdown = this.editingMarkdown;
this.state = EditState.MODIFIED;
} else {
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,
type: this.type,
originalLength: this.originalMarkdown.length,
currentLength: this.currentMarkdown.length
};
}
isImage() {
return this.type === SectionType.IMAGE;
}
redetectType(content = null) {
const markdown = content || this.currentMarkdown;
const oldType = this.type;
this.type = Section.detectType(markdown);
if (oldType !== this.type) {
debug(`Section ${this.id} type changed from ${oldType} to ${this.type}`, 'TYPE_DETECTION');
}
return this.type;
}
}
/**
* SectionManager - 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) {
// Split content into blocks separated by double newlines
const blocks = markdownContent.split(/\n\s*\n/);
const sections = [];
let position = 0;
for (const block of blocks) {
const trimmedBlock = block.trim();
if (!trimmedBlock) continue;
// Check if this block should be split further
const lines = trimmedBlock.split('\n');
let currentSection = '';
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
const isHeading = /^#{1,6}\s/.test(line.trim());
const isImage = /^\s*!\[.*?\]\(.*?\)\s*$/.test(line);
// Each heading or image starts a new section
if ((isHeading || isImage) && currentSection.trim()) {
// Save the previous section
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;
}
}
// Save the final section from this block
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);
position++;
}
}
this.emit('sections-created', { sections, count: sections.length });
return sections;
}
startEditing(sectionId) {
debug('MANAGER: startEditing called for: ' + sectionId, 'MANAGER');
const section = this.sections.get(sectionId);
if (!section) {
throw new Error(`Section ${sectionId} not found`);
}
if (section.isEditing()) {
debug('MANAGER: Section already in editing state: ' + sectionId, 'MANAGER');
return section.editingMarkdown;
}
debug('MANAGER: Starting edit for section: ' + sectionId, 'MANAGER');
const content = section.startEdit();
debug('MANAGER: About to emit edit-started event for: ' + sectionId, 'MANAGER');
this.emit('edit-started', { sectionId, content, section: section.getStatus() });
debug('MANAGER: Emitted edit-started event for: ' + sectionId, 'MANAGER');
return content;
}
updateContent(sectionId, markdown) {
const section = this.sections.get(sectionId);
if (!section) {
throw new Error(`Section ${sectionId} not found`);
}
const oldType = section.type;
section.updateContent(markdown);
const newType = section.redetectType(markdown);
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());
}
getDocumentStatus() {
const sections = Array.from(this.sections.values());
const editingSections = sections.filter(section => section.isEditing).length;
return {
totalSections: sections.length,
editingSections: editingSections
};
}
extractHeadings(content) {
if (!content) return [];
const lines = content.split('\n');
return lines.filter(line => /^#{1,6}\s/.test(line.trim()));
}
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.createSectionsFromMarkdown(newContent);
// Emit section-split event
this.emit('section-split', {
originalSectionId: sectionId,
newSections: newSections,
count: newSections.length
});
return newSections;
}
createSectionsFromContent(content) {
return this.createSectionsFromMarkdown(content);
}
}
// Export for use in tests and other modules
if (typeof module !== 'undefined' && module.exports) {
module.exports = { SectionManager, Section, EditState, SectionType };
}
// Export for browser use
if (typeof window !== 'undefined') {
window.SectionManager = SectionManager;
window.Section = Section;
window.EditState = EditState;
window.SectionType = SectionType;
}
// === js/components/debug-panel.js ===
/**
* DebugPanel Component
*
* Extracted from monolithic editor.js as part of architecture refactoring.
* Handles debug message display and management for client-side debugging.
*
* Dependencies:
* - None (standalone component)
*/
/**
* DebugPanel - Manages debug message display and interaction
*/
class DebugPanel {
constructor() {
this.messages = [];
this.isActive = false;
this.maxMessages = 1000; // Keep last 1000 messages
}
/**
* Add a debug message
*/
addMessage(message, category = 'INFO') {
const messageObj = {
message,
category,
timestamp: new Date().toLocaleTimeString()
};
this.messages.push(messageObj);
// Keep only last maxMessages
if (this.messages.length > this.maxMessages) {
this.messages = this.messages.slice(-this.maxMessages);
}
// Auto-update if panel is visible
if (this.isActive) {
this.update();
}
}
/**
* Toggle the debug panel on/off
*/
toggle() {
const debugContainer = document.getElementById('debug-messages-container');
const debugButton = document.getElementById('toggle-debug');
if (!debugContainer || !debugButton) {
console.warn('DebugPanel: Required DOM elements not found');
return;
}
if (this.isActive) {
this.hide();
} else {
this.show();
}
}
/**
* Show the debug panel
*/
show() {
const debugContainer = document.getElementById('debug-messages-container');
const debugButton = document.getElementById('toggle-debug');
if (!debugContainer || !debugButton) {
console.warn('DebugPanel: Required DOM elements not found');
return;
}
debugContainer.style.display = 'block';
debugButton.textContent = '🔍 Debug (ON)';
debugButton.style.background = '#28a745';
this.isActive = true;
this.update();
}
/**
* Hide the debug panel
*/
hide() {
const debugContainer = document.getElementById('debug-messages-container');
const debugButton = document.getElementById('toggle-debug');
if (!debugContainer || !debugButton) {
console.warn('DebugPanel: Required DOM elements not found');
return;
}
debugContainer.style.display = 'none';
debugButton.textContent = '🔍 Debug';
debugButton.style.background = '#6c757d';
this.isActive = false;
}
/**
* Update the debug panel with current messages
*/
update() {
const debugContainer = document.getElementById('debug-messages-container');
if (!debugContainer || !this.isActive) {
return;
}
if (this.messages.length === 0) {
debugContainer.innerHTML = '<div style="color: #6c757d; font-style: italic; padding: 12px;">No debug messages yet. Click sections to generate debug output.</div>';
return;
}
// Show the last 50 messages in reverse order (newest first)
const recentMessages = this.messages.slice(-50).reverse();
const messagesHtml = recentMessages.map(msg => {
const categoryColor = {
'INFO': '#17a2b8',
'WARNING': '#ffc107',
'ERROR': '#dc3545',
'SUCCESS': '#28a745',
'DEBUG': '#6f42c1'
}[msg.category] || '#6c757d';
return `
<div style="margin-bottom: 6px; padding: 4px; border-left: 3px solid ${categoryColor}; background: white; border-radius: 2px;">
<span style="color: #6c757d; font-size: 11px;">[${msg.timestamp}]</span>
<span style="color: ${categoryColor}; font-weight: bold;">${msg.category}:</span>
<span style="color: #333;">${msg.message}</span>
</div>
`;
}).join('');
debugContainer.innerHTML = `
<div style="margin-bottom: 8px; padding: 6px; background: #e9ecef; border-radius: 4px; font-weight: bold; color: #495057;">
Debug Messages (${this.messages.length} total, showing last ${recentMessages.length})
<button id="debug-clear-btn" style="float: right; background: #dc3545; color: white; border: none; padding: 2px 6px; border-radius: 2px; font-size: 11px; cursor: pointer;">Clear</button>
</div>
<div style="max-height: 250px; overflow-y: auto;">
${messagesHtml}
</div>
`;
// Add event listener for clear button
const clearBtn = debugContainer.querySelector('#debug-clear-btn');
if (clearBtn) {
clearBtn.addEventListener('click', () => {
this.clear();
});
}
// Auto-scroll to bottom to show newest messages
const scrollContainer = debugContainer.querySelector('div[style*="overflow-y"]');
if (scrollContainer) {
scrollContainer.scrollTop = scrollContainer.scrollHeight;
}
}
/**
* Clear all debug messages
*/
clear() {
this.messages = [];
this.update();
}
/**
* Get the number of messages
*/
getMessageCount() {
return this.messages.length;
}
/**
* Get recent messages
*/
getRecentMessages(count = 10) {
return this.messages.slice(-count);
}
}
// Export for use in tests and other modules
if (typeof module !== 'undefined' && module.exports) {
module.exports = { DebugPanel };
}
// Export for browser use
if (typeof window !== 'undefined') {
window.DebugPanel = DebugPanel;
}
// === js/components/document-controls.js ===
/**
* DocumentControls Component
*
* Extracted from monolithic editor.js as part of architecture refactoring.
* Handles the floating control panel and document-level actions.
*
* Dependencies:
* - None (standalone component)
*/
/**
* DocumentControls - Manages the floating control panel and its buttons
*/
class DocumentControls {
constructor() {
this.controlPanel = null;
this.buttons = new Map();
this.eventHandlers = new Map();
this.isVisible = true;
}
/**
* Create the control panel and add it to the DOM
*/
create() {
if (this.controlPanel) {
this.destroy(); // Remove existing panel
}
// Also remove any existing panel with the same ID in the DOM
const existingPanel = document.getElementById('markitect-global-controls');
if (existingPanel && existingPanel.parentNode) {
existingPanel.parentNode.removeChild(existingPanel);
}
// Create the floating control panel
this.controlPanel = document.createElement('div');
this.controlPanel.id = 'markitect-global-controls';
this.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;
`;
// Add title
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';
// Create button container
const buttonContainer = document.createElement('div');
buttonContainer.id = 'button-container';
buttonContainer.style.cssText = `
display: flex;
flex-direction: column;
gap: 6px;
`;
this.controlPanel.appendChild(title);
this.controlPanel.appendChild(buttonContainer);
// Add default buttons
this.addDefaultButtons();
// Add debug messages container
this.addDebugContainer();
// Add to DOM
document.body.appendChild(this.controlPanel);
}
/**
* Add default buttons to the control panel
*/
addDefaultButtons() {
// Save Document button
this.addButton('save-document', '💾 Save Document', '#28a745');
// Reset All button
this.addButton('reset-all', '🔄 Reset All', '#ffc107', '#212529');
// Show Status button
this.addButton('show-status', '📊 Show Status', '#17a2b8');
// Debug button
this.addButton('toggle-debug', '🔍 Debug', '#6c757d');
}
/**
* Add debug container to the control panel
*/
addDebugContainer() {
const debugContainer = document.createElement('div');
debugContainer.id = 'debug-messages-container';
debugContainer.style.cssText = `
margin-top: 12px;
max-height: 300px;
overflow-y: auto;
border: 1px solid #dee2e6;
border-radius: 4px;
background: #f8f9fa;
padding: 8px;
font-family: 'Courier New', monospace;
font-size: 12px;
line-height: 1.4;
display: none;
`;
this.controlPanel.appendChild(debugContainer);
}
/**
* Add a button to the control panel
*/
addButton(id, text, backgroundColor, textColor = 'white') {
const buttonContainer = this.controlPanel.querySelector('#button-container');
if (!buttonContainer) {
throw new Error('Button container not found. Call create() first.');
}
const button = document.createElement('button');
button.id = id;
button.textContent = text;
button.style.cssText = `
background: ${backgroundColor};
color: ${textColor};
border: none;
padding: 8px 12px;
border-radius: 4px;
cursor: pointer;
font-size: 13px;
font-weight: 500;
transition: background-color 0.2s;
`;
buttonContainer.appendChild(button);
this.buttons.set(id, button);
return button;
}
/**
* Remove a button from the control panel
*/
removeButton(id) {
const button = this.buttons.get(id);
if (button && button.parentNode) {
button.parentNode.removeChild(button);
this.buttons.delete(id);
this.eventHandlers.delete(id);
}
}
/**
* Set event handlers for buttons
*/
setEventHandlers(handlers) {
for (const [buttonId, handler] of Object.entries(handlers)) {
const button = this.buttons.get(buttonId);
if (button) {
// Remove existing handler if any
if (this.eventHandlers.has(buttonId)) {
button.removeEventListener('click', this.eventHandlers.get(buttonId));
}
// Add new handler
button.addEventListener('click', handler);
this.eventHandlers.set(buttonId, handler);
}
}
}
/**
* Show the control panel
*/
show() {
if (this.controlPanel) {
this.controlPanel.style.display = 'block';
this.isVisible = true;
}
}
/**
* Hide the control panel
*/
hide() {
if (this.controlPanel) {
this.controlPanel.style.display = 'none';
this.isVisible = false;
}
}
/**
* Update status display (can be extended as needed)
*/
updateStatus(status) {
// This method can be extended to show status information
// For now, it just stores the status for potential display
this.lastStatus = status;
// Could update a status indicator in the panel if needed
if (status && this.controlPanel) {
const title = this.controlPanel.querySelector('div');
if (title) {
const statusText = `Document Controls (${status.totalSections} sections, ${status.editingSections} editing)`;
// Could update title or add status indicator
}
}
}
/**
* Get the control panel element
*/
getControlPanel() {
return this.controlPanel;
}
/**
* Destroy the control panel and clean up
*/
destroy() {
if (this.controlPanel && this.controlPanel.parentNode) {
this.controlPanel.parentNode.removeChild(this.controlPanel);
}
// Clean up references
this.controlPanel = null;
this.buttons.clear();
this.eventHandlers.clear();
this.isVisible = true;
}
/**
* Check if the control panel is visible
*/
isVisible() {
return this.isVisible && this.controlPanel && this.controlPanel.style.display !== 'none';
}
/**
* Get all button IDs
*/
getButtonIds() {
return Array.from(this.buttons.keys());
}
/**
* Get a specific button by ID
*/
getButton(id) {
return this.buttons.get(id);
}
}
// Export for use in tests and other modules
if (typeof module !== 'undefined' && module.exports) {
module.exports = { DocumentControls };
}
// Export for browser use
if (typeof window !== 'undefined') {
window.DocumentControls = DocumentControls;
}
// === js/components/dom-renderer.js ===
/**
* DOMRenderer Component
*
* Extracted from monolithic editor.js as part of architecture refactoring.
* Handles all DOM interactions and UI rendering for section editing.
*
* Dependencies:
* - FloatingMenu component (to be extracted)
* - debug function (imported from utils)
*/
// Import dependencies (placeholders for now)
function debug(message, category = 'INFO') {
console.log(`DEBUG ${category}: ${message}`);
}
/**
* Simple FloatingMenu implementation (will be extracted to separate component later)
*/
class FloatingMenu {
constructor(sectionId, type, renderer) {
this.sectionId = sectionId;
this.type = type;
this.renderer = renderer;
this.element = null;
this.isVisible = false;
}
show(contentElement, controlsElement) {
if (this.isVisible) this.hide();
const targetElement = this.renderer.findSectionElement(this.sectionId);
if (!targetElement) return null;
// Get content dimensions and position
const rect = targetElement.getBoundingClientRect();
const viewport = {
width: window.innerWidth,
height: window.innerHeight
};
// Calculate content width and responsive extension
const contentWidth = rect.width;
const buttonAreaWidth = 120; // Space needed for buttons
const minMenuWidth = Math.max(300, contentWidth); // At least content width or 300px
const preferredMenuWidth = contentWidth + buttonAreaWidth;
// Check if we have space to extend to the right
const spaceOnRight = viewport.width - rect.right;
const canExtendRight = spaceOnRight >= buttonAreaWidth + 20; // 20px margin
// Determine final menu width
let menuWidth;
if (canExtendRight && viewport.width >= 800) { // Only on wide screens
menuWidth = Math.min(preferredMenuWidth, viewport.width - rect.left - 20);
} else {
menuWidth = Math.min(minMenuWidth, viewport.width - 40); // 20px margins
}
// Create floating menu element
this.element = document.createElement('div');
this.element.className = 'ui-edit-floating-menu';
this.element.style.cssText = `
position: fixed;
z-index: 10000;
background: white;
border: 1px solid #ddd;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
padding: 0;
width: ${menuWidth}px;
box-sizing: border-box;
`;
// Add headline
const headline = document.createElement('div');
headline.className = 'ui-edit-headline';
headline.textContent = `Editing ${this.type === 'image' ? 'Image' : 'Section'}`;
headline.style.cssText = `
background: #f8f9fa;
border-bottom: 1px solid #ddd;
padding: 8px 16px;
font-weight: 600;
font-size: 12px;
color: #495057;
border-radius: 8px 8px 0 0;
text-transform: uppercase;
letter-spacing: 0.5px;
`;
// Create content wrapper with padding
const contentWrapper = document.createElement('div');
contentWrapper.style.cssText = `
padding: 16px;
`;
this.element.appendChild(headline);
// Position directly over content (overlay positioning)
let left = rect.left;
let top = rect.top;
// Ensure menu doesn't go off-screen horizontally
if (left + menuWidth > viewport.width) {
left = viewport.width - menuWidth - 20;
}
if (left < 10) {
left = 10;
}
// For vertical positioning, prefer staying on top of content
// Only move if absolutely necessary
const menuHeight = this.type === 'image' ? 350 : 200; // Better height estimates
const wouldGoOffBottom = top + menuHeight > viewport.height;
const wouldGoOffTop = top < 10;
if (wouldGoOffBottom && !wouldGoOffTop) {
// Try to fit by moving up, but keep some overlay if possible
const maxTop = viewport.height - menuHeight - 10;
top = Math.max(rect.top - 50, maxTop); // Prefer staying near original position
} else if (wouldGoOffTop) {
top = 10; // Minimum distance from top
}
// Otherwise, keep the original overlay position
this.element.style.left = `${left}px`;
this.element.style.top = `${top}px`;
// Add content to wrapper
if (contentElement) {
contentWrapper.appendChild(contentElement);
}
if (controlsElement) {
contentWrapper.appendChild(controlsElement);
}
this.element.appendChild(contentWrapper);
// Add close button to headline
const closeButton = document.createElement('button');
closeButton.textContent = '×';
closeButton.style.cssText = `
position: absolute;
top: 4px;
right: 8px;
background: none;
border: none;
font-size: 18px;
cursor: pointer;
color: #666;
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
transition: background-color 0.2s ease;
`;
closeButton.addEventListener('mouseover', () => {
closeButton.style.backgroundColor = '#e9ecef';
});
closeButton.addEventListener('mouseout', () => {
closeButton.style.backgroundColor = 'transparent';
});
closeButton.addEventListener('click', (event) => {
event.stopPropagation();
this.hide();
});
this.element.appendChild(closeButton);
document.body.appendChild(this.element);
this.isVisible = true;
return this.element;
}
hide() {
if (this.element && this.element.parentNode) {
this.element.parentNode.removeChild(this.element);
}
this.element = null;
this.isVisible = false;
// Stop editing state in the section manager
const section = this.renderer.sectionManager.sections.get(this.sectionId);
if (section && section.isEditing()) {
section.stopEditing();
}
// Remove from editing sections
this.renderer.editingSections.delete(this.sectionId);
}
}
/**
* DOMRenderer - Handles DOM interactions and section rendering
*/
class DOMRenderer {
constructor(sectionManager, container) {
this.sectionManager = sectionManager;
this.container = container;
this.editingSections = new Set();
this.currentFloatingMenu = null;
this.eventListenersAttached = false;
this.lastClickTime = 0;
this.clickDebounceMs = 300; // Prevent rapid clicks
// Enhanced Event System - Track 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.handleKeydown = this.handleKeydown.bind(this);
this.setupEventListeners();
}
setupEventListeners() {
this.sectionManager.on('sections-created', (data) => {
this.renderAllSections(data.sections);
});
this.sectionManager.on('edit-started', (data) => {
debug('EVENT: edit-started event received for: ' + data.sectionId, 'EVENT');
this.showEditor(data.sectionId, data.content);
});
}
/**
* Render all sections to the DOM
*/
renderAllSections(sections) {
debug('21: renderAllSections called with ' + sections.length + ' sections', 'RENDER');
// Clear container
this.container.innerHTML = '';
debug('22: Container cleared', 'RENDER');
const contentArea = this.container.querySelector('#markdown-content') || this.container;
// Render each section
sections.forEach((section, index) => {
debug('23: Creating element for section ' + (index + 1) + ': ' + section.id, 'RENDER');
const element = this.renderSection(section);
if (element) {
contentArea.appendChild(element);
}
});
debug('24: All section elements added to container', 'RENDER');
// Attach event listeners only once
if (!this.eventListenersAttached) {
this.container.addEventListener('click', this.handleSectionClick);
this.eventListenersAttached = true;
debug('25: Enhanced event listeners attached for the first time', 'RENDER');
} else {
debug('25: Event listeners already attached, skipping', 'RENDER');
}
debug('25: Container content length: ' + this.container.innerHTML.length, 'RENDER');
}
/**
* Render a single section to DOM element
*/
renderSection(section) {
const element = document.createElement('div');
element.className = 'ui-edit-section';
element.setAttribute('data-section-id', section.id);
// Add section content
// Render all sections using markdown rendering (images need HTML conversion too)
const content = this.simpleMarkdownRender(section.currentMarkdown);
element.innerHTML = content;
// Add styling
element.style.cssText = `
margin: 16px 0;
padding: 12px;
border: 1px solid transparent;
border-radius: 4px;
cursor: pointer;
transition: all 0.2s ease;
`;
element.addEventListener('mouseenter', () => {
element.style.backgroundColor = 'rgba(0, 122, 204, 0.05)';
element.style.borderColor = 'rgba(0, 122, 204, 0.2)';
});
element.addEventListener('mouseleave', () => {
if (!section.isEditing()) {
element.style.backgroundColor = 'transparent';
element.style.borderColor = 'transparent';
}
});
debug('SECTION: Section element setup complete for ' + section.id, 'SECTION');
return element;
}
/**
* Simple markdown rendering (placeholder)
*/
simpleMarkdownRender(markdown) {
return markdown
.replace(/^# (.*$)/gim, '<h1>$1</h1>')
.replace(/^## (.*$)/gim, '<h2>$1</h2>')
.replace(/^### (.*$)/gim, '<h3>$1</h3>')
.replace(/!\[(.*?)\]\((.*?)\)/gim, '<img src="$2" alt="$1" style="max-width: 100%; height: auto; display: block; margin: 1rem auto;" />')
.replace(/\[([^\]]+)\]\(([^)]+)\)/gim, '<a href="$2" target="_blank">$1</a>')
.replace(/\*\*(.*?)\*\*/gim, '<strong>$1</strong>')
.replace(/\*(.*?)\*/gim, '<em>$1</em>')
.replace(/\n/gim, '<br>');
}
/**
* Find DOM element for a section
*/
findSectionElement(sectionId) {
return this.container.querySelector(`[data-section-id="${sectionId}"]`);
}
/**
* Handle section click events
*/
handleSectionClick(event) {
debug('handleSectionClick: Click detected on target: ' + event.target.tagName + ' ' + (event.target.className || ''), 'CLICK');
// Debounce rapid clicks
const now = Date.now();
if (now - this.lastClickTime < this.clickDebounceMs) {
debug('handleSectionClick: Click debounced (too rapid)', 'CLICK');
return;
}
this.lastClickTime = now;
// Don't handle clicks on form elements, buttons, or links
if (event.target.closest('textarea, button, input, a')) {
debug('handleSectionClick: Ignoring click on form element', 'CLICK');
return;
}
const sectionElement = event.target.closest('.ui-edit-section');
debug('handleSectionClick: Found section element: ' + (sectionElement ? sectionElement.getAttribute('data-section-id') : 'null'), 'CLICK');
if (!sectionElement) return;
const sectionId = sectionElement.getAttribute('data-section-id');
debug('handleSectionClick: Section ID: ' + sectionId, 'CLICK');
if (!sectionId) return;
// Track the click event
this.trackEvent('section-click', {
sectionId,
event,
timestamp: Date.now()
});
// Check if this section is already being edited
const section = this.sectionManager.sections.get(sectionId);
debug('handleSectionClick: Found section object: ' + (section ? 'YES' : 'NO'), 'CLICK');
if (section && section.isEditing()) {
debug('handleSectionClick: Section already being edited, checking if dialog is visible: ' + sectionId, 'CLICK');
// If section is editing but no dialog is visible, allow re-opening
const existingDialog = document.querySelector('.ui-edit-floating-menu');
if (existingDialog) {
debug('handleSectionClick: Dialog already visible, ignoring click', 'CLICK');
return;
} else {
debug('handleSectionClick: Section editing but no dialog visible, proceeding to show editor', 'CLICK');
}
}
debug('handleSectionClick: About to start editing for section: ' + sectionId, 'CLICK');
try {
debug('handleSectionClick: Calling sectionManager.startEditing', 'CLICK');
this.sectionManager.startEditing(sectionId);
debug('handleSectionClick: Successfully called startEditing', 'CLICK');
} catch (error) {
debug('handleSectionClick: ERROR in startEditing: ' + error.message, 'ERROR');
console.error('Failed to start editing:', error);
}
}
/**
* Show editor for a section
*/
showEditor(sectionId, content) {
debug('showEditor: called for section: ' + sectionId, 'EDITOR');
const element = this.findSectionElement(sectionId);
debug('showEditor: Found element: ' + (element ? 'YES' : 'NO'), 'EDITOR');
if (!element) return;
debug('showEditor: About to hide current editor', 'EDITOR');
this.hideCurrentEditor();
debug('showEditor: Hidden current editor', 'EDITOR');
const section = this.sectionManager.sections.get(sectionId);
const isImageSection = section && section.isImage();
if (isImageSection) {
this.showImageEditor(sectionId, section);
return;
}
// Create content area for text editing
const editorContent = document.createElement('div');
editorContent.className = 'ui-edit-editor-content';
// Check if we have space for side-by-side layout
const targetElement = this.findSectionElement(sectionId);
const rect = targetElement ? targetElement.getBoundingClientRect() : null;
const viewport = { width: window.innerWidth, height: window.innerHeight };
const hasWideLayout = rect && viewport.width >= 800 && (viewport.width - rect.right) >= 120;
if (hasWideLayout) {
// Side-by-side layout: textarea on left, controls on right
editorContent.style.cssText = `
display: flex;
gap: 16px;
flex: 1;
min-width: 0;
align-items: flex-start;
`;
} else {
// Stacked layout: textarea above, controls below
editorContent.style.cssText = `
display: flex;
flex-direction: column;
gap: 12px;
flex: 1;
min-width: 0;
`;
}
// Create textarea container
const textareaContainer = document.createElement('div');
textareaContainer.style.cssText = hasWideLayout ? 'flex: 1; min-width: 0;' : 'width: 100%;';
// Create textarea
const textarea = document.createElement('textarea');
textarea.value = content || section.currentMarkdown;
textarea.style.cssText = `
width: 100%;
min-height: 120px;
padding: 12px;
border: 1px solid #ddd;
border-radius: 4px;
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
font-size: 14px;
line-height: 1.5;
resize: vertical;
box-sizing: border-box;
`;
// Create controls
const controls = document.createElement('div');
if (hasWideLayout) {
controls.style.cssText = `
display: flex;
flex-direction: column;
gap: 8px;
min-width: 100px;
flex-shrink: 0;
`;
} else {
controls.style.cssText = `
display: flex;
gap: 8px;
justify-content: flex-end;
flex-wrap: wrap;
`;
}
const acceptButton = document.createElement('button');
acceptButton.textContent = hasWideLayout ? '✓' : 'Accept';
acceptButton.style.cssText = `
background: #28a745;
color: white;
border: none;
padding: ${hasWideLayout ? '8px 12px' : '8px 16px'};
border-radius: 4px;
cursor: pointer;
${hasWideLayout ? 'width: 100%;' : ''}
font-size: ${hasWideLayout ? '14px' : '13px'};
`;
const cancelButton = document.createElement('button');
cancelButton.textContent = hasWideLayout ? '✗' : 'Cancel';
cancelButton.style.cssText = `
background: #dc3545;
color: white;
border: none;
padding: ${hasWideLayout ? '8px 12px' : '8px 16px'};
border-radius: 4px;
cursor: pointer;
${hasWideLayout ? 'width: 100%;' : ''}
font-size: ${hasWideLayout ? '14px' : '13px'};
`;
const resetButton = document.createElement('button');
resetButton.textContent = hasWideLayout ? '↺' : '↺ Reset';
resetButton.style.cssText = `
background: #fd7e14;
color: white;
border: none;
padding: ${hasWideLayout ? '8px 12px' : '8px 16px'};
border-radius: 4px;
cursor: pointer;
${hasWideLayout ? 'width: 100%;' : ''}
font-size: ${hasWideLayout ? '14px' : '13px'};
`;
controls.appendChild(acceptButton);
controls.appendChild(cancelButton);
controls.appendChild(resetButton);
// Assemble the layout
textareaContainer.appendChild(textarea);
if (hasWideLayout) {
editorContent.appendChild(textareaContainer);
editorContent.appendChild(controls);
} else {
editorContent.appendChild(textareaContainer);
editorContent.appendChild(controls);
}
// Create floating menu
const floatingMenu = new FloatingMenu(sectionId, 'text', this);
this.currentFloatingMenu = floatingMenu;
this.editingSections.add(sectionId);
floatingMenu.show(editorContent);
// Add event listeners
acceptButton.addEventListener('click', () => {
this.sectionManager.updateContent(sectionId, textarea.value);
this.sectionManager.acceptChanges(sectionId);
floatingMenu.hide();
this.currentFloatingMenu = null; // Clear reference
});
cancelButton.addEventListener('click', () => {
this.sectionManager.cancelChanges(sectionId);
floatingMenu.hide();
this.currentFloatingMenu = null; // Clear reference
});
resetButton.addEventListener('click', () => {
// Reset textarea to original content and apply the change
const section = this.sectionManager.sections.get(sectionId);
if (section) {
textarea.value = section.originalMarkdown;
// Actually update the section content to original and accept the changes
this.sectionManager.updateContent(sectionId, section.originalMarkdown);
this.sectionManager.acceptChanges(sectionId);
// Close the editor
floatingMenu.hide();
this.currentFloatingMenu = null;
}
});
// Auto-focus textarea
setTimeout(() => textarea.focus(), 100);
}
/**
* Show advanced image editor with drag & drop, file upload, and preview
*/
showImageEditor(sectionId, section) {
debug('showImageEditor: called for image section: ' + sectionId, 'EDITOR');
// Track staging state for this editor
const stagingState = {
originalMarkdown: section.originalMarkdown,
currentAltText: '',
currentImageSrc: '',
stagedImageSrc: null,
stagedAltText: null,
hasChanges: false
};
// Parse markdown to extract image info
const imageMatch = section.currentMarkdown.match(/!\[(.*?)\]\((.*?)\)/);
if (imageMatch) {
const [, altText, imageSrc] = imageMatch;
stagingState.currentAltText = altText;
stagingState.currentImageSrc = imageSrc;
}
// Check if we have space for side-by-side layout
const targetElement = this.findSectionElement(sectionId);
const rect = targetElement ? targetElement.getBoundingClientRect() : null;
const viewport = { width: window.innerWidth, height: window.innerHeight };
const hasWideLayout = rect && viewport.width >= 800 && (viewport.width - rect.right) >= 120;
// Create image editor content area
const editorContent = document.createElement('div');
editorContent.className = 'ui-edit-image-content';
if (hasWideLayout) {
// Side-by-side layout: content on left, controls on right
editorContent.style.cssText = `
display: flex;
gap: 16px;
flex: 1;
min-width: 0;
align-items: flex-start;
`;
} else {
// Stacked layout: content above, controls below
editorContent.style.cssText = `
display: flex;
flex-direction: column;
gap: 15px;
flex: 1;
min-width: 0;
`;
}
// Create content container for image and alt text
const contentContainer = document.createElement('div');
contentContainer.style.cssText = hasWideLayout ? 'flex: 1; min-width: 0;' : 'width: 100%;';
if (!hasWideLayout) {
contentContainer.style.cssText += `
display: flex;
flex-direction: column;
gap: 15px;
`;
} else {
contentContainer.style.cssText += `
display: flex;
flex-direction: column;
gap: 12px;
`;
}
// Image preview with drop zone
const imagePreview = document.createElement('div');
imagePreview.className = 'ui-edit-image-preview';
imagePreview.style.cssText = `
width: 100%;
height: 180px;
text-align: center;
background: white;
padding: 12px;
border-radius: 8px;
border: 2px dashed #007bff;
transition: all 0.3s ease;
cursor: pointer;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
position: relative;
box-sizing: border-box;
overflow: hidden;
`;
// 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: 150px;
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: 16px;
`;
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: 14px;
`;
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: 12px;">Supports JPG, PNG, GIF, WebP</div>
`;
imagePreview.appendChild(placeholder);
}
};
// Initialize preview
updateImagePreview(stagingState.currentImageSrc, stagingState.currentAltText);
// File input for image selection
const fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.accept = 'image/*';
fileInput.style.display = 'none';
// Function to handle image file selection
const handleImageFile = (file) => {
if (file && file.type.startsWith('image/')) {
const reader = new FileReader();
reader.onload = (event) => {
stagingState.stagedImageSrc = event.target.result;
stagingState.hasChanges = true;
updateImagePreview(stagingState.stagedImageSrc, altTextInput.value);
updateChangeIndicator();
};
reader.readAsDataURL(file);
}
};
// Drag and drop functionality
imagePreview.addEventListener('dragover', (e) => {
e.preventDefault();
imagePreview.style.borderColor = '#28a745';
imagePreview.style.backgroundColor = '#f8fff8';
const overlay = imagePreview.querySelector('.drop-overlay');
if (overlay) overlay.style.display = 'flex';
});
imagePreview.addEventListener('dragleave', (e) => {
e.preventDefault();
imagePreview.style.borderColor = '#007bff';
imagePreview.style.backgroundColor = 'white';
const overlay = imagePreview.querySelector('.drop-overlay');
if (overlay) overlay.style.display = 'none';
});
imagePreview.addEventListener('drop', (e) => {
e.preventDefault();
imagePreview.style.borderColor = '#007bff';
imagePreview.style.backgroundColor = 'white';
const overlay = imagePreview.querySelector('.drop-overlay');
if (overlay) overlay.style.display = 'none';
const files = e.dataTransfer.files;
if (files.length > 0) {
handleImageFile(files[0]);
}
});
// Click to select file
imagePreview.addEventListener('click', () => {
fileInput.click();
});
fileInput.addEventListener('change', (e) => {
if (e.target.files.length > 0) {
handleImageFile(e.target.files[0]);
}
});
// Alt text editor
const altTextContainer = document.createElement('div');
altTextContainer.className = 'ui-edit-alt-text-container';
altTextContainer.style.cssText = `
display: flex;
flex-direction: column;
gap: 8px;
`;
const altTextLabel = document.createElement('label');
altTextLabel.textContent = 'Alt Text Description:';
altTextLabel.style.cssText = `
font-size: 13px;
font-weight: 600;
color: #333;
margin: 0;
`;
const altTextInput = document.createElement('input');
altTextInput.type = 'text';
altTextInput.value = stagingState.currentAltText;
altTextInput.style.cssText = `
width: 100%;
padding: 10px 12px;
border: 1px solid #ddd;
border-radius: 6px;
font-size: 14px;
box-sizing: border-box;
outline: none;
transition: border-color 0.2s ease;
`;
altTextInput.addEventListener('focus', () => {
altTextInput.style.borderColor = '#007bff';
});
altTextInput.addEventListener('blur', () => {
altTextInput.style.borderColor = '#ddd';
});
// Track alt text changes
altTextInput.addEventListener('input', () => {
stagingState.stagedAltText = altTextInput.value;
stagingState.hasChanges = altTextInput.value !== stagingState.currentAltText || stagingState.stagedImageSrc !== null;
updateChangeIndicator();
});
altTextContainer.appendChild(altTextLabel);
altTextContainer.appendChild(altTextInput);
// Change indicator
const changeIndicator = document.createElement('div');
changeIndicator.className = 'change-indicator';
changeIndicator.style.cssText = `
padding: 8px 12px;
background: #fff3cd;
border: 1px solid #ffeaa7;
border-radius: 6px;
color: #856404;
font-size: 12px;
text-align: center;
display: none;
font-weight: 500;
`;
changeIndicator.textContent = '⚠️ You have unsaved changes';
const updateChangeIndicator = () => {
if (stagingState.hasChanges) {
changeIndicator.style.display = 'block';
} else {
changeIndicator.style.display = 'none';
}
};
// Assemble content container
contentContainer.appendChild(imagePreview);
contentContainer.appendChild(altTextContainer);
contentContainer.appendChild(changeIndicator);
contentContainer.appendChild(fileInput);
// Create controls
const controls = document.createElement('div');
controls.className = 'ui-edit-controls';
if (hasWideLayout) {
controls.style.cssText = `
display: flex;
flex-direction: column;
gap: 8px;
min-width: 100px;
flex-shrink: 0;
`;
} else {
controls.style.cssText = `
display: flex;
flex-direction: column;
gap: 8px;
width: 100%;
`;
}
const acceptBtn = document.createElement('button');
acceptBtn.textContent = hasWideLayout ? '✓' : '✓ Accept';
acceptBtn.style.cssText = `
padding: ${hasWideLayout ? '8px 12px' : '8px 12px'};
font-size: ${hasWideLayout ? '14px' : '12px'};
border-radius: 6px;
border: none;
color: white;
cursor: pointer;
font-weight: 600;
transition: all 0.2s ease;
width: 100%;
text-align: center;
background: #28a745;
`;
const cancelBtn = document.createElement('button');
cancelBtn.textContent = hasWideLayout ? '✗' : '✗ Cancel';
cancelBtn.style.cssText = `
padding: ${hasWideLayout ? '8px 12px' : '8px 12px'};
font-size: ${hasWideLayout ? '14px' : '12px'};
border-radius: 6px;
border: none;
color: white;
cursor: pointer;
font-weight: 600;
transition: all 0.2s ease;
width: 100%;
text-align: center;
background: #dc3545;
`;
const resetBtn = document.createElement('button');
resetBtn.textContent = hasWideLayout ? '↺' : '↺ Reset';
resetBtn.style.cssText = `
padding: ${hasWideLayout ? '8px 12px' : '8px 12px'};
font-size: ${hasWideLayout ? '14px' : '12px'};
border-radius: 6px;
border: none;
color: white;
cursor: pointer;
font-weight: 600;
transition: all 0.2s ease;
width: 100%;
text-align: center;
background: #fd7e14;
`;
controls.appendChild(acceptBtn);
controls.appendChild(cancelBtn);
controls.appendChild(resetBtn);
// Event handlers
acceptBtn.addEventListener('click', () => {
// 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);
}
// Accept changes and hide editor
this.sectionManager.acceptChanges(sectionId);
this.currentFloatingMenu.hide();
this.currentFloatingMenu = null;
});
cancelBtn.addEventListener('click', () => {
// Discard all staged changes and hide editor
this.sectionManager.cancelChanges(sectionId);
this.currentFloatingMenu.hide();
this.currentFloatingMenu = null;
});
resetBtn.addEventListener('click', (event) => {
event.preventDefault();
event.stopPropagation();
// Reset to original content
const originalImageMatch = stagingState.originalMarkdown.match(/!\[(.*?)\]\((.*?)\)/);
if (originalImageMatch) {
const [, originalAltText, originalImageSrc] = originalImageMatch;
// Update staging state to original values
stagingState.currentAltText = originalAltText;
stagingState.currentImageSrc = originalImageSrc;
// Clear any staged changes
stagingState.stagedImageSrc = null;
stagingState.stagedAltText = null;
stagingState.hasChanges = false;
// Reset alt text input to original
altTextInput.value = originalAltText;
// Trigger input event to ensure UI consistency
const inputEvent = new Event('input', { bubbles: true, cancelable: true });
altTextInput.dispatchEvent(inputEvent);
// Reset preview to original image
updateImagePreview(originalImageSrc, originalAltText);
// Update change indicator
updateChangeIndicator();
// Actually update the section content to original and accept the changes
this.sectionManager.updateContent(sectionId, stagingState.originalMarkdown);
this.sectionManager.acceptChanges(sectionId);
// Close the editor
this.currentFloatingMenu.hide();
this.currentFloatingMenu = null;
}
});
// Assemble the final layout
if (hasWideLayout) {
editorContent.appendChild(contentContainer);
editorContent.appendChild(controls);
} else {
editorContent.appendChild(contentContainer);
editorContent.appendChild(controls);
}
// Create floating menu
const floatingMenu = new FloatingMenu(sectionId, 'image', this);
this.currentFloatingMenu = floatingMenu;
this.editingSections.add(sectionId);
floatingMenu.show(editorContent);
}
/**
* Hide current editor
*/
hideCurrentEditor() {
debug('EDITOR: hideCurrentEditor called', 'EDITOR');
if (this.currentFloatingMenu) {
this.currentFloatingMenu.hide();
this.currentFloatingMenu = null;
}
debug('EDITOR: hideCurrentEditor completed', 'EDITOR');
}
/**
* Track event for analytics
*/
trackEvent(eventType, data) {
const eventRecord = {
type: eventType,
data: data,
timestamp: new Date().toISOString()
};
this.eventHistory.push(eventRecord);
if (this.eventStats.hasOwnProperty(eventType)) {
this.eventStats[eventType]++;
}
// Keep only last 100 events
if (this.eventHistory.length > 100) {
this.eventHistory = this.eventHistory.slice(-100);
}
}
/**
* Get event statistics
*/
getEventStats() {
const totalEvents = Object.values(this.eventStats).reduce((sum, count) => sum + count, 0);
return {
stats: { ...this.eventStats },
totalEvents,
recentEvents: this.eventHistory.slice(-10)
};
}
/**
* Handle keyboard shortcuts
*/
handleKeydown(event) {
// Basic keyboard shortcut handling
if (event.ctrlKey || event.metaKey) {
if (event.key === 'Enter') {
// Accept changes
const activeSection = Array.from(this.editingSections)[0];
if (activeSection) {
this.trackEvent('keyboard-shortcut', { shortcut: 'ctrl+enter', action: 'accept' });
}
} else if (event.key === 'Escape') {
// Cancel changes
const activeSection = Array.from(this.editingSections)[0];
if (activeSection) {
this.trackEvent('keyboard-shortcut', { shortcut: 'escape', action: 'cancel' });
this.hideCurrentEditor();
}
}
}
}
}
// Export for use in tests and other modules
if (typeof module !== 'undefined' && module.exports) {
module.exports = { DOMRenderer, FloatingMenu };
}
// Export for browser use
if (typeof window !== 'undefined') {
window.DOMRenderer = DOMRenderer;
window.FloatingMenu = FloatingMenu;
}
// === js/controls/control-base.js ===
/**
* Base Control Class for Markitect UI Controls
* Provides common functionality for positioning, drag, resize, expand/collapse
* Supports Fail Fast strict mode for development
*/
// Development mode detection (must match main.js)
const MARKITECT_STRICT_MODE = (
window.location.hostname === 'localhost' ||
window.location.hostname === '127.0.0.1' ||
window.location.search.includes('strict=true') ||
window.markitectStrictMode === true
);
const Control = {
// Default configuration
config: {
icon: '🔧',
title: 'Control',
className: 'base-control',
defaultContent: 'Control content',
ariaLabel: 'Base Control',
position: 'w', // Default compass position: west (middle-left)
footer: null // If null, will use default Markitect copyright
},
// Utility functions for safe operations
safeOperation: function(operation, fallback = null, context = 'Unknown') {
try {
return operation();
} catch (error) {
console.warn(`Control operation failed in ${context}:`, error);
// Fail Fast in development mode
if (MARKITECT_STRICT_MODE) {
console.error(`🚨 STRICT MODE: Control operation failed in ${context}`);
throw error; // Re-throw for immediate debugging
}
if (window.MarkitectDebugSystem) {
window.MarkitectDebugSystem.addMessage(
`Safe operation failed: ${error.message}`,
'WARNING',
'Control',
{ context, eventType: 'ERROR' }
);
}
return typeof fallback === 'function' ? fallback() : fallback;
}
},
safeQuerySelector: function(selector, parent = document) {
try {
if (!parent || !parent.querySelector) {
return null;
}
return parent.querySelector(selector);
} catch (error) {
console.warn(`Invalid selector: ${selector}`, error);
return null;
}
},
safeQuerySelectorAll: function(selector, parent = document) {
try {
if (!parent || !parent.querySelectorAll) {
return [];
}
return Array.from(parent.querySelectorAll(selector));
} catch (error) {
console.warn(`Invalid selector: ${selector}`, error);
return [];
}
},
// Version and default footer
getMarkitectVersion: function() {
return this.safeOperation(() => {
// Try to get version from various sources
if (window.markitectVersion) {
return window.markitectVersion;
}
// Check for generator meta tag in document head
const generatorMeta = this.safeQuerySelector('meta[name="generator"]');
if (generatorMeta) {
const content = generatorMeta.getAttribute('content');
if (content && content.includes('Markitect')) {
// Extract version from generator content
// Expected formats: "Markitect 1.0.0" or "Markitect/1.0.0" or "Markitect v1.0.0"
const versionMatch = content.match(/Markitect[\s\/v]*([\d\.]+[\w\-\.]*)/i);
if (versionMatch && versionMatch[1]) {
return versionMatch[1];
}
}
}
// Fallback version with generation timestamp
const now = new Date();
const timestamp = now.toISOString().slice(0, 19).replace('T', ' ');
return `Generated ${timestamp}`;
}, () => 'Unknown Version', 'getMarkitectVersion');
},
getDefaultFooter: function() {
return `© Markitect ${this.getMarkitectVersion()}`;
},
getFooter: function() {
if (this.config.footer !== null) {
return this.config.footer;
}
return this.getDefaultFooter();
},
// Compass positioning system (top-aligned for proper expansion)
compassPositions: {
'n': { top: '20px', left: '50%', transform: 'translateX(-50%)' },
'nne': { top: '40px', right: '120px' },
'ne': { top: '20px', right: '20px' },
'ene': { top: '80px', right: '20px' },
'e': { top: '50vh', right: '20px', transform: 'translateY(-20px)' },
'ese': { top: 'calc(50vh + 80px)', right: '20px', transform: 'translateY(-20px)' },
'se': { bottom: '20px', right: '20px' },
'sse': { bottom: '40px', right: '120px' },
's': { bottom: '20px', left: '50%', transform: 'translateX(-50%)' },
'ssw': { bottom: '40px', left: '120px' },
'sw': { bottom: '20px', left: '20px' },
'wsw': { bottom: '80px', left: '20px' },
'w': { top: '50vh', left: '20px', transform: 'translateY(-20px)' },
'wnw': { top: 'calc(50vh - 80px)', left: '20px', transform: 'translateY(-20px)' },
'nw': { top: '20px', left: '20px' },
'nnw': { top: '40px', left: '120px' }
},
// State management
isExpanded: false,
isDragging: false,
isResizing: false,
element: null,
createControl: function() {
return this.safeOperation(() => {
console.log(`Creating ${this.config.title} control...`);
// Validate configuration
if (!this.config || !this.config.title) {
throw new Error('Invalid control configuration');
}
// Ensure document.body exists
if (!document.body) {
throw new Error('Document body not available');
}
// Create main control element
this.element = document.createElement('div');
this.element.className = `control-panel ${this.config.className || ''}`;
this.element.setAttribute('role', 'dialog');
this.element.setAttribute('aria-label', this.config.ariaLabel || this.config.title);
// Position the control using compass system
const position = this.compassPositions[this.config.position] || this.compassPositions['w'];
Object.assign(this.element.style, {
position: 'fixed',
zIndex: '1000',
...position
});
// Build the control structure
this.buildControlStructure();
// Add to document
document.body.appendChild(this.element);
console.log(`${this.config.title} control created and positioned at ${this.config.position}`);
return this.element;
}, () => {
console.error(`Failed to create ${this.config?.title || 'Unknown'} control`);
return null;
}, 'createControl');
},
buildControlStructure: function() {
this.safeOperation(() => {
if (!this.element) {
throw new Error('Control element not available');
}
// Sanitize configuration values
const safeIcon = (this.config.icon || '🔧').replace(/[<>"'&]/g, '');
const safeTitle = (this.config.title || 'Control').replace(/[<>"'&]/g, '');
const safeContent = (this.config.defaultContent || 'Control content').replace(/[<>]/g, '');
this.element.innerHTML = `
<div class="control-header" style="
display: flex; align-items: center; justify-content: space-between;
padding: 0.5rem; background: #f8f9fa; border-radius: 6px;
cursor: pointer; user-select: none; font-size: 0.9rem;
box-shadow: 0 2px 8px rgba(0,0,0,0.1); border: 1px solid #dee2e6;
transition: all 0.2s ease; min-width: 120px;">
<div style="display: flex; align-items: center; gap: 0.5rem;">
<span style="font-size: 1.2rem;">${safeIcon}</span>
<span class="control-title" style="font-weight: 500; color: #495057;">${safeTitle}</span>
</div>
<button class="control-close" style="
background: none; border: none; font-size: 1.1rem; color: #6c757d;
cursor: pointer; padding: 0; width: 20px; height: 20px;
display: none; align-items: center; justify-content: center;
border-radius: 50%; transition: all 0.2s ease;"
onmouseover="this.style.backgroundColor='#e9ecef'"
onmouseout="this.style.backgroundColor=''"
onclick="event.stopPropagation();">×</button>
</div>
<div class="control-content" style="
display: none; background: white; border: 1px solid #dee2e6;
border-radius: 6px; margin-top: 0.5rem; max-width: 350px;
min-width: 250px; max-height: 400px; overflow-y: auto;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);">
<div style="padding: 1rem;">
${safeContent}
</div>
<div class="control-footer" style="display: none;"></div>
</div>
`;
// Set up event listeners with error protection
this.safeOperation(() => this.setupEventListeners(), null, 'setupEventListeners');
this.safeOperation(() => this.addResizeHandle(), null, 'addResizeHandle');
this.safeOperation(() => this.addDragFunctionality(), null, 'addDragFunctionality');
}, () => {
console.error('Failed to build control structure');
if (this.element) {
this.element.innerHTML = '<div style="color: red; padding: 0.5rem;">Control failed to load</div>';
}
}, 'buildControlStructure');
},
setupEventListeners: function() {
const header = this.safeQuerySelector('.control-header', this.element);
const closeBtn = this.safeQuerySelector('.control-close', this.element);
if (!header || !closeBtn) {
console.warn('Control header or close button not found');
return;
}
// Toggle expand/collapse on header click
header.addEventListener('click', (e) => {
this.safeOperation(() => {
e.stopPropagation();
this.toggle();
}, null, 'headerClick');
});
// Close button
closeBtn.addEventListener('click', (e) => {
this.safeOperation(() => {
e.stopPropagation();
this.collapse();
}, null, 'closeClick');
});
// Show/hide close button and resize handle on hover with bounds checking
this.element.addEventListener('mouseenter', () => {
this.safeOperation(() => {
if (this.isExpanded && closeBtn) {
closeBtn.style.display = 'flex';
const resizeHandle = this.safeQuerySelector('.resize-handle', this.element);
if (resizeHandle) {
resizeHandle.style.display = 'block';
}
}
}, null, 'mouseEnter');
});
this.element.addEventListener('mouseleave', () => {
this.safeOperation(() => {
if (closeBtn) {
closeBtn.style.display = 'none';
}
const resizeHandle = this.safeQuerySelector('.resize-handle', this.element);
if (resizeHandle) {
resizeHandle.style.display = 'none';
}
}, null, 'mouseLeave');
});
},
addResizeHandle: function() {
const resizeHandle = document.createElement('div');
resizeHandle.className = 'resize-handle';
resizeHandle.innerHTML = ''; // Small circle via CSS
resizeHandle.style.cssText = `
position: absolute; bottom: 2px; right: 2px;
width: 8px; height: 8px; cursor: nw-resize;
display: none; background: #6c757d; border-radius: 50%;
`;
this.element.appendChild(resizeHandle);
// Resize functionality
let startX, startY, startWidth, startHeight;
resizeHandle.addEventListener('mousedown', (e) => {
e.preventDefault();
e.stopPropagation();
this.isResizing = true;
const content = this.element.querySelector('.control-content');
const rect = content.getBoundingClientRect();
startX = e.clientX;
startY = e.clientY;
startWidth = rect.width;
startHeight = rect.height;
document.addEventListener('mousemove', handleResize);
document.addEventListener('mouseup', stopResize);
});
const handleResize = (e) => {
if (!this.isResizing) return;
const content = this.element.querySelector('.control-content');
const deltaX = e.clientX - startX;
const deltaY = e.clientY - startY;
const newWidth = Math.max(200, startWidth + deltaX);
const newHeight = Math.max(100, startHeight + deltaY);
content.style.width = `${newWidth}px`;
content.style.height = `${newHeight}px`;
};
const stopResize = () => {
this.isResizing = false;
document.removeEventListener('mousemove', handleResize);
document.removeEventListener('mouseup', stopResize);
};
},
addDragFunctionality: function() {
const header = this.safeQuerySelector('.control-header', this.element);
if (!header) {
console.warn('Header not found for drag functionality');
return;
}
let startX, startY, startLeft, startTop, dragTimeout;
header.addEventListener('mousedown', (e) => {
this.safeOperation(() => {
if (e.target.closest('.control-close')) return;
// Clear any existing drag timeout
if (dragTimeout) {
clearTimeout(dragTimeout);
}
this.isDragging = true;
const rect = this.element.getBoundingClientRect();
startX = e.clientX;
startY = e.clientY;
startLeft = rect.left;
startTop = rect.top;
document.addEventListener('mousemove', handleDrag);
document.addEventListener('mouseup', stopDrag);
// Safety timeout to prevent infinite dragging
dragTimeout = setTimeout(() => {
if (this.isDragging) {
console.warn('Drag operation timed out');
stopDrag();
}
}, 30000); // 30 second timeout
}, null, 'dragStart');
});
const handleDrag = (e) => {
this.safeOperation(() => {
if (!this.isDragging || !this.element) return;
const deltaX = e.clientX - startX;
const deltaY = e.clientY - startY;
// Constrain to viewport bounds
const viewportWidth = window.innerWidth || document.documentElement.clientWidth;
const viewportHeight = window.innerHeight || document.documentElement.clientHeight;
const newLeft = Math.max(0, Math.min(viewportWidth - 100, startLeft + deltaX));
const newTop = Math.max(0, Math.min(viewportHeight - 50, startTop + deltaY));
this.element.style.left = `${newLeft}px`;
this.element.style.top = `${newTop}px`;
this.element.style.right = 'auto';
this.element.style.bottom = 'auto';
this.element.style.transform = 'none';
}, null, 'dragMove');
};
const stopDrag = () => {
this.safeOperation(() => {
this.isDragging = false;
if (dragTimeout) {
clearTimeout(dragTimeout);
dragTimeout = null;
}
document.removeEventListener('mousemove', handleDrag);
document.removeEventListener('mouseup', stopDrag);
}, null, 'dragStop');
};
},
expand: function() {
this.safeOperation(() => {
if (this.isExpanded) return;
const content = this.safeQuerySelector('.control-content', this.element);
const closeBtn = this.safeQuerySelector('.control-close', this.element);
if (!content || !closeBtn) {
console.warn('Control content or close button not found for expansion');
return;
}
content.style.display = 'block';
closeBtn.style.display = 'flex';
this.isExpanded = true;
// Style footer
this.styleFooter();
console.log(`${this.config.title || 'Unknown'} control expanded`);
}, null, 'expand');
},
collapse: function() {
this.safeOperation(() => {
if (!this.isExpanded) return;
const content = this.safeQuerySelector('.control-content', this.element);
const closeBtn = this.safeQuerySelector('.control-close', this.element);
const resizeHandle = this.safeQuerySelector('.resize-handle', this.element);
if (content) {
content.style.display = 'none';
content.style.width = '';
content.style.height = '';
}
if (closeBtn) {
closeBtn.style.display = 'none';
}
if (resizeHandle) {
resizeHandle.style.display = 'none';
}
this.isExpanded = false;
console.log(`${this.config.title || 'Unknown'} control collapsed`);
}, null, 'collapse');
},
toggle: function() {
this.safeOperation(() => {
if (this.isExpanded) {
this.collapse();
} else {
if (this.buildContent) {
this.buildContent();
} else {
this.expand();
}
}
}, null, 'toggle');
},
styleFooter: function() {
this.safeOperation(() => {
const footer = this.safeQuerySelector('.control-footer', this.element);
if (!footer) return;
const footerText = this.getFooter();
if (footerText && footerText.trim()) {
// Sanitize footer text
const safeText = footerText.replace(/[<>"'&]/g, '');
footer.textContent = safeText;
footer.style.cssText = `
display: block; padding: 0.5rem; font-size: 0.7rem;
color: #6c757d; text-align: center; font-style: italic;
background: #f8f9fa; border-top: 1px solid #e9ecef;
border-radius: 0 0 6px 6px;
`;
} else {
footer.style.display = 'none';
}
}, null, 'styleFooter');
},
// Virtual method - should be overridden by specific controls
buildContent: function() {
this.safeOperation(() => {
console.log(`${this.config.title || 'Unknown'} control - buildContent should be overridden`);
this.expand();
}, () => {
console.error('Failed to build content, expanding basic control');
this.expand();
}, 'buildContent');
}
};
// Export for use in other modules
window.Control = Control;
// === js/controls/contents-control.js ===
/**
* Contents Control - Displays document table of contents
* Implements the Robustness Principle with Fail Fast mode support
*/
class ContentsControl {
constructor() {
this.control = Object.create(Control);
this.control.config = {
icon: '☰',
title: 'Contents',
className: 'contents-control',
defaultContent: 'Click to view table of contents',
ariaLabel: 'Contents Control',
position: 'w'
};
// Bind methods to control
this.control.buildContent = () => {
const content = this.control.element.querySelector('.control-content');
const headings = this.extractHeadings();
content.innerHTML = `
<div style="padding: 1rem; font-size: 0.8rem;">
<h4 style="margin-top: 0;">Table of Contents</h4>
<div style="max-height: 250px; overflow-y: auto;">
${headings.length > 0 ?
headings.map(heading =>
`<div style="margin-bottom: 0.3rem; padding-left: ${(heading.level - 1) * 15}px;">
<a href="#${heading.id}"
style="text-decoration: none; color: #0066cc; font-size: ${0.9 - (heading.level - 1) * 0.05}rem;"
onclick="document.getElementById('${heading.id}')?.scrollIntoView({behavior: 'smooth'})">
${heading.text}
</a>
</div>`
).join('') :
'<p>No headings found in document</p>'
}
</div>
<div style="margin-top: 1rem; font-size: 0.7rem; color: #666;">
${headings.length} heading${headings.length !== 1 ? 's' : ''} found
</div>
</div>
`;
this.control.isExpanded = true;
};
this.control.toggle = () => {
if (this.control.isExpanded) {
this.control.element.querySelector('.control-content').style.display = 'none';
this.control.isExpanded = false;
} else {
this.control.buildContent();
this.control.element.querySelector('.control-content').style.display = 'block';
}
};
}
extractHeadings() {
const headings = [];
const elements = document.querySelectorAll('h1, h2, h3, h4, h5, h6');
elements.forEach((heading, index) => {
const level = parseInt(heading.tagName.charAt(1));
const text = heading.textContent || heading.innerText || '';
let id = heading.id;
// Generate ID if not present
if (!id) {
id = text.toLowerCase()
.replace(/[^\w\s-]/g, '')
.replace(/\s+/g, '-')
.replace(/-+/g, '-')
.replace(/^-|-$/g, '') || `heading-${index}`;
heading.id = id;
}
headings.push({
level: level,
text: text.trim(),
id: id
});
});
return headings;
}
createControl() {
return this.control.createControl();
}
}
window.ContentsControl = ContentsControl;
// === js/controls/status-control.js ===
/**
* Status Control - Document statistics and change tracking
*/
class StatusControl {
constructor() {
this.control = Object.create(Control);
// Configure for status functionality
this.control.config = {
icon: '📊',
title: 'Status',
className: 'status-control',
defaultContent: 'Document statistics and changes',
ariaLabel: 'Status Control',
position: 'e', // East positioning
footer: `Updated ${new Date().toLocaleTimeString()}`
};
// Initialize change tracking
this.control.changeTracking = {
headings: new Set(),
sections: new Set(),
images: new Set(),
tables: new Set(),
lastScanTime: null,
initialCounts: {
headings: 0,
sections: 0,
images: 0,
tables: 0,
lines: 0,
words: 0,
characters: 0
}
};
this.bindMethods();
}
bindMethods() {
// Bind utility functions
this.control.safeTextExtraction = this.safeTextExtraction.bind(this);
this.control.sanitizeText = this.sanitizeText.bind(this);
this.control.validateElement = this.validateElement.bind(this);
this.control.safeStatsOperation = this.safeStatsOperation.bind(this);
// Bind existing methods
this.control.calculateStats = this.calculateStats.bind(this);
this.control.isContentSection = this.isContentSection.bind(this);
this.control.isContentTable = this.isContentTable.bind(this);
this.control.updateChangeTracking = this.updateChangeTracking.bind(this);
this.control.buildContent = this.buildContent.bind(this);
this.control.refreshStats = this.refreshStats.bind(this);
this.control.resetChangeTracking = this.resetChangeTracking.bind(this);
this.control.setupAutoRefresh = this.setupAutoRefresh.bind(this);
// Override collapse to clean up intervals
const originalCollapse = this.control.collapse;
this.control.collapse = () => {
if (this.control.autoRefreshInterval) {
clearInterval(this.control.autoRefreshInterval);
this.control.autoRefreshInterval = null;
}
originalCollapse.call(this.control);
};
}
// Utility functions for safe operations
safeTextExtraction(element) {
if (!this.validateElement(element)) {
return '';
}
try {
const text = element.textContent || element.innerText || '';
return this.sanitizeText(text.trim());
} catch (error) {
console.warn('Text extraction failed:', error);
return '';
}
}
sanitizeText(text) {
if (typeof text !== 'string') {
return '';
}
// Remove potentially harmful characters and limit length
const maxLength = 100000; // 100KB text limit
const sanitized = text
.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '') // Remove control chars
.slice(0, maxLength); // Limit length
return sanitized;
}
validateElement(element) {
return element &&
element.nodeType === Node.ELEMENT_NODE &&
element.isConnected &&
!element.closest('.control-panel'); // Avoid control elements
}
safeStatsOperation(operation, fallback = 0, context = 'stats') {
try {
const result = operation();
// Validate numeric results
return typeof result === 'number' && isFinite(result) ? result : fallback;
} catch (error) {
console.warn(`Stats operation failed in ${context}:`, error);
if (window.MarkitectDebugSystem) {
window.MarkitectDebugSystem.addMessage(
`Stats operation failed: ${error.message}`,
'WARNING',
'StatusControl',
{ context, eventType: 'ERROR' }
);
}
return fallback;
}
}
calculateStats() {
const stats = {
headings: { total: 0, changed: 0 },
sections: { total: 0, changed: 0 },
images: { total: 0, changed: 0 },
tables: { total: 0, changed: 0 },
document: { lines: 0, words: 0, characters: 0 },
sections_detail: { lines: 0, words: 0, characters: 0 },
tables_detail: { lines: 0, words: 0, characters: 0 }
};
return this.safeStatsOperation(() => {
// Count headings (h1-h6, excluding control titles)
const headings = this.control.safeQuerySelectorAll('h1, h2, h3, h4, h5, h6');
const maxElements = 10000; // Limit processing to prevent DoS
headings.slice(0, maxElements).forEach(heading => {
if (!this.validateElement(heading)) return;
const text = this.safeTextExtraction(heading).toLowerCase();
// Skip control headings with enhanced filtering
const controlKeywords = ['contents', 'debug', 'status', 'control', 'menu', 'toolbar'];
const isControlHeading = controlKeywords.some(keyword => text.includes(keyword));
if (text.length > 0 && !isControlHeading) {
stats.headings.total++;
const fullText = this.safeTextExtraction(heading);
if (this.control.changeTracking.headings.has(fullText)) {
stats.headings.changed++;
}
}
});
// Count sections (content blocks excluding headings and table cells)
const sections = this.control.safeQuerySelectorAll('p, blockquote, pre, li, div');
sections.slice(0, maxElements).forEach(section => {
if (this.isContentSection(section)) {
stats.sections.total++;
const sectionText = this.safeTextExtraction(section);
if (sectionText.length > 0) {
const lines = this.safeStatsOperation(() => sectionText.split('\n').length, 0, 'countLines');
const words = this.safeStatsOperation(() =>
sectionText.split(/\s+/).filter(w => w.length > 0).length, 0, 'countWords');
const characters = Math.min(sectionText.length, 1000000); // Cap at 1MB
stats.sections_detail.lines += lines;
stats.sections_detail.words += words;
stats.sections_detail.characters += characters;
if (this.control.changeTracking.sections.has(sectionText)) {
stats.sections.changed++;
}
}
}
});
// Count tables as separate entities
const tables = this.control.safeQuerySelectorAll('table');
tables.slice(0, maxElements).forEach(table => {
if (this.isContentTable(table)) {
stats.tables.total++;
const tableText = this.safeTextExtraction(table);
if (tableText.length > 0) {
const lines = this.safeStatsOperation(() => tableText.split('\n').length, 0, 'countTableLines');
const words = this.safeStatsOperation(() =>
tableText.split(/\s+/).filter(w => w.length > 0).length, 0, 'countTableWords');
const characters = Math.min(tableText.length, 1000000); // Cap at 1MB
stats.tables_detail.lines += lines;
stats.tables_detail.words += words;
stats.tables_detail.characters += characters;
// Generate safer table identifier
const tableId = this.sanitizeText(table.id ||
table.outerHTML.substring(0, 100).replace(/[<>"'&]/g, ''));
if (this.control.changeTracking.tables.has(tableId)) {
stats.tables.changed++;
}
}
}
});
// Count images with validation
const images = this.control.safeQuerySelectorAll('img');
images.slice(0, maxElements).forEach(img => {
if (this.validateElement(img)) {
stats.images.total++;
// Safely extract and validate image source
const imgSrc = this.sanitizeText(img.src || img.getAttribute('src') || '');
if (imgSrc && this.control.changeTracking.images.has(imgSrc)) {
stats.images.changed++;
}
}
});
// Calculate total document stats with protection
const bodyText = this.safeTextExtraction(document.body);
if (bodyText) {
const cleanText = bodyText.replace(/\s+/g, ' ');
stats.document.lines = this.safeStatsOperation(() =>
bodyText.split('\n').length, 0, 'countDocLines');
stats.document.words = this.safeStatsOperation(() =>
cleanText.split(/\s+/).filter(w => w.length > 0).length, 0, 'countDocWords');
stats.document.characters = Math.min(cleanText.length, 10000000); // Cap at 10MB
}
return stats;
}, stats, 'calculateStats');
}
isContentSection(element) {
return this.safeStatsOperation(() => {
if (!this.validateElement(element)) {
return false;
}
// Enhanced control detection with timeout protection
let current = element;
let depth = 0;
const maxDepth = 50; // Prevent infinite loops
while (current && current !== document.body && depth < maxDepth) {
if (current.classList && (
current.classList.contains('control-panel') ||
current.classList.contains('control-content') ||
current.classList.contains('control-header') ||
current.className.includes('control') ||
current.id?.includes('control')
)) {
return false;
}
current = current.parentElement;
depth++;
}
// Skip if element is inside a table (tables are counted separately)
if (element.closest && element.closest('table')) {
return false;
}
// Skip if element has no meaningful text content
const text = this.safeTextExtraction(element);
return text.length > 0 && text.length < 50000; // Reasonable size limit
}, false, 'isContentSection');
}
isContentTable(table) {
return this.safeStatsOperation(() => {
if (!this.validateElement(table) || table.tagName !== 'TABLE') {
return false;
}
// Enhanced control detection with depth limiting
let current = table;
let depth = 0;
const maxDepth = 50;
while (current && current !== document.body && depth < maxDepth) {
if (current.classList && (
current.classList.contains('control-panel') ||
current.classList.contains('control-content') ||
current.classList.contains('control-header') ||
current.className.includes('control') ||
current.id?.includes('control')
)) {
return false;
}
current = current.parentElement;
depth++;
}
// Check if table has meaningful content with limits
const text = this.safeTextExtraction(table);
return text.length > 0 && text.length < 100000; // Reasonable table size limit
}, false, 'isContentTable');
}
updateChangeTracking() {
const now = Date.now();
// Headings
const headings = document.querySelectorAll('h1, h2, h3, h4, h5, h6');
headings.forEach(heading => {
const text = heading.textContent.trim();
if (text && !text.toLowerCase().includes('control')) {
const changed = heading.dataset.lastModified &&
(now - parseInt(heading.dataset.lastModified)) < 300000; // 5 minutes
if (changed) {
this.control.changeTracking.headings.add(text);
}
}
});
// Sections
const sections = document.querySelectorAll('p, blockquote, pre, li, div');
sections.forEach(section => {
if (this.isContentSection(section)) {
const text = section.textContent.trim();
if (text.length > 0) {
const changed = section.dataset.lastModified &&
(now - parseInt(section.dataset.lastModified)) < 300000; // 5 minutes
if (changed) {
this.control.changeTracking.sections.add(text);
}
}
}
});
// Tables
const tables = document.querySelectorAll('table');
tables.forEach(table => {
if (this.isContentTable(table)) {
const tableId = table.id || table.outerHTML.substring(0, 100);
const changed = table.dataset.lastModified &&
(now - parseInt(table.dataset.lastModified)) < 300000; // 5 minutes
if (changed) {
this.control.changeTracking.tables.add(tableId);
}
}
});
// Images
const images = document.querySelectorAll('img');
images.forEach(img => {
const src = img.src || img.getAttribute('src') || '';
const changed = img.dataset.lastModified &&
(now - parseInt(img.dataset.lastModified)) < 300000; // 5 minutes
if (changed && src) {
this.control.changeTracking.images.add(src);
}
});
this.control.changeTracking.lastScanTime = now;
}
buildContent() {
this.control.safeOperation(() => {
console.log("📊 Building status control content...");
const content = this.control.safeQuerySelector('.control-content', this.control.element);
if (!content) {
console.error("📊 Status control content element not found");
return;
}
// Update tracking and calculate stats with timeout protection
const timeout = setTimeout(() => {
console.warn('Status content build operation timed out');
}, 10000); // 10 second timeout
this.updateChangeTracking();
const stats = this.calculateStats();
clearTimeout(timeout);
// Sanitize numeric values to prevent injection
const safeStats = {
document: {
lines: Math.max(0, Math.floor(stats.document.lines || 0)),
words: Math.max(0, Math.floor(stats.document.words || 0)),
characters: Math.max(0, Math.floor(stats.document.characters || 0))
},
headings: {
total: Math.max(0, Math.floor(stats.headings.total || 0)),
changed: Math.max(0, Math.floor(stats.headings.changed || 0))
},
sections: {
total: Math.max(0, Math.floor(stats.sections.total || 0)),
changed: Math.max(0, Math.floor(stats.sections.changed || 0))
},
sections_detail: {
lines: Math.max(0, Math.floor(stats.sections_detail.lines || 0)),
words: Math.max(0, Math.floor(stats.sections_detail.words || 0)),
characters: Math.max(0, Math.floor(stats.sections_detail.characters || 0))
},
tables: {
total: Math.max(0, Math.floor(stats.tables.total || 0)),
changed: Math.max(0, Math.floor(stats.tables.changed || 0))
},
tables_detail: {
lines: Math.max(0, Math.floor(stats.tables_detail.lines || 0)),
words: Math.max(0, Math.floor(stats.tables_detail.words || 0)),
characters: Math.max(0, Math.floor(stats.tables_detail.characters || 0))
},
images: {
total: Math.max(0, Math.floor(stats.images.total || 0)),
changed: Math.max(0, Math.floor(stats.images.changed || 0))
}
};
// Use safe stats for display with proper escaping
content.innerHTML = `
<div style="font-size: 0.8rem; line-height: 1.4; color: #495057;">
<!-- Document Overview -->
<div style="margin-bottom: 0.75rem; padding: 0.5rem; background: #f8f9fa; border-radius: 4px;">
<div style="font-weight: 600; color: #212529; margin-bottom: 0.5rem;">📄 Document</div>
<div style="font-size: 0.7rem;">
<span>Lines: <strong>${safeStats.document.lines.toLocaleString()}</strong> | Words: <strong>${safeStats.document.words.toLocaleString()}</strong> | Chars: <strong>${safeStats.document.characters.toLocaleString()}</strong></span>
</div>
</div>
<!-- Headings -->
<div style="margin-bottom: 0.75rem; padding: 0.5rem; background: #f3e5f5; border-radius: 4px;">
<div style="font-weight: 600; color: #7b1fa2;">
📋 Headings: <strong>${safeStats.headings.total}</strong>
${safeStats.headings.changed > 0 ? `<span style="color: #28a745; font-size: 0.6rem;"> (+${safeStats.headings.changed})</span>` : ''}
</div>
</div>
<!-- Sections -->
<div style="margin-bottom: 0.75rem; padding: 0.5rem; background: #e3f2fd; border-radius: 4px;">
<div style="font-weight: 600; color: #1565c0; margin-bottom: 0.5rem;">
📄 Sections: <strong>${safeStats.sections.total}</strong>
${safeStats.sections.changed > 0 ? `<span style="color: #28a745; font-size: 0.6rem;"> (+${safeStats.sections.changed})</span>` : ''}
</div>
<div style="font-size: 0.7rem;">
<span>Lines: <strong>${safeStats.sections_detail.lines.toLocaleString()}</strong> | Words: <strong>${safeStats.sections_detail.words.toLocaleString()}</strong> | Chars: <strong>${safeStats.sections_detail.characters.toLocaleString()}</strong></span>
</div>
</div>
<!-- Tables -->
<div style="margin-bottom: 0.75rem; padding: 0.5rem; background: #fff3e0; border-radius: 4px;">
<div style="font-weight: 600; color: #ef6c00; margin-bottom: 0.5rem;">
🗂️ Tables: <strong>${safeStats.tables.total}</strong>
${safeStats.tables.changed > 0 ? `<span style="color: #28a745; font-size: 0.6rem;"> (+${safeStats.tables.changed})</span>` : ''}
</div>
<div style="font-size: 0.7rem;">
<span>Lines: <strong>${safeStats.tables_detail.lines.toLocaleString()}</strong> | Words: <strong>${safeStats.tables_detail.words.toLocaleString()}</strong> | Chars: <strong>${safeStats.tables_detail.characters.toLocaleString()}</strong></span>
</div>
</div>
<!-- Images -->
<div style="margin-bottom: 0.75rem; padding: 0.5rem; background: #e8f5e8; border-radius: 4px;">
<div style="font-weight: 600; color: #2e7d32;">
🖼️ Images: <strong>${safeStats.images.total}</strong>
${safeStats.images.changed > 0 ? `<span style="color: #28a745; font-size: 0.6rem;"> (+${safeStats.images.changed})</span>` : ''}
</div>
</div>
<!-- Actions with safer onclick handlers -->
<div style="display: flex; gap: 0.25rem; margin-top: 0.5rem;">
<button id="status-refresh-btn"
style="flex: 1; padding: 0.4rem; background: #6c757d; color: white;
border: none; border-radius: 3px; cursor: pointer; font-size: 0.7rem;">
🔄 Refresh
</button>
<button id="status-reset-btn"
style="flex: 1; padding: 0.4rem; background: #dc3545; color: white;
border: none; border-radius: 3px; cursor: pointer; font-size: 0.7rem;">
🔄 Reset Changes
</button>
</div>
</div>
`;
// Add safer event listeners instead of inline onclick
const refreshBtn = content.querySelector('#status-refresh-btn');
const resetBtn = content.querySelector('#status-reset-btn');
if (refreshBtn) {
refreshBtn.addEventListener('click', () => {
this.control.safeOperation(() => {
if (window.statusControl && window.statusControl.refreshStats) {
window.statusControl.refreshStats();
}
}, null, 'refreshButton');
});
}
if (resetBtn) {
resetBtn.addEventListener('click', () => {
this.control.safeOperation(() => {
if (window.statusControl && window.statusControl.resetChangeTracking) {
window.statusControl.resetChangeTracking();
}
}, null, 'resetButton');
});
}
console.log("📊 Status control content built successfully");
// Set up auto-refresh
this.setupAutoRefresh();
// Show panel and expand
this.control.expand();
}, () => {
console.error("📊 Error in buildContent: Failed to build status control content");
const content = this.control.safeQuerySelector('.control-content', this.control.element);
if (content) {
content.innerHTML = '<div style="color: #dc3545;">Status loading failed</div>';
}
}, 'buildContent');
}
refreshStats() {
if (this.control.isExpanded) {
this.updateChangeTracking();
// Update footer timestamp
this.control.config.footer = `Updated ${new Date().toLocaleTimeString()}`;
this.control.styleFooter();
const content = this.control.element.querySelector('.control-content');
if (content) {
const stats = this.calculateStats();
// Update the display without rebuilding entire content
this.buildContent();
}
}
}
resetChangeTracking() {
if (confirm('Reset all document changes? This will revert all sections to their original state.')) {
console.log('📊 Resetting document changes...');
// Reset using available infrastructure
if (window.sectionManager && window.domRenderer) {
// Use the proper document management infrastructure
try {
// Hide any open editors
window.domRenderer.hideCurrentEditor();
// Reset all sections to original state
const allSections = Array.from(window.sectionManager.sections.values());
allSections.forEach(section => {
section.resetToOriginal();
});
// Re-render all sections
window.domRenderer.renderAllSections(allSections);
console.log('📊 Document reset successful');
// Add to debug system
if (window.MarkitectDebugSystem) {
window.MarkitectDebugSystem.addMessage(
`Document reset completed - ${allSections.length} sections restored`,
'SUCCESS',
'StatusControl',
{ eventType: 'SYSTEM' }
);
}
} catch (error) {
console.error('📊 Document reset failed:', error);
if (window.MarkitectDebugSystem) {
window.MarkitectDebugSystem.addMessage(
`Document reset failed: ${error.message}`,
'ERROR',
'StatusControl',
{ eventType: 'SYSTEM' }
);
}
}
} else {
// Fallback to page reload if infrastructure not available
console.log('📊 Document management infrastructure not available, using page reload');
window.location.reload();
}
// Clear our own change tracking
this.control.changeTracking.headings.clear();
this.control.changeTracking.sections.clear();
this.control.changeTracking.images.clear();
this.control.changeTracking.tables.clear();
this.control.changeTracking.lastScanTime = Date.now();
// Refresh our display
this.refreshStats();
}
}
setupAutoRefresh() {
if (this.control.autoRefreshInterval) {
clearInterval(this.control.autoRefreshInterval);
}
this.control.autoRefreshInterval = setInterval(() => {
if (this.control.isExpanded) {
this.refreshStats();
}
}, 30000); // 30 seconds
}
createControl() {
return this.control.createControl();
}
}
// Export for global access
window.StatusControl = StatusControl;
// === js/controls/debug-control.js ===
/**
* Debug Control - Displays debug information and system messages
* Implements the Robustness Principle with Fail Fast mode support
*/
class DebugControl {
constructor() {
this.control = Object.create(Control);
this.control.config = {
icon: '🪲',
title: 'Debug',
className: 'debug-control',
defaultContent: 'Click to view debug information',
ariaLabel: 'Debug Control',
position: 'w'
};
// Bind methods to control
this.control.buildContent = () => {
const content = this.control.element.querySelector('.control-content');
const messages = window.MarkitectDebugSystem ?
window.MarkitectDebugSystem.getMessages() : [];
content.innerHTML = `
<div style="padding: 1rem; font-size: 0.8rem;">
<h4 style="margin-top: 0;">Debug Messages</h4>
<div style="max-height: 200px; overflow-y: auto;">
${messages.length > 0 ?
messages.slice(-10).map(msg =>
`<div style="margin-bottom: 0.5rem; padding: 0.3rem; background: #f8f9fa; border-radius: 3px;">
<strong>[${msg.category}]</strong> ${msg.component}: ${msg.message}
<div style="font-size: 0.7rem; color: #666;">${msg.displayTime}</div>
</div>`
).join('') :
'<p>No debug messages yet</p>'
}
</div>
<button onclick="if(window.MarkitectDebugSystem) window.MarkitectDebugSystem.clearMessages(); this.closest('.control-panel').querySelector('.control-content').innerHTML = '<div style=&quot;padding: 1rem; font-size: 0.8rem;&quot;><h4 style=&quot;margin-top: 0;&quot;>Debug Messages</h4><p>Messages cleared</p></div>'"
style="margin-top: 0.5rem; padding: 0.3rem 0.6rem; font-size: 0.7rem; background: #dc3545; color: white; border: none; border-radius: 3px; cursor: pointer;">
Clear Messages
</button>
</div>
`;
this.control.isExpanded = true;
};
this.control.toggle = () => {
if (this.control.isExpanded) {
this.control.element.querySelector('.control-content').style.display = 'none';
this.control.isExpanded = false;
} else {
this.control.buildContent();
this.control.element.querySelector('.control-content').style.display = 'block';
}
};
}
createControl() {
return this.control.createControl();
}
}
window.DebugControl = DebugControl;
// === js/controls/edit-control.js ===
/**
* Edit Control - Document editing tools and actions
* Implements the Robustness Principle with Fail Fast mode support
*/
class EditControl {
constructor() {
this.control = Object.create(Control);
this.control.config = {
icon: '✏️',
title: 'Edit',
className: 'edit-control',
defaultContent: 'Document editing tools',
ariaLabel: 'Edit Control',
position: 'e'
};
// Bind methods to control
this.control.buildContent = () => {
const content = this.control.element.querySelector('.control-content');
content.innerHTML = `
<div style="padding: 1rem; font-size: 0.8rem;">
<h4 style="margin-top: 0;">Edit Tools</h4>
<div style="margin-bottom: 1rem;">
<button onclick="window.print()"
style="width: 100%; padding: 0.5rem; margin-bottom: 0.5rem; background: #28a745; color: white; border: none; border-radius: 3px; cursor: pointer; font-size: 0.8rem;">
🖨️ Print Document
</button>
<button onclick="navigator.clipboard?.writeText(window.location.href) || prompt('Copy this URL:', window.location.href)"
style="width: 100%; padding: 0.5rem; margin-bottom: 0.5rem; background: #17a2b8; color: white; border: none; border-radius: 3px; cursor: pointer; font-size: 0.8rem;">
📋 Copy Link
</button>
<button onclick="window.scrollTo({top: 0, behavior: 'smooth'})"
style="width: 100%; padding: 0.5rem; margin-bottom: 0.5rem; background: #6c757d; color: white; border: none; border-radius: 3px; cursor: pointer; font-size: 0.8rem;">
⬆️ Scroll to Top
</button>
</div>
<div style="margin-top: 1rem; font-size: 0.7rem; color: #666; border-top: 1px solid #dee2e6; padding-top: 0.5rem;">
<strong>Page Info:</strong><br>
Title: ${document.title}<br>
Words: ~${(document.body.textContent || '').split(/\\s+/).filter(w => w.length > 0).length}<br>
Modified: ${document.lastModified}
</div>
</div>
`;
this.control.isExpanded = true;
};
this.control.toggle = () => {
if (this.control.isExpanded) {
this.control.element.querySelector('.control-content').style.display = 'none';
this.control.isExpanded = false;
} else {
this.control.buildContent();
this.control.element.querySelector('.control-content').style.display = 'block';
}
};
}
createControl() {
return this.control.createControl();
}
}
window.EditControl = EditControl;
// === Component Initialization ===
document.addEventListener('DOMContentLoaded', function() {
// Create container for the markdown content
const container = document.getElementById('markdown-content') || document.body;
// Initialize components
const sectionManager = new SectionManager();
const domRenderer = new DOMRenderer(sectionManager, container);
const debugPanel = new DebugPanel();
const documentControls = new DocumentControls();
// Create document controls
documentControls.create();
// Step 4: Initialize modern Control-based architecture with compass positioning
console.log("🎛️ Initializing modern Control system with compass positioning...");
// ContentsControl (positioned upper left - nw)
const contentsControl = new ContentsControl();
contentsControl.control.config.position = 'nw'; // Upper left
contentsControl.createControl();
window.contentsControl = contentsControl;
// StatusControl (positioned right - e)
const statusControl = new StatusControl();
statusControl.control.config.position = 'e'; // Right
statusControl.createControl();
window.statusControl = statusControl;
// DebugControl (positioned lower right - se)
const debugControl = new DebugControl();
debugControl.control.config.position = 'se'; // Lower right
debugControl.createControl();
window.debugControl = debugControl;
// EditControl (positioned upper right - ne)
const editControl = new EditControl();
editControl.control.config.position = 'ne'; // Upper right
editControl.createControl();
window.editControl = editControl;
console.log("🎛️ Modern Control system initialized with compass positioning");
// Wire up event handlers
documentControls.setEventHandlers({
'save-document': () => {
console.log('Save document clicked');
try {
// Get current markdown content from section manager
const currentMarkdown = sectionManager.getDocumentMarkdown();
// Create filename with timestamp suffix following the established convention
const now = new Date();
const timestamp = now.toISOString().slice(0, 19).replace(/:/g, '-').replace('T', '-');
// Extract original filename from config or use default
const originalFilename = window.editorConfig?.originalFilename || 'document';
const editedFilename = `${originalFilename}-edited-${timestamp}.md`;
// Create and download the file
const blob = new Blob([currentMarkdown], { type: 'text/markdown' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = editedFilename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
// Log success to debug panel
debugPanel.addMessage(`Document saved as: ${editedFilename}`, 'SUCCESS');
console.log(`Document successfully saved as: ${editedFilename}`);
} catch (error) {
debugPanel.addMessage(`Save failed: ${error.message}`, 'ERROR');
console.error('Save error:', error);
}
},
'reset-all': () => {
console.log('Reset all clicked');
// Hide any open editors
domRenderer.hideCurrentEditor();
// Reset all sections to original state
const allSections = Array.from(sectionManager.sections.values());
allSections.forEach(section => {
section.resetToOriginal();
});
// Re-render all sections
domRenderer.renderAllSections(allSections);
debugPanel.addMessage(`Reset all sections to original state`, 'INFO');
},
'show-status': () => {
const status = sectionManager.getDocumentStatus();
alert(`Document Status:\nTotal Sections: ${status.totalSections}\nEditing Sections: ${status.editingSections}`);
},
'toggle-debug': () => {
debugPanel.toggle();
}
});
// Set up debug panel integration
sectionManager.on('sections-created', (data) => {
debugPanel.addMessage(`Created ${data.count} sections`, 'INFO');
});
sectionManager.on('edit-started', (data) => {
debugPanel.addMessage(`Edit started for section: ${data.sectionId}`, 'DEBUG');
});
sectionManager.on('changes-accepted', (data) => {
debugPanel.addMessage(`Changes accepted for section: ${data.sectionId}`, 'SUCCESS');
// Re-render the section to show updated content
const section = sectionManager.sections.get(data.sectionId);
if (section) {
const sectionElement = domRenderer.findSectionElement(data.sectionId);
if (sectionElement) {
const newElement = domRenderer.renderSection(section);
sectionElement.parentNode.replaceChild(newElement, sectionElement);
debugPanel.addMessage(`DOM updated for section: ${data.sectionId}`, 'INFO');
}
}
});
sectionManager.on('changes-cancelled', (data) => {
debugPanel.addMessage(`Changes cancelled for section: ${data.sectionId}`, 'WARNING');
});
// Initialize with markdown content
const markdownToRender = markdownContent || '';
if (markdownToRender.trim()) {
const sections = sectionManager.createSectionsFromMarkdown(markdownToRender);
domRenderer.renderAllSections(sections);
debugPanel.addMessage(`Initialized with ${sections.length} sections`, 'INFO');
} else {
debugPanel.addMessage('No markdown content to initialize', 'WARNING');
}
// Make components globally available for debugging
window.markitectComponents = {
sectionManager,
domRenderer,
debugPanel,
documentControls
};
console.log('Markitect modular editor initialized successfully');
});
// Always render content first (graceful degradation)
document.addEventListener('DOMContentLoaded', function() {
console.log("🎯 Rendering content in edit mode...");
// Initialize edit/insert capabilities first (always needed)
if ((typeof MARKITECT_EDIT_MODE !== 'undefined' && MARKITECT_EDIT_MODE) ||
(typeof MARKITECT_INSERT_MODE !== 'undefined' && MARKITECT_INSERT_MODE)) {
const mode = (typeof MARKITECT_INSERT_MODE !== 'undefined' && MARKITECT_INSERT_MODE) ? 'insert' : 'edit';
console.log(`🚀 Initializing clean ${mode} capabilities...`);
try {
console.log("Creating clean editor instance...");
initializeCleanEditor();
if (mode === 'insert') {
console.log("✅ Clean insert mode active - click any section to edit (headings 1-3 protected)");
} else {
console.log("✅ Clean edit mode active - click any section to edit");
}
} catch (error) {
console.error(`❌ Clean ${mode} mode failed to initialize:`, error);
}
}
// Check if modular components are being used for content rendering
if (typeof SectionManager !== 'undefined') {
console.log("✓ Modular components detected - skipping fallback content rendering");
console.log("✓ Content will be rendered by modular architecture");
return;
}
const contentDiv = document.getElementById('markdown-content');
// Step 1: Ensure content is always displayed (fallback for non-modular mode)
if (contentDiv) {
if (typeof marked !== 'undefined') {
try {
const html = marked.parse(markdownContentWithDogtag);
// Add target="_blank" to all links
const htmlWithTargetBlank = html.replace(/<a href="([^"]*)"([^>]*)>/g, '<a href="$1" target="_blank"$2>');
contentDiv.innerHTML = htmlWithTargetBlank;
console.log("✓ Content rendered successfully");
} catch (error) {
contentDiv.innerHTML = '<p>Error rendering markdown: ' + error.message + '</p>';
console.error("Content rendered with errors");
console.error("Markdown parsing failed:", error.message);
}
} else {
// Fallback: display raw markdown with basic formatting
const fallbackHtml = markdownContent
.replace(/^# (.*$)/gim, '<h1>$1</h1>')
.replace(/^## (.*$)/gim, '<h2>$1</h2>')
.replace(/^### (.*$)/gim, '<h3>$1</h3>')
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
.replace(/\*(.*?)\*/g, '<em>$1</em>')
.replace(/^- (.*$)/gim, '<li>$1</li>')
.replace(/\n\n/g, '<br><br>')
.replace(/\n/g, '<br>');
contentDiv.innerHTML = '<div style="white-space: pre-wrap;">' + fallbackHtml + '</div>';
console.warn("Content rendered with fallback parser");
console.warn("CDN library failed to load - using basic fallback rendering");
}
}
// Step 3: Initialize scroll indicators
try {
initializeScrollIndicators();
} catch (error) {
console.error("Scroll indicators failed to initialize:", error);
}
});
window.addEventListener('load', function() {
if (window.markitectMarkedError) {
console.error("CDN library failed to load - network or firewall blocking marked.js");
}
});
</script>
</body>
</html>