Files
testdrive-jsui/docs/prototypes/DebugControlContent.html
tegwick 1fe4b6b9fa refactor: complete post-migration cleanup
Implemented all cleanup items from CLEANUP_REPORT.md:

Legacy Code Removal:
- Removed document-controls-legacy.js wrapper
- Updated 4 test files to use DocumentControls directly
- Updated scripts/list_components.py acronym mappings
- Updated tests/test_component_listing.py expectations

Archive and Organization:
- Moved relicts/ to docs/prototypes/ with README explaining history
- Moved MIGRATION_STATUS.md to docs/migration/
- Removed IMPLEMENTATION_NOTES.md legacy references

Test Verification:
- All 68 JavaScript tests passing (Jest)
- All 3 Python component tests passing
- No breaking changes to functionality

The codebase is now cleaner with no legacy wrappers or empty
directories. Migration is complete and documented.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-16 11:43:42 +01:00

4092 lines
157 KiB
HTML
Executable File
Raw Permalink 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>Test Document</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 = "# Test Document\n\nIntroduction section with some content.\n\n## Features\n\nThis section describes various features.\n\n### Feature A\n\nDetails about the first feature.\n\n### Feature B\n\nInformation about the second feature.\n\n## Conclusion\n\nFinal summary section.\n";
const markdownContentWithDogtag = "# Test Document\n\nIntroduction section with some content.\n\n## Features\n\nThis section describes various features.\n\n### Feature A\n\nDetails about the first feature.\n\n### Feature B\n\nInformation about the second feature.\n\n## Conclusion\n\nFinal summary section.\n\n\n---\n*-- html from markdown by <a href=\"https://coulomb.social/open/MarkiTect\" target=\"_blank\">MarkiTect</a> on 2025-11-11 00:01:28 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-11 00:01:28 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: 'tmp1x9ds_fj',
version: 'Markitect v0.8.1.dev23+g3839a6761.d20251110',
repoName: 'Markitect'
};
// Make config available globally
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;
}
// === 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();
// Define abstract Control class for UI controls (same as viewing mode)
const Control = {
// Abstract control properties
element: null,
isExpanded: false,
isHeaderOnly: false, // New state for header-only mode
isDragging: false,
isResizing: false, // New state for resizing mode
dragOffset: { x: 0, y: 0 },
resizeStartSize: { width: 280, height: 'auto' },
originalPosition: { top: '80px', left: '20px' },
defaultSize: { width: 280, minWidth: 200, minHeight: 150 },
// Configuration properties (to be overridden by subclasses)
config: {
icon: '?',
title: 'Control',
className: 'control',
defaultContent: 'Template only',
ariaLabel: 'Control',
position: 'w' // Default compass position: west (middle-left)
},
// Compass positioning system (top-aligned for proper expansion)
compassPositions: {
// North positions (top)
'n': { top: '20px', left: '50%', transform: 'translateX(-50%)' },
'nne': { top: '20px', left: '65%', transform: 'translateX(-50%)' },
'ne': { top: '20px', right: '20px' },
'ene': { top: '80px', right: '20px' }, // Top-aligned instead of center
// East positions (right)
'e': { top: '50vh', right: '20px', transform: 'translateY(-20px)' }, // Anchor at icon level
'ese': { top: 'calc(65vh - 20px)', right: '20px' }, // Top-aligned
'se': { bottom: '20px', right: '20px' },
'sse': { bottom: '20px', right: '35%', transform: 'translateX(50%)' },
// South positions (bottom)
's': { bottom: '20px', left: '50%', transform: 'translateX(-50%)' },
'ssw': { bottom: '20px', left: '35%', transform: 'translateX(-50%)' },
'sw': { bottom: '20px', left: '20px' },
'wsw': { bottom: '80px', left: '20px' }, // Top-aligned instead of center
// West positions (left) - top-aligned for proper expansion
'w': { top: '50vh', left: '20px', transform: 'translateY(-20px)' }, // Anchor at icon level
'wnw': { top: '80px', left: '20px' }, // Top-aligned instead of center
'nw': { top: '20px', left: '20px' },
'nnw': { top: '20px', left: '35%', transform: 'translateX(-50%)' }
},
// Get expansion direction based on compass position
getExpansionDirection: function() {
const pos = this.config.position;
const rightBorderPositions = ['ne', 'ene', 'e', 'ese', 'se'];
const bottomBorderPositions = ['sw', 'ssw', 's', 'sse', 'se'];
return {
header: rightBorderPositions.includes(pos) ? 'left' : 'right',
body: bottomBorderPositions.includes(pos) ? 'up' : 'down'
};
},
// Calculate position styles based on compass direction
getPositionStyles: function() {
const compassPos = this.compassPositions[this.config.position] || this.compassPositions['w'];
return {
position: 'fixed',
top: compassPos.top || 'auto',
right: compassPos.right || 'auto',
bottom: compassPos.bottom || 'auto',
left: compassPos.left || 'auto',
transform: compassPos.transform || 'none',
zIndex: 1001
};
},
// Abstract methods (to be implemented by subclasses)
buildContent: function() {
const content = this.element.querySelector('.control-content');
content.innerHTML = `<p style="padding: 1rem; color: #666;">${this.config.defaultContent}</p>`;
},
// Concrete methods (shared by all controls)
createControl: function() {
console.log(`🎛️ Creating ${this.config.title} control...`);
this.element = document.createElement('div');
this.element.className = this.config.className;
this.element.innerHTML = `
<button class="control-toggle" aria-label="${this.config.ariaLabel}">${this.config.icon}</button>
<div class="control-panel" style="display: none;">
<div class="control-header">
<span class="control-icon">${this.config.icon}</span>
<h3>${this.config.title}</h3>
<button class="control-close">✕</button>
</div>
<div class="control-content">Loading...</div>
</div>
`;
// Position using compass direction
const positionStyles = this.getPositionStyles();
this.element.style.cssText = `
position: ${positionStyles.position};
top: ${positionStyles.top};
right: ${positionStyles.right};
bottom: ${positionStyles.bottom};
left: ${positionStyles.left};
transform: ${positionStyles.transform};
z-index: ${positionStyles.zIndex};
background: rgba(255, 255, 255, 0.95);
border: 1px solid #e1e5e9;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
backdrop-filter: blur(8px);
width: 40px;
transition: all 0.3s ease;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
`;
// Store original position for reset
this.originalPosition = {
top: positionStyles.top,
right: positionStyles.right,
bottom: positionStyles.bottom,
left: positionStyles.left,
transform: positionStyles.transform
};
// Style toggle button
const toggleBtn = this.element.querySelector('.control-toggle');
toggleBtn.style.cssText = `
width: 100%;
height: 40px;
border: none;
background: transparent;
cursor: pointer;
font-size: 16px;
color: #666;
transition: color 0.2s ease;
`;
// Handle click to build content on-demand
toggleBtn.addEventListener('click', () => {
if (this.isExpanded) {
this.collapse();
} else {
console.log(`🎛️ ${this.config.title} toggle clicked - building content...`);
this.buildContent();
}
});
// Close button handler
const closeBtn = this.element.querySelector('.control-close');
closeBtn.addEventListener('click', () => {
this.collapse();
});
document.body.appendChild(this.element);
console.log(`🎛️ ${this.config.title} control created`);
},
styleHeader: function() {
const header = this.element.querySelector('.control-header');
// Style the header to show icon, title, and close button in one line
// Match the height of the collapsed icon state (40px)
header.style.cssText = `
display: flex;
align-items: center;
justify-content: space-between;
height: 40px;
padding: 0 1rem;
border-bottom: 1px solid #eee;
margin-bottom: 0;
`;
const icon = header.querySelector('.control-icon');
if (icon) {
icon.style.cssText = `
font-size: 16px;
color: #666;
margin-right: 0.5rem;
cursor: grab;
user-select: none;
`;
// Make icon draggable
this.setupDragHandlers(icon);
}
const title = header.querySelector('h3');
if (title) {
title.style.cssText = `
margin: 0;
font-size: 0.9rem;
font-weight: 600;
flex-grow: 1;
line-height: 1;
cursor: pointer;
user-select: none;
`;
// Add click handler to toggle header-only mode
title.addEventListener('click', () => {
this.toggleHeaderOnly();
});
}
const closeBtn = header.querySelector('.control-close');
if (closeBtn) {
closeBtn.style.cssText = `
background: none;
border: none;
font-size: 14px;
cursor: pointer;
color: #666;
padding: 0;
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
`;
}
},
styleContent: function() {
const content = this.element.querySelector('.control-content');
const expansion = this.getExpansionDirection();
// Style the content area based on expansion direction
let contentStyles = `
padding: 0.5rem;
overflow-y: auto;
`;
if (expansion.body === 'up') {
// Body expands upward (for bottom border positions)
contentStyles += `
max-height: calc(80vh - 40px);
`;
content.parentElement.style.flexDirection = 'column-reverse';
} else {
// Body expands downward (default)
contentStyles += `
max-height: calc(80vh - 40px);
`;
content.parentElement.style.flexDirection = 'column';
}
content.style.cssText = contentStyles;
},
expand: function() {
this.isExpanded = true;
const panel = this.element.querySelector('.control-panel');
const toggleBtn = this.element.querySelector('.control-toggle');
// Get expansion direction based on compass position
const expansion = this.getExpansionDirection();
// Apply expansion styling based on direction
if (expansion.header === 'left') {
// Header expands to the left (for right border positions)
this.element.style.width = '300px';
this.element.style.transformOrigin = 'top right';
} else {
// Header expands to the right (default)
this.element.style.width = '300px';
this.element.style.transformOrigin = 'top left';
}
panel.style.display = 'block';
toggleBtn.style.display = 'none';
this.styleHeader();
this.styleContent();
this.addResizeHandle();
},
collapse: function() {
this.isExpanded = false;
this.isHeaderOnly = false; // Reset header-only state
const panel = this.element.querySelector('.control-panel');
const toggleBtn = this.element.querySelector('.control-toggle');
panel.style.display = 'none';
// Reset size to default
this.element.style.width = '40px';
this.element.style.height = 'auto';
// Remove resize handle
this.removeResizeHandle();
toggleBtn.style.display = 'block';
// Reset position to original compass location
this.element.style.top = this.originalPosition.top;
this.element.style.right = this.originalPosition.right;
this.element.style.bottom = this.originalPosition.bottom;
this.element.style.left = this.originalPosition.left;
this.element.style.transform = this.originalPosition.transform;
},
toggleHeaderOnly: function() {
if (!this.isExpanded) {
// If collapsed, first expand normally
this.buildContent();
return;
}
const content = this.element.querySelector('.control-content');
if (this.isHeaderOnly) {
// Show content area (go to full expanded mode)
this.isHeaderOnly = false;
content.style.display = 'block';
console.log(`🎛️ ${this.config.title} expanded to full view`);
} else {
// Hide content area (go to header-only mode)
this.isHeaderOnly = true;
content.style.display = 'none';
console.log(`🎛️ ${this.config.title} collapsed to header only`);
}
},
setupDragHandlers: function(dragElement) {
dragElement.addEventListener('mousedown', (e) => {
this.isDragging = true;
const rect = this.element.getBoundingClientRect();
const iconRect = dragElement.getBoundingClientRect();
// Calculate offset relative to the icon position, not the element
this.dragOffset.x = e.clientX - rect.left;
this.dragOffset.y = iconRect.top - rect.top + (iconRect.height / 2); // Keep mouse at icon center
dragElement.style.cursor = 'grabbing';
e.preventDefault();
});
document.addEventListener('mousemove', (e) => {
if (!this.isDragging || !this.isExpanded) return;
const newX = e.clientX - this.dragOffset.x;
const newY = e.clientY - this.dragOffset.y;
// Keep within viewport bounds
const maxX = window.innerWidth - this.element.offsetWidth;
const maxY = window.innerHeight - this.element.offsetHeight;
const boundedX = Math.max(0, Math.min(newX, maxX));
const boundedY = Math.max(0, Math.min(newY, maxY));
this.element.style.left = boundedX + 'px';
this.element.style.top = boundedY + 'px';
});
document.addEventListener('mouseup', () => {
if (this.isDragging) {
this.isDragging = false;
dragElement.style.cursor = 'grab';
}
});
},
// Add resize handle to expanded control (same as viewing mode)
addResizeHandle: function() {
// Remove existing resize handle if any
this.removeResizeHandle();
const resizeHandle = document.createElement('div');
resizeHandle.className = 'control-resize-handle';
resizeHandle.innerHTML = '↘';
resizeHandle.style.cssText = `
position: absolute;
bottom: 0;
right: 0;
width: 20px;
height: 20px;
background: rgba(108, 117, 125, 0.8);
color: white;
cursor: nw-resize;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
line-height: 1;
border-top-left-radius: 4px;
user-select: none;
z-index: 1002;
`;
this.element.appendChild(resizeHandle);
this.setupResizeHandlers(resizeHandle);
},
// Remove resize handle
removeResizeHandle: function() {
const existingHandle = this.element.querySelector('.control-resize-handle');
if (existingHandle) {
existingHandle.remove();
}
},
// Set up resize event handlers
setupResizeHandlers: function(resizeHandle) {
resizeHandle.addEventListener('mousedown', (e) => {
this.isResizing = true;
const rect = this.element.getBoundingClientRect();
this.resizeStartSize = {
width: rect.width,
height: rect.height,
startX: e.clientX,
startY: e.clientY
};
resizeHandle.style.cursor = 'nw-resize';
resizeHandle.style.background = 'rgba(40, 167, 69, 0.9)';
e.preventDefault();
e.stopPropagation(); // Prevent triggering drag
});
document.addEventListener('mousemove', (e) => {
if (!this.isResizing || !this.isExpanded) return;
const deltaX = e.clientX - this.resizeStartSize.startX;
const deltaY = e.clientY - this.resizeStartSize.startY;
const newWidth = Math.max(this.defaultSize.minWidth, this.resizeStartSize.width + deltaX);
const newHeight = Math.max(this.defaultSize.minHeight, this.resizeStartSize.height + deltaY);
// Check viewport bounds
const maxWidth = window.innerWidth - this.element.offsetLeft;
const maxHeight = window.innerHeight - this.element.offsetTop;
const boundedWidth = Math.min(newWidth, maxWidth - 20);
const boundedHeight = Math.min(newHeight, maxHeight - 20);
this.element.style.width = boundedWidth + 'px';
this.element.style.height = boundedHeight + 'px';
// Ensure content areas resize properly
this.updateContentSize();
});
document.addEventListener('mouseup', () => {
if (this.isResizing) {
this.isResizing = false;
resizeHandle.style.cursor = 'nw-resize';
resizeHandle.style.background = 'rgba(108, 117, 125, 0.8)';
}
});
},
// Update content area sizes during resize
updateContentSize: function() {
const content = this.element.querySelector('.control-content');
if (content) {
// Adjust content height to fit the resized control
const headerHeight = 40; // Header is 40px
const padding = 16; // Account for padding
const controlHeight = this.element.offsetHeight;
const availableHeight = controlHeight - headerHeight - padding;
content.style.maxHeight = Math.max(100, availableHeight) + 'px';
}
}
};
// Create ContentsControl for edit mode (new implementation based on Control class)
try {
const contentsControl = Object.create(Control);
// Configure for contents navigation in edit mode
contentsControl.config = {
icon: '☰',
title: 'Contents',
className: 'contents-control edit-mode',
defaultContent: 'No headings found',
ariaLabel: 'Document Navigation',
position: 'wnw' // West-north-west positioning
};
// Override buildContent method for navigation functionality
contentsControl.buildContent = function() {
const content = this.element.querySelector('.control-content');
// Build navigation content from current DOM
const allHeadings = document.querySelectorAll('h1, h2, h3');
// Filter out headings that contain "Contents" or similar navigation-related text
const headings = Array.from(allHeadings).filter(heading => {
const text = heading.textContent.trim().toLowerCase();
return !text.includes('contents') && !text.includes('table of contents') && !text.includes('navigation');
});
console.log("📋 Found headings for navigation:", headings.length);
if (headings.length === 0) {
content.innerHTML = '<p style="padding: 1rem; color: #666;">No headings found</p>';
} else {
let navHtml = '';
headings.forEach((heading, index) => {
if (!heading.id) {
heading.id = `heading-${index + 1}`;
}
const level = parseInt(heading.tagName.substring(1));
const indent = (level - 1) * 1;
navHtml += `
<a href="#${heading.id}"
style="display: block; padding: 0.5rem; margin-left: ${indent}rem;
text-decoration: none; color: #333; font-size: 0.9rem;
border-radius: 4px; cursor: pointer;"
onmouseover="this.style.backgroundColor='#f5f5f5'"
onmouseout="this.style.backgroundColor=''"
onclick="event.preventDefault(); document.getElementById('${heading.id}').scrollIntoView({behavior: 'smooth'}); if (window.innerWidth <= 768) setTimeout(() => contentsControl.collapse(), 500);">
${heading.textContent.trim()}
</a>
`;
});
content.innerHTML = navHtml;
}
// Show panel
this.expand();
};
// Initialize the ContentsControl
contentsControl.createControl();
// Make globally available for mobile collapse
window.contentsControl = contentsControl;
} catch (error) {
console.error("ContentsControl failed to initialize:", error);
}
// Step 7: Initialize Independent Debug System
try {
// Create independent debug system using IndexedDB for persistence
window.MarkitectDebugSystem = {
db: null,
messages: [],
maxMessages: 1000,
isEnabled: true,
subscribers: [],
// Selection and filtering system
selectionCriteria: {
includeDocumentEvents: true,
includeSystemEvents: false,
includeControlEvents: true,
includeEditingEvents: true,
includeNavigationEvents: false,
includedHeadings: new Set(), // Track which document headings to monitor
excludedSources: new Set(['ContentsControl', 'DocumentNavigator'])
},
// Initialize IndexedDB for persistence
async init() {
return new Promise((resolve, reject) => {
const request = indexedDB.open('MarkitectDebugDB', 1);
request.onerror = () => reject(request.error);
request.onsuccess = () => {
this.db = request.result;
this.loadMessages().then(resolve);
};
request.onupgradeneeded = (e) => {
const db = e.target.result;
if (!db.objectStoreNames.contains('messages')) {
const store = db.createObjectStore('messages', { keyPath: 'id', autoIncrement: true });
store.createIndex('timestamp', 'timestamp', { unique: false });
store.createIndex('category', 'category', { unique: false });
}
};
});
},
// Add a debug message with selection filtering
async addMessage(message, category = 'INFO', source = 'System', context = {}) {
// Check if this message should be included based on selection criteria
if (!this.shouldIncludeMessage(message, category, source, context)) {
return null;
}
const messageObj = {
message: String(message),
category: category.toUpperCase(),
source: source,
context: context,
timestamp: new Date().toISOString(),
displayTime: new Date().toLocaleTimeString()
};
// Add to memory
this.messages.push(messageObj);
// Keep only last maxMessages in memory
if (this.messages.length > this.maxMessages) {
this.messages = this.messages.slice(-this.maxMessages);
}
// Persist to IndexedDB
if (this.db) {
try {
const transaction = this.db.transaction(['messages'], 'readwrite');
const store = transaction.objectStore('messages');
await store.add(messageObj);
} catch (e) {
console.warn('Failed to persist debug message:', e);
}
}
// Notify subscribers
this.subscribers.forEach(callback => {
try { callback(messageObj); } catch (e) { console.error('Debug subscriber error:', e); }
});
return messageObj;
},
// Selection filtering logic
shouldIncludeMessage(message, category, source, context) {
if (!this.isEnabled) return false;
// Check excluded sources
if (this.selectionCriteria.excludedSources.has(source)) {
return false;
}
// Category-based filtering
const categoryChecks = {
'DOCUMENT': () => this.selectionCriteria.includeDocumentEvents,
'SYSTEM': () => this.selectionCriteria.includeSystemEvents,
'CONTROL': () => this.selectionCriteria.includeControlEvents,
'EDITING': () => this.selectionCriteria.includeEditingEvents,
'NAVIGATION': () => this.selectionCriteria.includeNavigationEvents
};
// Check if we have a specific category filter
if (context.eventType && categoryChecks[context.eventType]) {
return categoryChecks[context.eventType]();
}
// Document heading specific filtering
if (context.headingId) {
return this.selectionCriteria.includedHeadings.has(context.headingId) ||
this.selectionCriteria.includedHeadings.size === 0; // Include all if none specifically selected
}
// Default: include based on general settings
return category === 'ERROR' || // Always include errors
this.selectionCriteria.includeSystemEvents;
},
// Document structure awareness
scanDocumentStructure() {
const headings = document.querySelectorAll('h1, h2, h3, h4, h5, h6');
const documentStructure = {
headings: [],
totalSections: 0
};
headings.forEach((heading, index) => {
// Skip control headings (like Contents, Debug, etc.)
const text = heading.textContent.trim().toLowerCase();
if (text.includes('contents') || text.includes('debug') ||
text.includes('control') || text.includes('navigation')) {
return;
}
if (!heading.id) {
heading.id = `document-heading-${index + 1}`;
}
documentStructure.headings.push({
id: heading.id,
text: heading.textContent.trim(),
level: parseInt(heading.tagName.substring(1)),
element: heading
});
});
documentStructure.totalSections = documentStructure.headings.length;
this.documentStructure = documentStructure;
// Auto-include all document headings for monitoring
this.selectionCriteria.includedHeadings.clear();
documentStructure.headings.forEach(heading => {
this.selectionCriteria.includedHeadings.add(heading.id);
});
this.addMessage(`Document structure scanned: ${documentStructure.totalSections} sections found`,
'INFO', 'DocumentScanner', { eventType: 'DOCUMENT', headings: documentStructure.headings });
return documentStructure;
},
// Selection management methods
includeHeading(headingId) {
this.selectionCriteria.includedHeadings.add(headingId);
},
excludeHeading(headingId) {
this.selectionCriteria.includedHeadings.delete(headingId);
},
includeSource(source) {
this.selectionCriteria.excludedSources.delete(source);
},
excludeSource(source) {
this.selectionCriteria.excludedSources.add(source);
},
// Get selection criteria for UI
getSelectionCriteria() {
return {
...this.selectionCriteria,
includedHeadings: Array.from(this.selectionCriteria.includedHeadings),
excludedSources: Array.from(this.selectionCriteria.excludedSources)
};
},
// Load messages from IndexedDB
async loadMessages() {
if (!this.db) return;
try {
const transaction = this.db.transaction(['messages'], 'readonly');
const store = transaction.objectStore('messages');
const request = store.getAll();
request.onsuccess = () => {
this.messages = request.result.slice(-this.maxMessages);
console.log(`📊 Loaded ${this.messages.length} debug messages from IndexedDB`);
};
} catch (e) {
console.warn('Failed to load debug messages:', e);
}
},
// Clear all messages
async clearMessages() {
this.messages = [];
if (this.db) {
try {
const transaction = this.db.transaction(['messages'], 'readwrite');
const store = transaction.objectStore('messages');
await store.clear();
} catch (e) {
console.warn('Failed to clear debug messages from DB:', e);
}
}
this.subscribers.forEach(callback => {
try { callback({ type: 'clear' }); } catch (e) { console.error('Debug subscriber error:', e); }
});
},
// Subscribe to debug updates
subscribe(callback) {
this.subscribers.push(callback);
return () => {
const index = this.subscribers.indexOf(callback);
if (index > -1) this.subscribers.splice(index, 1);
};
},
// Get messages (with filtering)
getMessages(category = null, limit = null) {
let filtered = this.messages;
if (category) {
filtered = filtered.filter(msg => msg.category === category.toUpperCase());
}
if (limit) {
filtered = filtered.slice(-limit);
}
return filtered;
},
// Get recent messages
getRecentMessages(count = 50) {
return this.messages.slice(-count);
},
// Export messages as JSON
exportMessages() {
return JSON.stringify(this.messages, null, 2);
}
};
// Initialize the debug system
window.MarkitectDebugSystem.init().then(() => {
console.log('📊 Markitect Debug System initialized');
// Add initial message
window.MarkitectDebugSystem.addMessage('Markitect Debug System initialized', 'INFO', 'DebugSystem', {eventType: 'SYSTEM'});
}).catch(error => {
console.warn('📊 Debug System initialization failed, using memory only:', error);
window.MarkitectDebugSystem.addMessage('Debug System initialized (memory only)', 'WARNING', 'DebugSystem', {eventType: 'SYSTEM'});
});
} catch (error) {
console.error("Debug System initialization failed:", error);
}
// Step 8: Initialize DebugControl (new implementation based on Control class)
try {
const debugControl = Object.create(Control);
// Configure for debug functionality
debugControl.config = {
icon: '🪲',
title: 'Debug',
className: 'debug-control',
defaultContent: 'Debug panel controls',
ariaLabel: 'Debug Control',
position: 'ese' // East-south-east positioning
};
// Override buildContent method for debug functionality
debugControl.buildContent = function() {
console.log("🪲 Building debug control content...");
try {
const content = this.element.querySelector('.control-content');
if (!content) {
console.error("🪲 Debug control content element not found");
return;
}
// Build debug control panel with selection and filtering
content.innerHTML = `
<div style="padding: 0.5rem;">
<!-- Debug Control Header -->
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.5rem;
padding-bottom: 0.5rem; border-bottom: 1px solid #eee;">
<h4 style="margin: 0; font-size: 0.9rem; font-weight: 600;">Debug Messages</h4>
<div>
<button onclick="window.MarkitectDebugSystem?.scanDocumentStructure()"
style="padding: 0.25rem 0.5rem; background: #28a745; color: white;
border: none; border-radius: 3px; cursor: pointer; font-size: 0.7rem; margin-right: 0.25rem;">
📊 Scan
</button>
<button onclick="window.MarkitectDebugSystem?.clearMessages()"
style="padding: 0.25rem 0.5rem; background: #dc3545; color: white;
border: none; border-radius: 3px; cursor: pointer; font-size: 0.7rem;">
Clear
</button>
</div>
</div>
<!-- Selection Controls -->
<div style="margin-bottom: 0.5rem; padding: 0.5rem; background: #f8f9fa; border-radius: 4px; font-size: 0.8rem;">
<div style="font-weight: bold; margin-bottom: 0.25rem;">Event Types:</div>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 0.25rem;">
<label style="display: flex; align-items: center; cursor: pointer;">
<input type="checkbox" id="docEvents" checked onchange="window.MarkitectDebugSystem.selectionCriteria.includeDocumentEvents = this.checked" style="margin-right: 0.25rem;">
<span style="font-size: 0.75rem;">Document</span>
</label>
<label style="display: flex; align-items: center; cursor: pointer;">
<input type="checkbox" id="editEvents" checked onchange="window.MarkitectDebugSystem.selectionCriteria.includeEditingEvents = this.checked" style="margin-right: 0.25rem;">
<span style="font-size: 0.75rem;">Editing</span>
</label>
<label style="display: flex; align-items: center; cursor: pointer;">
<input type="checkbox" id="ctrlEvents" checked onchange="window.MarkitectDebugSystem.selectionCriteria.includeControlEvents = this.checked" style="margin-right: 0.25rem;">
<span style="font-size: 0.75rem;">Controls</span>
</label>
<label style="display: flex; align-items: center; cursor: pointer;">
<input type="checkbox" id="sysEvents" onchange="window.MarkitectDebugSystem.selectionCriteria.includeSystemEvents = this.checked" style="margin-right: 0.25rem;">
<span style="font-size: 0.75rem;">System</span>
</label>
</div>
</div>
<!-- Debug Switch -->
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.5rem;
padding: 0.5rem; background: #e9ecef; border-radius: 4px;">
<span style="font-size: 0.9rem; font-weight: bold;">Debug System:</span>
<label style="display: flex; align-items: center; cursor: pointer;">
<input type="checkbox" id="debugToggle" checked
onchange="window.MarkitectDebugSystem.isEnabled = this.checked; document.getElementById('debugStatus').textContent = this.checked ? 'ON' : 'OFF'; document.getElementById('debugStatus').style.color = this.checked ? '#28a745' : '#dc3545';"
style="margin-right: 0.5rem;">
<span id="debugStatus" style="color: #28a745; font-weight: bold;">ON</span>
</label>
</div>
<!-- Debug Messages Container -->
<div id="debugMessagesContainer" style="max-height: 180px; overflow-y: auto;
border: 1px solid #ddd; border-radius: 4px;
background: #f8f9fa; font-family: monospace; font-size: 0.8rem;">
<div style="padding: 0.5rem; color: #666; text-align: center;">
Click 📊 Scan to analyze document structure
</div>
</div>
<!-- Export and Tools -->
<div style="margin-top: 0.5rem; display: flex; gap: 0.25rem;">
<button onclick="navigator.clipboard.writeText(window.MarkitectDebugSystem.exportMessages()).then(() => window.MarkitectDebugSystem.addMessage('Debug messages exported to clipboard', 'SUCCESS', 'DebugControl'))"
style="flex: 1; padding: 0.4rem; background: #17a2b8; color: white;
border: none; border-radius: 3px; cursor: pointer; font-size: 0.8rem;">
📋 Export
</button>
<button onclick="console.table(window.MarkitectDebugSystem.getSelectionCriteria())"
style="flex: 1; padding: 0.4rem; background: #6f42c1; color: white;
border: none; border-radius: 3px; cursor: pointer; font-size: 0.8rem;">
🔍 Criteria
</button>
</div>
</div>
`;
console.log("🪲 Debug control HTML content built successfully");
// Note: Debug switch state is set directly in HTML (checked attribute)
// No need to call updateDebugStatus() since checkbox manages its own state
// Set up periodic update of debug messages
try {
this.setupMessageUpdates();
} catch (e) {
console.log("🪲 Message updates setup failed:", e);
}
// Show panel and call expand
console.log("🪲 Calling expand...");
this.expand();
console.log("🪲 Expand called successfully");
} catch (error) {
console.error("🪲 Error in buildContent:", error);
// Fallback simple content
const content = this.element.querySelector('.control-content');
if (content) {
content.innerHTML = `<div style="padding: 1rem; color: #666;">Debug control error: ${error.message}</div>`;
this.expand();
}
}
};
// Add method to update debug switch status (updated for new debug system)
debugControl.updateDebugStatus = function() {
const checkbox = this.element.querySelector('#debugToggle');
const statusText = this.element.querySelector('#debugStatus');
if (checkbox && statusText) {
try {
if (window.MarkitectDebugSystem) {
// Use the new debug system's enabled state
const isEnabled = window.MarkitectDebugSystem.isEnabled;
checkbox.checked = isEnabled;
statusText.textContent = isEnabled ? 'ON' : 'OFF';
statusText.style.color = isEnabled ? '#28a745' : '#dc3545';
} else {
// Fallback when debug system is not available
checkbox.checked = false;
statusText.textContent = 'UNAVAILABLE';
statusText.style.color = '#dc3545';
}
} catch (e) {
console.log("🪲 Error updating debug status:", e);
checkbox.checked = false;
statusText.textContent = 'ERROR';
statusText.style.color = '#dc3545';
}
}
};
// Add method to setup message updates
debugControl.setupMessageUpdates = function() {
try {
// Update messages every 500ms when debug control is open
this.messageUpdateInterval = setInterval(() => {
if (this.isExpanded && !this.isHeaderOnly) {
this.updateMessages();
// Note: updateDebugStatus() removed - checkbox manages its own state
}
}, 500);
console.log("🪲 Message update interval set up successfully");
} catch (e) {
console.log("🪲 Failed to set up message updates:", e);
}
};
// Add method to update debug messages display using new system
debugControl.updateMessages = function() {
const container = this.element.querySelector('#debugMessagesContainer');
if (!container) return;
try {
if (window.MarkitectDebugSystem) {
const messages = window.MarkitectDebugSystem.getRecentMessages(50);
if (messages && messages.length > 0) {
// Show messages in reverse order (newest first)
const reversedMessages = [...messages].reverse();
container.innerHTML = `
<div style="padding: 0.25rem 0.5rem; background: #e9ecef; font-size: 0.7rem; color: #6c757d;">
${messages.length} messages (newest first) |
<span onclick="window.MarkitectDebugSystem.addMessage('Test message from debug control', 'INFO', 'DebugControl', {eventType: 'CONTROL'})"
style="cursor: pointer; text-decoration: underline;">Add Test</span>
</div>
${reversedMessages.map(msg => `
<div style="padding: 0.25rem 0.5rem; border-bottom: 1px solid #e9ecef;
color: ${this.getMessageColor(msg.category)};">
<div style="display: flex; justify-content: space-between; align-items: flex-start;">
<div style="flex: 1;">
<small style="color: #6c757d;">[${msg.displayTime}]</small>
<span style="font-weight: bold; margin: 0 0.25rem 0 0.5rem;">${msg.category}:</span>
${msg.message}
</div>
${msg.source ? `<small style="color: #868e96; font-size: 0.7rem; margin-left: 0.5rem;">${msg.source}</small>` : ''}
</div>
${msg.context && msg.context.headingId ? `
<div style="font-size: 0.7rem; color: #6c757d; padding-left: 1rem;">
📍 Section: ${msg.context.headingId}
</div>
` : ''}
</div>
`).join('')}
`;
} else {
container.innerHTML = `
<div style="padding: 0.5rem; color: #666; text-align: center;">
No debug messages yet<br>
<button onclick="window.MarkitectDebugSystem.addMessage('First debug message', 'INFO')"
style="margin-top: 0.5rem; padding: 0.25rem 0.5rem; background: #28a745; color: white; border: none; border-radius: 3px; cursor: pointer;">
Add Test Message
</button>
</div>
`;
}
} else {
container.innerHTML = `
<div style="padding: 0.5rem; color: #dc3545; text-align: center;">
Debug system not initialized
</div>
`;
}
} catch (e) {
console.log("🪲 Error updating messages:", e);
container.innerHTML = `
<div style="padding: 0.5rem; color: #dc3545; text-align: center;">
Error loading messages: ${e.message}
</div>
`;
}
};
// Add method to get message colors by type
debugControl.getMessageColor = function(type) {
const colors = {
'ERROR': '#dc3545',
'WARNING': '#fd7e14',
'SUCCESS': '#28a745',
'INFO': '#17a2b8',
'DEBUG': '#6f42c1'
};
return colors[type] || '#495057';
};
// Override collapse to clean up intervals
const originalCollapse = debugControl.collapse;
debugControl.collapse = function() {
if (this.messageUpdateInterval) {
clearInterval(this.messageUpdateInterval);
this.messageUpdateInterval = null;
}
originalCollapse.call(this);
};
// Create and show the debug control
console.log("🪲 Creating debug control...");
debugControl.createControl();
console.log("🪲 Debug control created, element:", debugControl.element);
// Make debug control globally accessible
window.debugControl = debugControl;
console.log("🪲 Debug control setup complete and globally accessible");
} catch (error) {
console.error("DebugControl failed to initialize:", error);
}
// 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 system
window.MarkitectDebugSystem?.addMessage(`Document saved as: ${editedFilename}`, 'SUCCESS', 'DocumentSaver', {eventType: 'DOCUMENT'});
console.log(`Document successfully saved as: ${editedFilename}`);
} catch (error) {
window.MarkitectDebugSystem?.addMessage(`Save failed: ${error.message}`, 'ERROR', 'DocumentSaver', {eventType: 'DOCUMENT'});
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);
window.MarkitectDebugSystem?.addMessage(`Reset all sections to original state`, 'INFO', 'SectionManager', {eventType: 'EDITING'});
}
});
// Set up debug system integration with section-aware logging
sectionManager.on('sections-created', (data) => {
window.MarkitectDebugSystem?.addMessage(`Created ${data.count} sections`, 'INFO', 'SectionManager', {eventType: 'DOCUMENT'});
});
sectionManager.on('edit-started', (data) => {
window.MarkitectDebugSystem?.addMessage(`Edit started for section: ${data.sectionId}`, 'DEBUG', 'SectionManager', {eventType: 'EDITING', sectionId: data.sectionId});
});
sectionManager.on('changes-accepted', (data) => {
window.MarkitectDebugSystem?.addMessage(`Changes accepted for section: ${data.sectionId}`, 'SUCCESS', 'SectionManager', {eventType: 'EDITING', sectionId: data.sectionId});
// 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);
window.MarkitectDebugSystem?.addMessage(`DOM updated for section: ${data.sectionId}`, 'INFO', 'DOMRenderer', {eventType: 'EDITING', sectionId: data.sectionId});
}
}
});
sectionManager.on('changes-cancelled', (data) => {
window.MarkitectDebugSystem?.addMessage(`Changes cancelled for section: ${data.sectionId}`, 'WARNING', 'SectionManager', {eventType: 'EDITING', sectionId: data.sectionId});
});
// Initialize with markdown content
const markdownToRender = markdownContent || '';
if (markdownToRender.trim()) {
const sections = sectionManager.createSectionsFromMarkdown(markdownToRender);
domRenderer.renderAllSections(sections);
window.MarkitectDebugSystem?.addMessage(`Initialized with ${sections.length} sections`, 'INFO', 'DocumentRenderer', {eventType: 'DOCUMENT'});
} else {
window.MarkitectDebugSystem?.addMessage('No markdown content to initialize', 'WARNING', 'DocumentRenderer', {eventType: 'DOCUMENT'});
}
// 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...");
// Check if modular components are being used
if (typeof SectionManager !== 'undefined') {
console.log("✓ Modular components detected - skipping direct 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");
console.log('✓ Markdown 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 2: Initialize edit/insert capabilities if enabled
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);
}
}
// Step 3: Initialize document scroll indicators (always available)
try {
initializeScrollIndicators();
} catch (error) {
console.error("Scroll indicators failed to initialize:", error);
}
// Step 4: Define abstract Control class for UI controls
const Control = {
// Abstract control properties
element: null,
isExpanded: false,
isHeaderOnly: false, // New state for header-only mode
isDragging: false,
isResizing: false, // New state for resizing mode
dragOffset: { x: 0, y: 0 },
resizeStartSize: { width: 280, height: 'auto' },
originalPosition: { top: '80px', left: '20px' },
defaultSize: { width: 280, minWidth: 200, minHeight: 150 },
// Configuration properties (to be overridden by subclasses)
config: {
icon: '?',
title: 'Control',
className: 'control',
defaultContent: 'Template only',
ariaLabel: 'Control',
position: 'w' // Default compass position: west (middle-left)
},
// Compass positioning system (top-aligned for proper expansion)
compassPositions: {
// North positions (top)
'n': { top: '20px', left: '50%', transform: 'translateX(-50%)' },
'nne': { top: '20px', left: '65%', transform: 'translateX(-50%)' },
'ne': { top: '20px', right: '20px' },
'ene': { top: '80px', right: '20px' }, // Top-aligned instead of center
// East positions (right)
'e': { top: '50vh', right: '20px', transform: 'translateY(-20px)' }, // Anchor at icon level
'ese': { top: 'calc(65vh - 20px)', right: '20px' }, // Top-aligned
'se': { bottom: '20px', right: '20px' },
'sse': { bottom: '20px', right: '35%', transform: 'translateX(50%)' },
// South positions (bottom)
's': { bottom: '20px', left: '50%', transform: 'translateX(-50%)' },
'ssw': { bottom: '20px', left: '35%', transform: 'translateX(-50%)' },
'sw': { bottom: '20px', left: '20px' },
'wsw': { bottom: '80px', left: '20px' }, // Top-aligned instead of center
// West positions (left) - top-aligned for proper expansion
'w': { top: '50vh', left: '20px', transform: 'translateY(-20px)' }, // Anchor at icon level
'wnw': { top: '80px', left: '20px' }, // Top-aligned instead of center
'nw': { top: '20px', left: '20px' },
'nnw': { top: '20px', left: '35%', transform: 'translateX(-50%)' }
},
// Get expansion direction based on compass position
getExpansionDirection: function() {
const pos = this.config.position;
const rightBorderPositions = ['ne', 'ene', 'e', 'ese', 'se'];
const bottomBorderPositions = ['sw', 'ssw', 's', 'sse', 'se'];
return {
header: rightBorderPositions.includes(pos) ? 'left' : 'right',
body: bottomBorderPositions.includes(pos) ? 'up' : 'down'
};
},
// Calculate position styles based on compass direction
getPositionStyles: function() {
const compassPos = this.compassPositions[this.config.position] || this.compassPositions['w'];
return {
position: 'fixed',
top: compassPos.top || 'auto',
right: compassPos.right || 'auto',
bottom: compassPos.bottom || 'auto',
left: compassPos.left || 'auto',
transform: compassPos.transform || 'none',
zIndex: 1000
};
},
// Abstract methods (to be implemented by subclasses)
buildContent: function() {
const content = this.element.querySelector('.control-content');
content.innerHTML = `<p style="padding: 1rem; color: #666;">${this.config.defaultContent}</p>`;
},
// Concrete methods (shared by all controls)
createControl: function() {
console.log(`🎛️ Creating ${this.config.title} control...`);
this.element = document.createElement('div');
this.element.className = this.config.className;
this.element.innerHTML = `
<button class="control-toggle" aria-label="${this.config.ariaLabel}">${this.config.icon}</button>
<div class="control-panel" style="display: none;">
<div class="control-header">
<span class="control-icon">${this.config.icon}</span>
<h3>${this.config.title}</h3>
<button class="control-close">✕</button>
</div>
<div class="control-content">Loading...</div>
</div>
`;
// Position using compass direction
const positionStyles = this.getPositionStyles();
this.element.style.cssText = `
position: ${positionStyles.position};
top: ${positionStyles.top};
right: ${positionStyles.right};
bottom: ${positionStyles.bottom};
left: ${positionStyles.left};
transform: ${positionStyles.transform};
z-index: ${positionStyles.zIndex};
background: rgba(255, 255, 255, 0.95);
border: 1px solid #e1e5e9;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
backdrop-filter: blur(8px);
width: 40px;
transition: all 0.3s ease;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
`;
// Store original position for reset
this.originalPosition = {
top: positionStyles.top,
right: positionStyles.right,
bottom: positionStyles.bottom,
left: positionStyles.left,
transform: positionStyles.transform
};
// Style toggle button
const toggleBtn = this.element.querySelector('.control-toggle');
toggleBtn.style.cssText = `
width: 100%;
height: 40px;
border: none;
background: transparent;
cursor: pointer;
font-size: 16px;
color: #666;
transition: color 0.2s ease;
`;
// Handle click to build content on-demand
toggleBtn.addEventListener('click', () => {
if (this.isExpanded) {
this.collapse();
} else {
console.log(`🎛️ ${this.config.title} toggle clicked - building content...`);
this.buildContent();
}
});
// Close button handler
const closeBtn = this.element.querySelector('.control-close');
closeBtn.addEventListener('click', () => {
this.collapse();
});
// Responsive behavior
window.addEventListener('resize', () => {
if (window.innerWidth <= 768) {
this.element.style.display = 'none';
} else {
this.element.style.display = '';
}
});
document.body.appendChild(this.element);
// Hide on mobile
if (window.innerWidth <= 768) {
this.element.style.display = 'none';
}
console.log(`🎛️ ${this.config.title} control created`);
},
styleHeader: function() {
const header = this.element.querySelector('.control-header');
// Style the header to show icon, title, and close button in one line
// Match the height of the collapsed icon state (40px)
header.style.cssText = `
display: flex;
align-items: center;
justify-content: space-between;
height: 40px;
padding: 0 1rem;
border-bottom: 1px solid #eee;
margin-bottom: 0;
`;
const icon = header.querySelector('.control-icon');
if (icon) {
icon.style.cssText = `
font-size: 16px;
color: #666;
margin-right: 0.5rem;
cursor: grab;
user-select: none;
`;
// Make icon draggable
this.setupDragHandlers(icon);
}
const title = header.querySelector('h3');
if (title) {
title.style.cssText = `
margin: 0;
font-size: 0.9rem;
font-weight: 600;
flex-grow: 1;
line-height: 1;
cursor: pointer;
user-select: none;
`;
// Add click handler to toggle header-only mode
title.addEventListener('click', () => {
this.toggleHeaderOnly();
});
}
const closeBtn = header.querySelector('.control-close');
if (closeBtn) {
closeBtn.style.cssText = `
background: none;
border: none;
font-size: 14px;
cursor: pointer;
color: #666;
padding: 0;
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
`;
}
},
styleContent: function() {
const content = this.element.querySelector('.control-content');
const expansion = this.getExpansionDirection();
// Style the content area based on expansion direction
let contentStyles = `
padding: 0.5rem;
overflow-y: auto;
`;
if (expansion.body === 'up') {
// Body expands upward (for bottom border positions)
contentStyles += `
max-height: calc(80vh - 40px);
`;
content.parentElement.style.flexDirection = 'column-reverse';
} else {
// Body expands downward (default)
contentStyles += `
max-height: calc(80vh - 40px);
`;
content.parentElement.style.flexDirection = 'column';
}
content.style.cssText = contentStyles;
},
expand: function() {
this.isExpanded = true;
const panel = this.element.querySelector('.control-panel');
const toggleBtn = this.element.querySelector('.control-toggle');
// Get expansion direction based on compass position
const expansion = this.getExpansionDirection();
// Apply expansion styling based on direction
if (expansion.header === 'left') {
// Header expands to the left (for right border positions)
this.element.style.width = '280px';
this.element.style.transformOrigin = 'top right';
} else {
// Header expands to the right (default)
this.element.style.width = '280px';
this.element.style.transformOrigin = 'top left';
}
panel.style.display = 'block';
toggleBtn.style.display = 'none';
this.styleHeader();
this.styleContent();
this.addResizeHandle();
},
collapse: function() {
this.isExpanded = false;
this.isHeaderOnly = false; // Reset header-only state
const panel = this.element.querySelector('.control-panel');
const toggleBtn = this.element.querySelector('.control-toggle');
panel.style.display = 'none';
// Reset size to default
this.element.style.width = '40px';
this.element.style.height = 'auto';
// Remove resize handle
this.removeResizeHandle();
toggleBtn.style.display = 'block';
// Reset position to original compass location
this.element.style.top = this.originalPosition.top;
this.element.style.right = this.originalPosition.right;
this.element.style.bottom = this.originalPosition.bottom;
this.element.style.left = this.originalPosition.left;
this.element.style.transform = this.originalPosition.transform;
},
toggleHeaderOnly: function() {
if (!this.isExpanded) {
// If collapsed, first expand normally
this.buildContent();
return;
}
const content = this.element.querySelector('.control-content');
if (this.isHeaderOnly) {
// Show content area (go to full expanded mode)
this.isHeaderOnly = false;
content.style.display = 'block';
console.log(`🎛️ ${this.config.title} expanded to full view`);
} else {
// Hide content area (go to header-only mode)
this.isHeaderOnly = true;
content.style.display = 'none';
console.log(`🎛️ ${this.config.title} collapsed to header only`);
}
},
setupDragHandlers: function(dragElement) {
dragElement.addEventListener('mousedown', (e) => {
this.isDragging = true;
const rect = this.element.getBoundingClientRect();
const iconRect = dragElement.getBoundingClientRect();
// Calculate offset relative to the icon position, not the element
this.dragOffset.x = e.clientX - rect.left;
this.dragOffset.y = iconRect.top - rect.top + (iconRect.height / 2); // Keep mouse at icon center
dragElement.style.cursor = 'grabbing';
e.preventDefault();
});
document.addEventListener('mousemove', (e) => {
if (!this.isDragging || !this.isExpanded) return;
const newX = e.clientX - this.dragOffset.x;
const newY = e.clientY - this.dragOffset.y;
// Keep within viewport bounds
const maxX = window.innerWidth - this.element.offsetWidth;
const maxY = window.innerHeight - this.element.offsetHeight;
const boundedX = Math.max(0, Math.min(newX, maxX));
const boundedY = Math.max(0, Math.min(newY, maxY));
this.element.style.left = boundedX + 'px';
this.element.style.top = boundedY + 'px';
});
document.addEventListener('mouseup', () => {
if (this.isDragging) {
this.isDragging = false;
dragElement.style.cursor = 'grab';
}
});
},
// Add resize handle to expanded control
addResizeHandle: function() {
// Remove existing resize handle if any
this.removeResizeHandle();
const resizeHandle = document.createElement('div');
resizeHandle.className = 'control-resize-handle';
resizeHandle.innerHTML = '↘';
resizeHandle.style.cssText = `
position: absolute;
bottom: 0;
right: 0;
width: 20px;
height: 20px;
background: rgba(108, 117, 125, 0.8);
color: white;
cursor: nw-resize;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
line-height: 1;
border-top-left-radius: 4px;
user-select: none;
z-index: 1001;
`;
this.element.appendChild(resizeHandle);
this.setupResizeHandlers(resizeHandle);
},
// Remove resize handle
removeResizeHandle: function() {
const existingHandle = this.element.querySelector('.control-resize-handle');
if (existingHandle) {
existingHandle.remove();
}
},
// Set up resize event handlers
setupResizeHandlers: function(resizeHandle) {
resizeHandle.addEventListener('mousedown', (e) => {
this.isResizing = true;
const rect = this.element.getBoundingClientRect();
this.resizeStartSize = {
width: rect.width,
height: rect.height,
startX: e.clientX,
startY: e.clientY
};
resizeHandle.style.cursor = 'nw-resize';
resizeHandle.style.background = 'rgba(40, 167, 69, 0.9)';
e.preventDefault();
e.stopPropagation(); // Prevent triggering drag
});
document.addEventListener('mousemove', (e) => {
if (!this.isResizing || !this.isExpanded) return;
const deltaX = e.clientX - this.resizeStartSize.startX;
const deltaY = e.clientY - this.resizeStartSize.startY;
const newWidth = Math.max(this.defaultSize.minWidth, this.resizeStartSize.width + deltaX);
const newHeight = Math.max(this.defaultSize.minHeight, this.resizeStartSize.height + deltaY);
// Check viewport bounds
const maxWidth = window.innerWidth - this.element.offsetLeft;
const maxHeight = window.innerHeight - this.element.offsetTop;
const boundedWidth = Math.min(newWidth, maxWidth - 20);
const boundedHeight = Math.min(newHeight, maxHeight - 20);
this.element.style.width = boundedWidth + 'px';
this.element.style.height = boundedHeight + 'px';
// Ensure content areas resize properly
this.updateContentSize();
});
document.addEventListener('mouseup', () => {
if (this.isResizing) {
this.isResizing = false;
resizeHandle.style.cursor = 'nw-resize';
resizeHandle.style.background = 'rgba(108, 117, 125, 0.8)';
}
});
},
// Update content area sizes during resize
updateContentSize: function() {
const content = this.element.querySelector('.control-content');
if (content) {
// Adjust content height to fit the resized control
const headerHeight = 40; // Header is 40px
const padding = 16; // Account for padding
const controlHeight = this.element.offsetHeight;
const availableHeight = controlHeight - headerHeight - padding;
content.style.maxHeight = Math.max(100, availableHeight) + 'px';
}
}
};
// Step 5: Initialize ContentsControl (new implementation based on Control class)
try {
const contentsControl = Object.create(Control);
// Configure for contents navigation
contentsControl.config = {
icon: '☰',
title: 'Contents',
className: 'contents-control',
defaultContent: 'No headings found',
ariaLabel: 'Document Navigation',
position: 'wnw' // West-north-west positioning
};
// Override buildContent method for navigation functionality
contentsControl.buildContent = function() {
const content = this.element.querySelector('.control-content');
// Build navigation content from current DOM
const allHeadings = document.querySelectorAll('h1, h2, h3');
// Filter out headings that contain "Contents" or similar navigation-related text
const headings = Array.from(allHeadings).filter(heading => {
const text = heading.textContent.trim().toLowerCase();
return !text.includes('contents') && !text.includes('table of contents') && !text.includes('navigation');
});
console.log("📋 Found headings for navigation:", headings.length);
if (headings.length === 0) {
content.innerHTML = '<p style="padding: 1rem; color: #666;">No headings found</p>';
} else {
let navHtml = '';
headings.forEach((heading, index) => {
if (!heading.id) {
heading.id = `heading-${index + 1}`;
}
const level = parseInt(heading.tagName.substring(1));
const indent = (level - 1) * 1;
navHtml += `
<a href="#${heading.id}"
style="display: block; padding: 0.5rem; margin-left: ${indent}rem;
text-decoration: none; color: #333; font-size: 0.9rem;
border-radius: 4px; cursor: pointer;"
onmouseover="this.style.backgroundColor='#f5f5f5'"
onmouseout="this.style.backgroundColor=''"
onclick="event.preventDefault(); document.getElementById('${heading.id}').scrollIntoView({behavior: 'smooth'}); if (window.innerWidth <= 768) setTimeout(() => contentsControl.collapse(), 500);">
${heading.textContent.trim()}
</a>
`;
});
content.innerHTML = navHtml;
}
// Show panel
this.expand();
};
// Initialize the ContentsControl
contentsControl.createControl();
// Make globally available for mobile collapse
window.contentsControl = contentsControl;
} catch (error) {
console.error("ContentsControl failed to initialize:", error);
}
});
// Handle CDN loading errors
window.addEventListener('load', function() {
if (window.markitectMarkedError) {
console.error("CDN library failed to load - network or firewall blocking marked.js");
}
});
</script>
</body>
</html>