4092 lines
157 KiB
HTML
Executable File
4092 lines
157 KiB
HTML
Executable File
<!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> |