1769 lines
59 KiB
JavaScript
1769 lines
59 KiB
JavaScript
|
|
// Clean Editor Architecture
|
|||
|
|
/**
|
|||
|
|
* Test-Driven Section Editor Implementation
|
|||
|
|
*
|
|||
|
|
* A clean, object-oriented approach to handling section editing
|
|||
|
|
* that can be tested independently of the DOM.
|
|||
|
|
*/
|
|||
|
|
|
|||
|
|
// Debug system - choose one of: 'off', 'console', 'alerts'
|
|||
|
|
const DEBUG_MODE = 'console';
|
|||
|
|
|
|||
|
|
// Advanced State Management
|
|||
|
|
const EditState = Object.freeze({
|
|||
|
|
ORIGINAL: 'original',
|
|||
|
|
EDITING: 'editing',
|
|||
|
|
MODIFIED: 'modified',
|
|||
|
|
SAVED: 'saved'
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
function debug(message, category = 'INFO') {
|
|||
|
|
const prefix = `DEBUG ${category}:`;
|
|||
|
|
|
|||
|
|
switch (DEBUG_MODE) {
|
|||
|
|
case 'off':
|
|||
|
|
// No debugging output
|
|||
|
|
break;
|
|||
|
|
case 'console':
|
|||
|
|
console.log(prefix, message);
|
|||
|
|
break;
|
|||
|
|
case 'alerts':
|
|||
|
|
alert(`${prefix} ${message}`);
|
|||
|
|
console.log(prefix, message); // Also log to console for reference
|
|||
|
|
break;
|
|||
|
|
default:
|
|||
|
|
console.warn('Invalid DEBUG_MODE. Use: off, console, or alerts');
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Enums for clear state management (already defined above)
|
|||
|
|
|
|||
|
|
const SectionType = Object.freeze({
|
|||
|
|
HEADING: 'heading',
|
|||
|
|
PARAGRAPH: 'paragraph',
|
|||
|
|
LIST: 'list',
|
|||
|
|
CODE: 'code',
|
|||
|
|
QUOTE: 'quote',
|
|||
|
|
IMAGE: 'image',
|
|||
|
|
OTHER: 'other'
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Section class - Represents a single editable section
|
|||
|
|
*/
|
|||
|
|
class Section {
|
|||
|
|
constructor(id, markdown, type) {
|
|||
|
|
this.id = id;
|
|||
|
|
this.originalMarkdown = markdown;
|
|||
|
|
this.currentMarkdown = markdown;
|
|||
|
|
this.editingMarkdown = markdown;
|
|||
|
|
this.pendingMarkdown = null;
|
|||
|
|
this.type = type;
|
|||
|
|
this.state = EditState.ORIGINAL;
|
|||
|
|
this.domElement = null;
|
|||
|
|
this.lastSaved = null;
|
|||
|
|
this.created = new Date();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
static generateId(markdown, position) {
|
|||
|
|
const cleanText = markdown.replace(/[^a-zA-Z0-9]/g, '');
|
|||
|
|
const hash = cleanText.substring(0, 8) + position;
|
|||
|
|
return `section-${hash}`;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
static detectType(markdown) {
|
|||
|
|
const trimmed = markdown.trim();
|
|||
|
|
if (trimmed.startsWith('#')) return SectionType.HEADING;
|
|||
|
|
if (trimmed.startsWith('```')) return SectionType.CODE;
|
|||
|
|
if (trimmed.startsWith('>')) return SectionType.QUOTE;
|
|||
|
|
if (trimmed.includes('![')) return SectionType.IMAGE;
|
|||
|
|
if (trimmed.startsWith('-') || trimmed.startsWith('*') || trimmed.startsWith('1.')) return SectionType.LIST;
|
|||
|
|
return SectionType.PARAGRAPH;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
startEdit() {
|
|||
|
|
if (this.state === EditState.EDITING) {
|
|||
|
|
throw new Error(`Section ${this.id} is already being edited`);
|
|||
|
|
}
|
|||
|
|
this.editingMarkdown = this.pendingMarkdown || this.currentMarkdown;
|
|||
|
|
this.state = EditState.EDITING;
|
|||
|
|
return this.editingMarkdown;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
updateContent(markdown) {
|
|||
|
|
if (this.state !== EditState.EDITING) {
|
|||
|
|
throw new Error(`Section ${this.id} is not in editing state`);
|
|||
|
|
}
|
|||
|
|
this.editingMarkdown = markdown;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
acceptChanges() {
|
|||
|
|
if (this.state !== EditState.EDITING) {
|
|||
|
|
throw new Error(`Section ${this.id} is not in editing state`);
|
|||
|
|
}
|
|||
|
|
this.currentMarkdown = this.editingMarkdown;
|
|||
|
|
this.editingMarkdown = null;
|
|||
|
|
this.pendingMarkdown = null;
|
|||
|
|
this.state = EditState.SAVED;
|
|||
|
|
this.lastSaved = new Date();
|
|||
|
|
return this.currentMarkdown;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
cancelChanges() {
|
|||
|
|
if (this.state !== EditState.EDITING) {
|
|||
|
|
throw new Error(`Section ${this.id} is not in editing state`);
|
|||
|
|
}
|
|||
|
|
this.editingMarkdown = null;
|
|||
|
|
if (this.pendingMarkdown !== null) {
|
|||
|
|
this.state = EditState.MODIFIED;
|
|||
|
|
return this.pendingMarkdown;
|
|||
|
|
} else if (this.lastSaved !== null) {
|
|||
|
|
this.state = EditState.SAVED;
|
|||
|
|
return this.currentMarkdown;
|
|||
|
|
} else {
|
|||
|
|
this.state = this.hasChanges() ? EditState.MODIFIED : EditState.ORIGINAL;
|
|||
|
|
return this.currentMarkdown;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
stopEditing() {
|
|||
|
|
if (this.state !== EditState.EDITING) {
|
|||
|
|
return this.state;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// If we have editing changes that differ from current content, preserve them as pending
|
|||
|
|
if (this.editingMarkdown && this.editingMarkdown !== this.currentMarkdown) {
|
|||
|
|
this.pendingMarkdown = this.editingMarkdown;
|
|||
|
|
this.state = EditState.MODIFIED; // Has pending changes
|
|||
|
|
} else {
|
|||
|
|
// No changes made during this edit session
|
|||
|
|
this.pendingMarkdown = null;
|
|||
|
|
if (this.lastSaved !== null) {
|
|||
|
|
this.state = EditState.SAVED;
|
|||
|
|
} else {
|
|||
|
|
this.state = this.hasChanges() ? EditState.MODIFIED : EditState.ORIGINAL;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
this.editingMarkdown = null;
|
|||
|
|
return this.state;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
resetToOriginal() {
|
|||
|
|
this.currentMarkdown = this.originalMarkdown;
|
|||
|
|
this.editingMarkdown = this.originalMarkdown;
|
|||
|
|
this.pendingMarkdown = null;
|
|||
|
|
this.state = EditState.ORIGINAL;
|
|||
|
|
return this.originalMarkdown;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
isEditing() {
|
|||
|
|
return this.state === EditState.EDITING;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
hasChanges() {
|
|||
|
|
return this.currentMarkdown !== this.originalMarkdown;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
getStatus() {
|
|||
|
|
return {
|
|||
|
|
id: this.id,
|
|||
|
|
state: this.state,
|
|||
|
|
hasChanges: this.hasChanges(),
|
|||
|
|
isEditing: this.isEditing(),
|
|||
|
|
contentLength: this.currentMarkdown.length,
|
|||
|
|
lastSaved: this.lastSaved,
|
|||
|
|
sectionType: this.sectionType,
|
|||
|
|
// Legacy compatibility
|
|||
|
|
type: this.type,
|
|||
|
|
originalLength: this.originalMarkdown.length,
|
|||
|
|
currentLength: this.currentMarkdown.length
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
isImage() {
|
|||
|
|
return this.type === SectionType.IMAGE;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
isProtectedHeading() {
|
|||
|
|
// In insert mode, headings 1-3 are protected
|
|||
|
|
if (this.type === SectionType.HEADING) {
|
|||
|
|
const headingMatch = this.originalMarkdown.match(/^(#{1,3})\s/);
|
|||
|
|
if (headingMatch) {
|
|||
|
|
this.headingLevel = headingMatch[1].length;
|
|||
|
|
return this.headingLevel <= 3;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
getHeadingContent() {
|
|||
|
|
if (this.type === SectionType.HEADING) {
|
|||
|
|
const lines = this.currentMarkdown.split('\n');
|
|||
|
|
return lines.slice(1).join('\n').trim();
|
|||
|
|
}
|
|||
|
|
return this.currentMarkdown;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* SectionManager class - Manages the collection of sections
|
|||
|
|
*/
|
|||
|
|
class SectionManager {
|
|||
|
|
constructor() {
|
|||
|
|
this.sections = new Map();
|
|||
|
|
this.listeners = new Map();
|
|||
|
|
this.statusInterval = null;
|
|||
|
|
this.lastStatusUpdate = new Date().toISOString();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
on(event, callback) {
|
|||
|
|
if (!this.listeners.has(event)) {
|
|||
|
|
this.listeners.set(event, []);
|
|||
|
|
}
|
|||
|
|
this.listeners.get(event).push(callback);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
emit(event, data) {
|
|||
|
|
if (this.listeners.has(event)) {
|
|||
|
|
this.listeners.get(event).forEach(callback => callback(data));
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
createSectionsFromMarkdown(markdownContent) {
|
|||
|
|
const lines = markdownContent.split('\n');
|
|||
|
|
const sections = [];
|
|||
|
|
let currentSection = '';
|
|||
|
|
let position = 0;
|
|||
|
|
|
|||
|
|
for (let i = 0; i < lines.length; i++) {
|
|||
|
|
const line = lines[i];
|
|||
|
|
const isHeading = /^#{1,6}\s/.test(line);
|
|||
|
|
const isNewParagraph = line.trim() && i > 0 && !lines[i-1].trim();
|
|||
|
|
const isNewSection = isHeading || isNewParagraph;
|
|||
|
|
|
|||
|
|
if (isNewSection && currentSection.trim()) {
|
|||
|
|
const sectionId = Section.generateId(currentSection, position);
|
|||
|
|
const sectionType = Section.detectType(currentSection);
|
|||
|
|
const section = new Section(sectionId, currentSection.trim(), sectionType);
|
|||
|
|
sections.push(section);
|
|||
|
|
this.sections.set(sectionId, section);
|
|||
|
|
position++;
|
|||
|
|
currentSection = line;
|
|||
|
|
} else {
|
|||
|
|
if (currentSection) currentSection += '\n';
|
|||
|
|
currentSection += line;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (currentSection.trim()) {
|
|||
|
|
const sectionId = Section.generateId(currentSection, position);
|
|||
|
|
const sectionType = Section.detectType(currentSection);
|
|||
|
|
const section = new Section(sectionId, currentSection.trim(), sectionType);
|
|||
|
|
sections.push(section);
|
|||
|
|
this.sections.set(sectionId, section);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
this.emit('sections-created', { sections, count: sections.length });
|
|||
|
|
return sections;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
startEditing(sectionId) {
|
|||
|
|
const section = this.sections.get(sectionId);
|
|||
|
|
if (!section) {
|
|||
|
|
throw new Error(`Section ${sectionId} not found`);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (section.isEditing()) {
|
|||
|
|
console.log('Section already in editing state:', sectionId);
|
|||
|
|
return section.editingMarkdown;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const content = section.startEdit();
|
|||
|
|
this.emit('edit-started', { sectionId, content, section: section.getStatus() });
|
|||
|
|
return content;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
updateContent(sectionId, markdown) {
|
|||
|
|
const section = this.sections.get(sectionId);
|
|||
|
|
if (!section) {
|
|||
|
|
throw new Error(`Section ${sectionId} not found`);
|
|||
|
|
}
|
|||
|
|
section.updateContent(markdown);
|
|||
|
|
this.emit('content-updated', { sectionId, markdown, section: section.getStatus() });
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
acceptChanges(sectionId) {
|
|||
|
|
const section = this.sections.get(sectionId);
|
|||
|
|
if (!section) {
|
|||
|
|
throw new Error(`Section ${sectionId} not found`);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const content = section.acceptChanges();
|
|||
|
|
this.emit('changes-accepted', { sectionId, content, section: section.getStatus() });
|
|||
|
|
return content;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
cancelChanges(sectionId) {
|
|||
|
|
const section = this.sections.get(sectionId);
|
|||
|
|
if (!section) {
|
|||
|
|
throw new Error(`Section ${sectionId} not found`);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const content = section.cancelChanges();
|
|||
|
|
this.emit('changes-cancelled', { sectionId, content, section: section.getStatus() });
|
|||
|
|
return content;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
resetSection(sectionId) {
|
|||
|
|
const section = this.sections.get(sectionId);
|
|||
|
|
if (!section) {
|
|||
|
|
throw new Error(`Section ${sectionId} not found`);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const content = section.resetToOriginal();
|
|||
|
|
this.emit('section-reset', { sectionId, content, section: section.getStatus() });
|
|||
|
|
return content;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
getDocumentMarkdown() {
|
|||
|
|
const sortedSections = Array.from(this.sections.values())
|
|||
|
|
.sort((a, b) => a.created - b.created);
|
|||
|
|
|
|||
|
|
return sortedSections.map(section => section.currentMarkdown).join('\n\n');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
getAllSections() {
|
|||
|
|
return Array.from(this.sections.values());
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
getSectionStatus() {
|
|||
|
|
return Array.from(this.sections.values()).map(section => section.getStatus());
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
escapeRegex(str) {
|
|||
|
|
return str.replace(/[.*+?^${}()|\[\]\\]/g, '\\\\$&');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Check if new content contains new headings that would require section splitting
|
|||
|
|
* @param {string} newContent - The new content to check
|
|||
|
|
* @param {string} originalContent - The original content to compare against
|
|||
|
|
* @returns {boolean} True if section splitting is needed
|
|||
|
|
*/
|
|||
|
|
checkForSectionSplits(newContent, originalContent) {
|
|||
|
|
const originalHeadings = this.extractHeadings(originalContent);
|
|||
|
|
const newHeadings = this.extractHeadings(newContent);
|
|||
|
|
|
|||
|
|
// If new content has more headings than original, we need to split
|
|||
|
|
return newHeadings.length > originalHeadings.length;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Extract heading lines from markdown content
|
|||
|
|
* @param {string} content - Markdown content
|
|||
|
|
* @returns {Array} Array of heading lines
|
|||
|
|
*/
|
|||
|
|
extractHeadings(content) {
|
|||
|
|
if (!content) return [];
|
|||
|
|
const lines = content.split('\n');
|
|||
|
|
return lines.filter(line => /^#{1,6}\s/.test(line.trim()));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Handle splitting a section when new headings are detected
|
|||
|
|
* @param {string} sectionId - ID of the section to split
|
|||
|
|
* @param {string} newContent - New content with multiple headings
|
|||
|
|
*/
|
|||
|
|
handleSectionSplit(sectionId, newContent) {
|
|||
|
|
const section = this.sections.get(sectionId);
|
|||
|
|
if (!section) {
|
|||
|
|
throw new Error(`Section ${sectionId} not found`);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Remove the original section
|
|||
|
|
this.sections.delete(sectionId);
|
|||
|
|
|
|||
|
|
// Create new sections from the content
|
|||
|
|
const newSections = this.createSectionsFromContent(newContent);
|
|||
|
|
|
|||
|
|
// Emit section-split event
|
|||
|
|
this.emit('section-split', {
|
|||
|
|
originalSectionId: sectionId,
|
|||
|
|
newSections: newSections,
|
|||
|
|
count: newSections.length
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
return newSections;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Create sections from content (alias for createSectionsFromMarkdown)
|
|||
|
|
* @param {string} content - Markdown content
|
|||
|
|
* @returns {Array} Array of created sections
|
|||
|
|
*/
|
|||
|
|
createSectionsFromContent(content) {
|
|||
|
|
return this.createSectionsFromMarkdown(content);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Get global status information about all sections
|
|||
|
|
* @returns {Object} Status object with global information
|
|||
|
|
*/
|
|||
|
|
getGlobalStatus() {
|
|||
|
|
const sections = this.getAllSections();
|
|||
|
|
const editingSections = sections.filter(s => s.isEditing()).map(s => s.id);
|
|||
|
|
const modifiedSections = sections.filter(s => s.hasChanges());
|
|||
|
|
const hasModifications = modifiedSections.length > 0 || editingSections.length > 0;
|
|||
|
|
|
|||
|
|
let state = 'ready';
|
|||
|
|
if (editingSections.length > 0) {
|
|||
|
|
state = 'editing';
|
|||
|
|
} else if (hasModifications) {
|
|||
|
|
state = 'modified';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
totalSections: sections.length,
|
|||
|
|
editingSections: editingSections,
|
|||
|
|
modifiedSections: modifiedSections.length,
|
|||
|
|
hasModifications: hasModifications,
|
|||
|
|
state: state,
|
|||
|
|
lastUpdate: this.lastStatusUpdate
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Update global status and emit status-updated event
|
|||
|
|
*/
|
|||
|
|
updateGlobalStatus() {
|
|||
|
|
this.lastStatusUpdate = new Date().toISOString();
|
|||
|
|
const status = this.getGlobalStatus();
|
|||
|
|
this.emit('status-updated', status);
|
|||
|
|
return status;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Start periodic status tracking
|
|||
|
|
* @param {number} intervalMs - Update interval in milliseconds (default: 2000)
|
|||
|
|
*/
|
|||
|
|
startStatusTracking(intervalMs = 2000) {
|
|||
|
|
if (this.statusInterval) {
|
|||
|
|
clearInterval(this.statusInterval);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
this.statusInterval = setInterval(() => {
|
|||
|
|
this.updateGlobalStatus();
|
|||
|
|
}, intervalMs);
|
|||
|
|
|
|||
|
|
// Emit initial status
|
|||
|
|
this.updateGlobalStatus();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Stop periodic status tracking
|
|||
|
|
*/
|
|||
|
|
stopStatusTracking() {
|
|||
|
|
if (this.statusInterval) {
|
|||
|
|
clearInterval(this.statusInterval);
|
|||
|
|
this.statusInterval = null;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* DOM Renderer - Handles DOM interactions
|
|||
|
|
*/
|
|||
|
|
class DOMRenderer {
|
|||
|
|
constructor(sectionManager, container) {
|
|||
|
|
this.sectionManager = sectionManager;
|
|||
|
|
this.container = container;
|
|||
|
|
this.editingSections = new Set();
|
|||
|
|
|
|||
|
|
this.handleSectionClick = this.handleSectionClick.bind(this);
|
|||
|
|
this.handleAccept = this.handleAccept.bind(this);
|
|||
|
|
this.handleCancel = this.handleCancel.bind(this);
|
|||
|
|
this.handleReset = this.handleReset.bind(this);
|
|||
|
|
this.handleKeydown = this.handleKeydown.bind(this);
|
|||
|
|
|
|||
|
|
this.setupEventListeners();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
setupEventListeners() {
|
|||
|
|
this.sectionManager.on('sections-created', (data) => {
|
|||
|
|
this.renderAllSections(data.sections);
|
|||
|
|
});
|
|||
|
|
this.sectionManager.on('edit-started', (data) => {
|
|||
|
|
this.showEditor(data.sectionId, data.content);
|
|||
|
|
});
|
|||
|
|
this.sectionManager.on('edit-stopped', (data) => {
|
|||
|
|
this.hideEditor(data.sectionId);
|
|||
|
|
});
|
|||
|
|
this.sectionManager.on('changes-accepted', (data) => {
|
|||
|
|
this.hideEditor(data.sectionId);
|
|||
|
|
this.updateSectionContent(data.sectionId, data.content);
|
|||
|
|
});
|
|||
|
|
this.sectionManager.on('changes-cancelled', (data) => {
|
|||
|
|
this.hideEditor(data.sectionId);
|
|||
|
|
this.updateSectionContent(data.sectionId, data.content);
|
|||
|
|
});
|
|||
|
|
this.sectionManager.on('section-reset', (data) => {
|
|||
|
|
this.updateTextareaContent(data.content, data.sectionId);
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
renderAllSections(sections) {
|
|||
|
|
debug('21: renderAllSections called with ' + sections.length + ' sections', 'RENDER');
|
|||
|
|
|
|||
|
|
this.container.innerHTML = '';
|
|||
|
|
debug('22: Container cleared', 'RENDER');
|
|||
|
|
|
|||
|
|
sections.forEach((section, index) => {
|
|||
|
|
debug('23: Creating element for section ' + (index + 1) + ': ' + section.id, 'RENDER');
|
|||
|
|
const element = this.createSectionElement(section);
|
|||
|
|
section.domElement = element;
|
|||
|
|
this.container.appendChild(element);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
debug('24: All section elements added to container', 'RENDER');
|
|||
|
|
this.container.addEventListener('click', this.handleSectionClick);
|
|||
|
|
debug('25: Click listener attached - container content length: ' + this.container.innerHTML.length, 'RENDER');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
createSectionElement(section) {
|
|||
|
|
const element = document.createElement('div');
|
|||
|
|
element.setAttribute('data-section-id', section.id);
|
|||
|
|
debug('SECTION: Creating element for section ' + section.id + ' with content: ' + section.currentMarkdown.substring(0, 50) + '...', 'SECTION');
|
|||
|
|
|
|||
|
|
if (typeof marked !== 'undefined') {
|
|||
|
|
const html = marked.parse(section.currentMarkdown);
|
|||
|
|
const htmlWithTargetBlank = html.replace(/<a href="([^"]*)"([^>]*)>/g, '<a href="$1" target="_blank"$2>');
|
|||
|
|
element.innerHTML = htmlWithTargetBlank;
|
|||
|
|
} else {
|
|||
|
|
element.innerHTML = `<p>${section.currentMarkdown}</p>`;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
this.setupSectionElement(element);
|
|||
|
|
debug('SECTION: Section element setup complete for ' + section.id, 'SECTION');
|
|||
|
|
return element;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
handleSectionClick(event) {
|
|||
|
|
debug('CLICK: Click detected on target: ' + event.target.tagName + ' ' + (event.target.className || ''), 'CLICK');
|
|||
|
|
|
|||
|
|
// Don't handle clicks on form elements, buttons, or links
|
|||
|
|
if (event.target.closest('textarea, button, input, a')) {
|
|||
|
|
debug('CLICK: Ignoring click on form element', 'CLICK');
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const sectionElement = event.target.closest('.ui-edit-section');
|
|||
|
|
debug('CLICK: Found section element: ' + (sectionElement ? sectionElement.getAttribute('data-section-id') : 'null'), 'CLICK');
|
|||
|
|
if (!sectionElement) return;
|
|||
|
|
|
|||
|
|
const sectionId = sectionElement.getAttribute('data-section-id');
|
|||
|
|
debug('CLICK: Section ID: ' + sectionId, 'CLICK');
|
|||
|
|
if (!sectionId) return;
|
|||
|
|
|
|||
|
|
// Check if this section is already being edited
|
|||
|
|
const section = this.sectionManager.sections.get(sectionId);
|
|||
|
|
if (section && section.isEditing()) {
|
|||
|
|
console.log('Section already being edited:', sectionId);
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
console.log('Starting edit for section:', sectionId);
|
|||
|
|
this.sectionManager.startEditing(sectionId);
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('Failed to start editing:', error);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
showEditor(sectionId, content) {
|
|||
|
|
const element = this.findSectionElement(sectionId);
|
|||
|
|
if (!element) return;
|
|||
|
|
|
|||
|
|
this.hideCurrentEditor();
|
|||
|
|
|
|||
|
|
const section = this.sectionManager.sections.get(sectionId);
|
|||
|
|
const isImageSection = section && section.isImage();
|
|||
|
|
|
|||
|
|
if (isImageSection) {
|
|||
|
|
this.showImageEditor(sectionId, section);
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const editorContainer = document.createElement('div');
|
|||
|
|
editorContainer.className = 'ui-edit-editor-container';
|
|||
|
|
|
|||
|
|
const textarea = document.createElement('textarea');
|
|||
|
|
textarea.className = 'ui-edit-textarea ui-edit-textarea-main';
|
|||
|
|
textarea.value = content;
|
|||
|
|
textarea.style.cssText = `
|
|||
|
|
flex: 1;
|
|||
|
|
min-height: 100px;
|
|||
|
|
font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace;
|
|||
|
|
font-size: 14px;
|
|||
|
|
line-height: 1.5;
|
|||
|
|
padding: 12px;
|
|||
|
|
border: 2px solid #007bff;
|
|||
|
|
border-radius: 6px;
|
|||
|
|
resize: vertical;
|
|||
|
|
outline: none;
|
|||
|
|
background: white;
|
|||
|
|
color: #333;
|
|||
|
|
`;
|
|||
|
|
|
|||
|
|
// Setup auto-resize functionality
|
|||
|
|
this.setupAutoResize(textarea);
|
|||
|
|
|
|||
|
|
const controls = document.createElement('div');
|
|||
|
|
controls.className = 'ui-edit-controls';
|
|||
|
|
controls.style.cssText = `
|
|||
|
|
display: flex;
|
|||
|
|
gap: 8px;
|
|||
|
|
justify-content: flex-end;
|
|||
|
|
margin-top: 8px;
|
|||
|
|
`;
|
|||
|
|
|
|||
|
|
const acceptBtn = this.createButton('Accept', 'ui-edit-button-accept', this.handleAccept);
|
|||
|
|
const cancelBtn = this.createButton('Cancel', 'ui-edit-button-cancel', this.handleCancel);
|
|||
|
|
|
|||
|
|
controls.appendChild(acceptBtn);
|
|||
|
|
controls.appendChild(cancelBtn);
|
|||
|
|
|
|||
|
|
editorContainer.appendChild(textarea);
|
|||
|
|
editorContainer.appendChild(controls);
|
|||
|
|
|
|||
|
|
element.innerHTML = '';
|
|||
|
|
element.appendChild(editorContainer);
|
|||
|
|
|
|||
|
|
textarea.focus();
|
|||
|
|
this.editingSections.add(sectionId);
|
|||
|
|
|
|||
|
|
textarea.addEventListener('input', () => {
|
|||
|
|
this.sectionManager.updateContent(sectionId, textarea.value);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// Add keyboard shortcuts
|
|||
|
|
textarea.addEventListener('keydown', this.handleKeydown);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Show image editor with manipulation controls
|
|||
|
|
* @param {string} sectionId - The section ID
|
|||
|
|
* @param {Section} section - The section object
|
|||
|
|
*/
|
|||
|
|
showImageEditor(sectionId, section) {
|
|||
|
|
const element = this.findSectionElement(sectionId);
|
|||
|
|
if (!element) return;
|
|||
|
|
|
|||
|
|
this.hideCurrentEditor();
|
|||
|
|
|
|||
|
|
const editorContainer = document.createElement('div');
|
|||
|
|
editorContainer.className = 'ui-edit-image-editor-container';
|
|||
|
|
editorContainer.style.cssText = `
|
|||
|
|
display: flex;
|
|||
|
|
flex-direction: column;
|
|||
|
|
gap: 12px;
|
|||
|
|
margin-top: 12px;
|
|||
|
|
padding: 16px;
|
|||
|
|
background: #f8f9fa;
|
|||
|
|
border-radius: 8px;
|
|||
|
|
border: 2px solid #007bff;
|
|||
|
|
`;
|
|||
|
|
|
|||
|
|
// Image preview
|
|||
|
|
const imagePreview = document.createElement('div');
|
|||
|
|
imagePreview.className = 'ui-edit-image-preview';
|
|||
|
|
imagePreview.style.cssText = `
|
|||
|
|
max-width: 100%;
|
|||
|
|
text-align: center;
|
|||
|
|
background: white;
|
|||
|
|
padding: 16px;
|
|||
|
|
border-radius: 6px;
|
|||
|
|
border: 1px solid #dee2e6;
|
|||
|
|
`;
|
|||
|
|
|
|||
|
|
// Parse markdown to extract image info
|
|||
|
|
const imageMatch = section.currentMarkdown.match(/!\[(.*?)\]\((.*?)\)/);
|
|||
|
|
if (imageMatch) {
|
|||
|
|
const [, altText, imageSrc] = imageMatch;
|
|||
|
|
const img = document.createElement('img');
|
|||
|
|
img.src = imageSrc;
|
|||
|
|
img.alt = altText;
|
|||
|
|
img.style.cssText = `
|
|||
|
|
max-width: 100%;
|
|||
|
|
max-height: 300px;
|
|||
|
|
border-radius: 4px;
|
|||
|
|
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
|||
|
|
`;
|
|||
|
|
imagePreview.appendChild(img);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Image controls
|
|||
|
|
const controlPanel = document.createElement('div');
|
|||
|
|
controlPanel.className = 'ui-edit-image-controls';
|
|||
|
|
controlPanel.style.cssText = `
|
|||
|
|
display: grid;
|
|||
|
|
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
|
|||
|
|
gap: 8px;
|
|||
|
|
`;
|
|||
|
|
|
|||
|
|
// Alt text editor
|
|||
|
|
const altTextContainer = document.createElement('div');
|
|||
|
|
altTextContainer.style.cssText = `grid-column: 1 / -1; margin-bottom: 8px;`;
|
|||
|
|
const altTextLabel = document.createElement('label');
|
|||
|
|
altTextLabel.textContent = 'Alt Text:';
|
|||
|
|
altTextLabel.style.cssText = `display: block; margin-bottom: 4px; font-weight: bold;`;
|
|||
|
|
|
|||
|
|
const altTextInput = document.createElement('input');
|
|||
|
|
altTextInput.type = 'text';
|
|||
|
|
altTextInput.value = imageMatch ? imageMatch[1] : '';
|
|||
|
|
altTextInput.style.cssText = `
|
|||
|
|
width: 100%;
|
|||
|
|
padding: 8px;
|
|||
|
|
border: 1px solid #ccc;
|
|||
|
|
border-radius: 4px;
|
|||
|
|
font-size: 14px;
|
|||
|
|
`;
|
|||
|
|
|
|||
|
|
// Add keyboard shortcuts to alt text input
|
|||
|
|
altTextInput.addEventListener('keydown', this.handleKeydown);
|
|||
|
|
|
|||
|
|
altTextContainer.appendChild(altTextLabel);
|
|||
|
|
altTextContainer.appendChild(altTextInput);
|
|||
|
|
|
|||
|
|
// Image manipulation buttons
|
|||
|
|
const buttons = [
|
|||
|
|
{ text: 'Replace Image', action: () => this.replaceImage(sectionId) },
|
|||
|
|
{ text: 'Resize', action: () => this.resizeImage(sectionId) },
|
|||
|
|
{ text: 'Add Caption', action: () => this.addImageCaption(sectionId) },
|
|||
|
|
{ text: 'Remove Image', action: () => this.removeImage(sectionId) }
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
buttons.forEach(({ text, action }) => {
|
|||
|
|
const btn = this.createButton(text, 'ui-edit-image-btn', action);
|
|||
|
|
btn.style.fontSize = '12px';
|
|||
|
|
btn.style.padding = '6px 12px';
|
|||
|
|
controlPanel.appendChild(btn);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// Standard editor controls
|
|||
|
|
const editorControls = document.createElement('div');
|
|||
|
|
editorControls.className = 'ui-edit-controls';
|
|||
|
|
editorControls.style.cssText = `
|
|||
|
|
display: flex;
|
|||
|
|
gap: 8px;
|
|||
|
|
justify-content: flex-end;
|
|||
|
|
margin-top: 12px;
|
|||
|
|
`;
|
|||
|
|
|
|||
|
|
const acceptBtn = this.createButton('✓ Accept', 'ui-edit-accept', (e) => {
|
|||
|
|
// Update alt text if changed
|
|||
|
|
if (imageMatch && altTextInput.value !== imageMatch[1]) {
|
|||
|
|
const newMarkdown = section.currentMarkdown.replace(
|
|||
|
|
/!\[(.*?)\]/,
|
|||
|
|
`![${altTextInput.value}]`
|
|||
|
|
);
|
|||
|
|
this.sectionManager.updateContent(sectionId, newMarkdown);
|
|||
|
|
}
|
|||
|
|
this.handleAccept(e);
|
|||
|
|
});
|
|||
|
|
const cancelBtn = this.createButton('✗ Cancel', 'ui-edit-cancel', this.handleCancel);
|
|||
|
|
const resetBtn = this.createButton('↺ Reset', 'ui-edit-reset', this.handleReset);
|
|||
|
|
|
|||
|
|
acceptBtn.style.background = '#28a745';
|
|||
|
|
cancelBtn.style.background = '#dc3545';
|
|||
|
|
resetBtn.style.background = '#fd7e14';
|
|||
|
|
|
|||
|
|
editorControls.appendChild(acceptBtn);
|
|||
|
|
editorControls.appendChild(cancelBtn);
|
|||
|
|
editorControls.appendChild(resetBtn);
|
|||
|
|
|
|||
|
|
// Assemble the editor
|
|||
|
|
editorContainer.appendChild(imagePreview);
|
|||
|
|
editorContainer.appendChild(altTextContainer);
|
|||
|
|
editorContainer.appendChild(controlPanel);
|
|||
|
|
editorContainer.appendChild(editorControls);
|
|||
|
|
|
|||
|
|
element.appendChild(editorContainer);
|
|||
|
|
altTextInput.focus();
|
|||
|
|
this.editingSections.add(sectionId);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Image manipulation methods
|
|||
|
|
*/
|
|||
|
|
replaceImage(sectionId) {
|
|||
|
|
const input = document.createElement('input');
|
|||
|
|
input.type = 'file';
|
|||
|
|
input.accept = 'image/*';
|
|||
|
|
input.addEventListener('change', (e) => {
|
|||
|
|
const file = e.target.files[0];
|
|||
|
|
if (file) {
|
|||
|
|
const reader = new FileReader();
|
|||
|
|
reader.onload = (event) => {
|
|||
|
|
const section = this.sectionManager.sections.get(sectionId);
|
|||
|
|
const imageMatch = section.currentMarkdown.match(/!\[(.*?)\]\((.*?)\)/);
|
|||
|
|
if (imageMatch) {
|
|||
|
|
const newMarkdown = section.currentMarkdown.replace(
|
|||
|
|
/!\[(.*?)\]\((.*?)\)/,
|
|||
|
|
`![${imageMatch[1]}](${event.target.result})`
|
|||
|
|
);
|
|||
|
|
this.sectionManager.updateContent(sectionId, newMarkdown);
|
|||
|
|
this.hideEditor(sectionId);
|
|||
|
|
setTimeout(() => this.showImageEditor(sectionId, this.sectionManager.sections.get(sectionId)), 100);
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
reader.readAsDataURL(file);
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
input.click();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
resizeImage(sectionId) {
|
|||
|
|
const section = this.sectionManager.sections.get(sectionId);
|
|||
|
|
const size = prompt('Enter image width (e.g., 300px, 50%, or leave empty for auto):', '');
|
|||
|
|
if (size !== null) {
|
|||
|
|
const imageMatch = section.currentMarkdown.match(/!\[(.*?)\]\((.*?)\)/);
|
|||
|
|
if (imageMatch) {
|
|||
|
|
const style = size ? ` style="width: ${size};"` : '';
|
|||
|
|
const newMarkdown = section.currentMarkdown.replace(
|
|||
|
|
/!\[(.*?)\]\((.*?)\)/,
|
|||
|
|
`<img src="${imageMatch[2]}" alt="${imageMatch[1]}"${style}>`
|
|||
|
|
);
|
|||
|
|
this.sectionManager.updateContent(sectionId, newMarkdown);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
addImageCaption(sectionId) {
|
|||
|
|
const section = this.sectionManager.sections.get(sectionId);
|
|||
|
|
const caption = prompt('Enter image caption:', '');
|
|||
|
|
if (caption) {
|
|||
|
|
const newMarkdown = section.currentMarkdown + `\n\n*${caption}*`;
|
|||
|
|
this.sectionManager.updateContent(sectionId, newMarkdown);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
removeImage(sectionId) {
|
|||
|
|
if (confirm('Are you sure you want to remove this image?')) {
|
|||
|
|
this.sectionManager.updateContent(sectionId, '');
|
|||
|
|
this.hideEditor(sectionId);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
createButton(text, className, handler) {
|
|||
|
|
const btn = document.createElement('button');
|
|||
|
|
btn.textContent = text;
|
|||
|
|
btn.className = className;
|
|||
|
|
btn.style.cssText = `
|
|||
|
|
background: #007bff;
|
|||
|
|
color: white;
|
|||
|
|
border: none;
|
|||
|
|
padding: 8px 16px;
|
|||
|
|
border-radius: 4px;
|
|||
|
|
cursor: pointer;
|
|||
|
|
font-size: 14px;
|
|||
|
|
`;
|
|||
|
|
btn.addEventListener('click', handler);
|
|||
|
|
return btn;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
handleAccept(event) {
|
|||
|
|
const sectionId = this.getCurrentEditingSectionId(event.target);
|
|||
|
|
if (sectionId) {
|
|||
|
|
this.sectionManager.acceptChanges(sectionId);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
handleCancel(event) {
|
|||
|
|
const sectionId = this.getCurrentEditingSectionId(event.target);
|
|||
|
|
if (sectionId) {
|
|||
|
|
this.sectionManager.cancelChanges(sectionId);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
handleReset(event) {
|
|||
|
|
const sectionId = this.getCurrentEditingSectionId(event.target);
|
|||
|
|
if (sectionId) {
|
|||
|
|
this.sectionManager.resetSection(sectionId);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
handleKeydown(event) {
|
|||
|
|
// Enhanced keyboard shortcuts for section editing
|
|||
|
|
const textarea = event.target.closest('textarea');
|
|||
|
|
if (!textarea) return;
|
|||
|
|
|
|||
|
|
// Find the section being edited
|
|||
|
|
const editorContainer = textarea.closest('.ui-edit-editor-container, .ui-edit-image-editor-container');
|
|||
|
|
if (!editorContainer) return;
|
|||
|
|
|
|||
|
|
const sectionElement = editorContainer.parentElement;
|
|||
|
|
const sectionId = sectionElement ? sectionElement.getAttribute('data-section-id') : null;
|
|||
|
|
if (!sectionId) return;
|
|||
|
|
|
|||
|
|
// Handle keyboard shortcuts
|
|||
|
|
if (event.ctrlKey || event.metaKey) {
|
|||
|
|
switch (event.key) {
|
|||
|
|
case 'Enter':
|
|||
|
|
event.preventDefault();
|
|||
|
|
this.sectionManager.acceptChanges(sectionId);
|
|||
|
|
debug('Keyboard shortcut: Ctrl+Enter - accepted changes for section ' + sectionId, 'KEYBOARD');
|
|||
|
|
break;
|
|||
|
|
case 'Escape':
|
|||
|
|
event.preventDefault();
|
|||
|
|
this.sectionManager.cancelChanges(sectionId);
|
|||
|
|
debug('Keyboard shortcut: Ctrl+Escape - cancelled changes for section ' + sectionId, 'KEYBOARD');
|
|||
|
|
break;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Handle plain Escape (without Ctrl)
|
|||
|
|
if (event.key === 'Escape' && !event.ctrlKey && !event.metaKey) {
|
|||
|
|
event.preventDefault();
|
|||
|
|
this.sectionManager.cancelChanges(sectionId);
|
|||
|
|
debug('Keyboard shortcut: Escape - cancelled changes for section ' + sectionId, 'KEYBOARD');
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
getCurrentEditingSectionId(button) {
|
|||
|
|
const editorContainer = button.closest('.ui-edit-editor-container');
|
|||
|
|
if (!editorContainer) return null;
|
|||
|
|
|
|||
|
|
const sectionElement = editorContainer.parentElement;
|
|||
|
|
return sectionElement ? sectionElement.getAttribute('data-section-id') : null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
hideEditor(sectionId) {
|
|||
|
|
const element = this.findSectionElement(sectionId);
|
|||
|
|
if (!element) return;
|
|||
|
|
|
|||
|
|
const section = this.sectionManager.sections.get(sectionId);
|
|||
|
|
if (section) {
|
|||
|
|
this.updateSectionContent(sectionId, section.currentMarkdown);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
this.editingSections.delete(sectionId);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
hideCurrentEditor() {
|
|||
|
|
this.editingSections.forEach(sectionId => {
|
|||
|
|
const element = this.findSectionElement(sectionId);
|
|||
|
|
if (element && element.querySelector('.ui-edit-editor-container')) {
|
|||
|
|
this.hideEditor(sectionId);
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
updateSectionContent(sectionId, content) {
|
|||
|
|
const element = this.findSectionElement(sectionId);
|
|||
|
|
if (!element) return;
|
|||
|
|
|
|||
|
|
if (typeof marked !== 'undefined') {
|
|||
|
|
const html = marked.parse(content);
|
|||
|
|
const htmlWithTargetBlank = html.replace(/<a href="([^"]*)"([^>]*)>/g, '<a href="$1" target="_blank"$2>');
|
|||
|
|
element.innerHTML = htmlWithTargetBlank;
|
|||
|
|
} else {
|
|||
|
|
element.innerHTML = `<p>${content}</p>`;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
this.setupSectionElement(element);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
findSectionElement(sectionId) {
|
|||
|
|
return this.container.querySelector(`[data-section-id="${sectionId}"]`);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
setupSectionElement(element) {
|
|||
|
|
element.className = 'ui-edit-section';
|
|||
|
|
element.style.cssText = `
|
|||
|
|
margin: 16px 0;
|
|||
|
|
padding: 12px;
|
|||
|
|
border-radius: 6px;
|
|||
|
|
cursor: pointer;
|
|||
|
|
transition: all 0.2s ease;
|
|||
|
|
border: 2px solid transparent;
|
|||
|
|
`;
|
|||
|
|
|
|||
|
|
element.removeEventListener('mouseenter', element._mouseenterHandler);
|
|||
|
|
element.removeEventListener('mouseleave', element._mouseleaveHandler);
|
|||
|
|
|
|||
|
|
element._mouseenterHandler = () => {
|
|||
|
|
element.style.backgroundColor = 'rgba(0, 0, 0, 0.02)';
|
|||
|
|
element.style.borderColor = 'rgba(0, 0, 0, 0.1)';
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
element._mouseleaveHandler = () => {
|
|||
|
|
element.style.backgroundColor = '';
|
|||
|
|
element.style.borderColor = 'transparent';
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
element.addEventListener('mouseenter', element._mouseenterHandler);
|
|||
|
|
element.addEventListener('mouseleave', element._mouseleaveHandler);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Setup auto-resize for textarea
|
|||
|
|
* @param {HTMLTextAreaElement} textarea - The textarea element
|
|||
|
|
*/
|
|||
|
|
setupAutoResize(textarea) {
|
|||
|
|
const autoResize = () => {
|
|||
|
|
const transition = textarea.style.transition;
|
|||
|
|
textarea.style.transition = 'none';
|
|||
|
|
|
|||
|
|
textarea.style.height = 'auto';
|
|||
|
|
const contentHeight = textarea.scrollHeight;
|
|||
|
|
const padding = 24;
|
|||
|
|
|
|||
|
|
const lineCount = textarea.value.split('\n').length;
|
|||
|
|
const minHeight = Math.max(60, lineCount * 24 + padding);
|
|||
|
|
const maxHeight = 360;
|
|||
|
|
|
|||
|
|
const newHeight = Math.max(60, Math.min(maxHeight, Math.max(minHeight, contentHeight + 4)));
|
|||
|
|
textarea.style.height = newHeight + 'px';
|
|||
|
|
|
|||
|
|
textarea.style.transition = transition;
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
textarea.addEventListener('input', autoResize);
|
|||
|
|
textarea.addEventListener('paste', () => setTimeout(autoResize, 10));
|
|||
|
|
|
|||
|
|
// Initial sizing
|
|||
|
|
setTimeout(autoResize, 20);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Create status panel for real-time status display
|
|||
|
|
* @returns {HTMLElement} Status panel element
|
|||
|
|
*/
|
|||
|
|
createStatusPanel() {
|
|||
|
|
const panel = document.createElement('div');
|
|||
|
|
panel.className = 'ui-edit-status-panel';
|
|||
|
|
panel.style.cssText = `
|
|||
|
|
position: fixed;
|
|||
|
|
top: 20px;
|
|||
|
|
right: 20px;
|
|||
|
|
background: white;
|
|||
|
|
border: 1px solid #ddd;
|
|||
|
|
border-radius: 8px;
|
|||
|
|
padding: 12px 16px;
|
|||
|
|
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
|||
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|||
|
|
font-size: 14px;
|
|||
|
|
z-index: 1000;
|
|||
|
|
min-width: 180px;
|
|||
|
|
`;
|
|||
|
|
|
|||
|
|
const statusText = document.createElement('div');
|
|||
|
|
statusText.className = 'ui-edit-status-text';
|
|||
|
|
statusText.textContent = 'Ready';
|
|||
|
|
|
|||
|
|
const detailsText = document.createElement('div');
|
|||
|
|
detailsText.className = 'ui-edit-status-details';
|
|||
|
|
detailsText.style.cssText = `
|
|||
|
|
font-size: 12px;
|
|||
|
|
color: #666;
|
|||
|
|
margin-top: 4px;
|
|||
|
|
`;
|
|||
|
|
|
|||
|
|
panel.appendChild(statusText);
|
|||
|
|
panel.appendChild(detailsText);
|
|||
|
|
|
|||
|
|
return panel;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Update status display with current status information
|
|||
|
|
* @param {Object} status - Status object from SectionManager
|
|||
|
|
*/
|
|||
|
|
updateStatusDisplay(status) {
|
|||
|
|
let statusPanel = document.querySelector('.ui-edit-status-panel');
|
|||
|
|
|
|||
|
|
if (!statusPanel) {
|
|||
|
|
statusPanel = this.createStatusPanel();
|
|||
|
|
document.body.appendChild(statusPanel);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const statusText = statusPanel.querySelector('.ui-edit-status-text');
|
|||
|
|
const detailsText = statusPanel.querySelector('.ui-edit-status-details');
|
|||
|
|
|
|||
|
|
// Update status text and color based on state
|
|||
|
|
switch (status.state) {
|
|||
|
|
case 'ready':
|
|||
|
|
statusText.textContent = '✓ Ready';
|
|||
|
|
statusText.style.color = '#28a745';
|
|||
|
|
break;
|
|||
|
|
case 'editing':
|
|||
|
|
statusText.textContent = '✏️ Editing';
|
|||
|
|
statusText.style.color = '#007bff';
|
|||
|
|
break;
|
|||
|
|
case 'modified':
|
|||
|
|
statusText.textContent = '⚠️ Modified';
|
|||
|
|
statusText.style.color = '#ffc107';
|
|||
|
|
break;
|
|||
|
|
default:
|
|||
|
|
statusText.textContent = 'Unknown';
|
|||
|
|
statusText.style.color = '#6c757d';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Update details
|
|||
|
|
const details = [];
|
|||
|
|
details.push(`${status.totalSections} sections`);
|
|||
|
|
|
|||
|
|
if (status.editingSections.length > 0) {
|
|||
|
|
details.push(`${status.editingSections.length} editing`);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (status.modifiedSections > 0) {
|
|||
|
|
details.push(`${status.modifiedSections} modified`);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
detailsText.textContent = details.join(' • ');
|
|||
|
|
|
|||
|
|
// Update timestamp
|
|||
|
|
const now = new Date().toLocaleTimeString();
|
|||
|
|
statusPanel.setAttribute('title', `Last update: ${now}`);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Main Editor Integration
|
|||
|
|
*/
|
|||
|
|
class MarkitectCleanEditor {
|
|||
|
|
constructor(markdownContent, containerElement, options = {}) {
|
|||
|
|
debug('10: MarkitectCleanEditor constructor called', 'EDITOR');
|
|||
|
|
|
|||
|
|
this.options = {
|
|||
|
|
theme: 'github',
|
|||
|
|
keyboardShortcuts: true,
|
|||
|
|
autosave: false,
|
|||
|
|
originalFilename: null,
|
|||
|
|
...options
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
debug('11: Creating SectionManager', 'EDITOR');
|
|||
|
|
this.sectionManager = new SectionManager();
|
|||
|
|
|
|||
|
|
debug('12: Creating DOMRenderer', 'EDITOR');
|
|||
|
|
this.domRenderer = new DOMRenderer(this.sectionManager, containerElement);
|
|||
|
|
this.originalMarkdown = markdownContent;
|
|||
|
|
|
|||
|
|
this.cleanMarkdownContent = options.cleanMarkdownContent || markdownContent;
|
|||
|
|
this.dogtagContent = options.dogtagContent || '';
|
|||
|
|
|
|||
|
|
debug('13: About to call initialize()', 'EDITOR');
|
|||
|
|
this.initialize();
|
|||
|
|
debug('14: initialize() completed', 'EDITOR');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
initialize() {
|
|||
|
|
try {
|
|||
|
|
debug('15: Starting initialize() - markdown length: ' + this.originalMarkdown.length, 'INIT');
|
|||
|
|
|
|||
|
|
const sections = this.sectionManager.createSectionsFromMarkdown(this.originalMarkdown);
|
|||
|
|
debug('16: Created ' + sections.length + ' sections', 'INIT');
|
|||
|
|
|
|||
|
|
// Mark the dogtag section as protected if we have a dogtag
|
|||
|
|
if (this.dogtagContent) {
|
|||
|
|
debug('17: Marking dogtag section', 'INIT');
|
|||
|
|
this.markDogtagSection(sections);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Mark base64 reference sections as protected
|
|||
|
|
debug('18: Marking base64 reference sections', 'INIT');
|
|||
|
|
this.markBase64ReferenceSections(sections);
|
|||
|
|
|
|||
|
|
console.log(`✓ Initialized clean editor with ${sections.length} sections`);
|
|||
|
|
|
|||
|
|
// Add global control panel
|
|||
|
|
debug('19: Adding global controls', 'INIT');
|
|||
|
|
this.addGlobalControls();
|
|||
|
|
|
|||
|
|
// Setup status tracking
|
|||
|
|
debug('19.5: Setting up status tracking', 'INIT');
|
|||
|
|
this.setupStatusTracking();
|
|||
|
|
|
|||
|
|
debug('20: Initialize completed successfully', 'INIT');
|
|||
|
|
return true;
|
|||
|
|
} catch (error) {
|
|||
|
|
debug('ERROR in initialize: ' + error.message, 'ERROR');
|
|||
|
|
console.error('Failed to initialize clean editor:', error);
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
markDogtagSection(sections) {
|
|||
|
|
// Find the section that contains the dogtag content
|
|||
|
|
const dogtagText = this.dogtagContent.trim();
|
|||
|
|
if (!dogtagText) return;
|
|||
|
|
|
|||
|
|
for (const section of sections) {
|
|||
|
|
if (section.currentMarkdown.includes(dogtagText)) {
|
|||
|
|
section.isDogtagSection = true;
|
|||
|
|
console.log('Marked dogtag section as protected:', section.id);
|
|||
|
|
break;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
markBase64ReferenceSections(sections) {
|
|||
|
|
// Find sections that contain base64 image references
|
|||
|
|
if (!window.markitectBase64References) return;
|
|||
|
|
|
|||
|
|
const refIds = Object.keys(window.markitectBase64References);
|
|||
|
|
if (refIds.length === 0) return;
|
|||
|
|
|
|||
|
|
for (const section of sections) {
|
|||
|
|
const markdown = section.currentMarkdown;
|
|||
|
|
|
|||
|
|
// Check if this section contains base64 reference syntax
|
|||
|
|
for (const refId of refIds) {
|
|||
|
|
if (markdown.includes(`[${refId}]:`)) {
|
|||
|
|
section.isBase64RefSection = true;
|
|||
|
|
console.log('Marked base64 reference section as protected:', section.id);
|
|||
|
|
break;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
addGlobalControls() {
|
|||
|
|
// Create a floating control panel
|
|||
|
|
const controlPanel = document.createElement('div');
|
|||
|
|
controlPanel.id = 'markitect-global-controls';
|
|||
|
|
controlPanel.style.cssText = `
|
|||
|
|
position: fixed;
|
|||
|
|
top: 20px;
|
|||
|
|
right: 20px;
|
|||
|
|
background: rgba(248, 249, 250, 0.95);
|
|||
|
|
border: 1px solid #dee2e6;
|
|||
|
|
border-radius: 8px;
|
|||
|
|
padding: 12px;
|
|||
|
|
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
|||
|
|
z-index: 1000;
|
|||
|
|
backdrop-filter: blur(8px);
|
|||
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|||
|
|
font-size: 14px;
|
|||
|
|
min-width: 200px;
|
|||
|
|
`;
|
|||
|
|
|
|||
|
|
const title = document.createElement('div');
|
|||
|
|
title.style.cssText = `
|
|||
|
|
font-weight: 600;
|
|||
|
|
margin-bottom: 8px;
|
|||
|
|
color: #495057;
|
|||
|
|
border-bottom: 1px solid #dee2e6;
|
|||
|
|
padding-bottom: 4px;
|
|||
|
|
`;
|
|||
|
|
title.textContent = 'Document Controls';
|
|||
|
|
|
|||
|
|
const buttonContainer = document.createElement('div');
|
|||
|
|
buttonContainer.style.cssText = `
|
|||
|
|
display: flex;
|
|||
|
|
flex-direction: column;
|
|||
|
|
gap: 6px;
|
|||
|
|
`;
|
|||
|
|
|
|||
|
|
// Save Document button
|
|||
|
|
const saveButton = document.createElement('button');
|
|||
|
|
saveButton.id = 'save-document';
|
|||
|
|
saveButton.textContent = '💾 Save Document';
|
|||
|
|
saveButton.style.cssText = `
|
|||
|
|
background: #28a745;
|
|||
|
|
color: white;
|
|||
|
|
border: none;
|
|||
|
|
padding: 8px 12px;
|
|||
|
|
border-radius: 4px;
|
|||
|
|
cursor: pointer;
|
|||
|
|
font-size: 13px;
|
|||
|
|
font-weight: 500;
|
|||
|
|
transition: background-color 0.2s;
|
|||
|
|
`;
|
|||
|
|
|
|||
|
|
// Reset All button
|
|||
|
|
const resetButton = document.createElement('button');
|
|||
|
|
resetButton.id = 'reset-all';
|
|||
|
|
resetButton.textContent = '🔄 Reset All';
|
|||
|
|
resetButton.style.cssText = `
|
|||
|
|
background: #ffc107;
|
|||
|
|
color: #212529;
|
|||
|
|
border: none;
|
|||
|
|
padding: 8px 12px;
|
|||
|
|
border-radius: 4px;
|
|||
|
|
cursor: pointer;
|
|||
|
|
font-size: 13px;
|
|||
|
|
font-weight: 500;
|
|||
|
|
transition: background-color 0.2s;
|
|||
|
|
`;
|
|||
|
|
|
|||
|
|
// Show Status button
|
|||
|
|
const statusButton = document.createElement('button');
|
|||
|
|
statusButton.id = 'show-status';
|
|||
|
|
statusButton.textContent = '📊 Show Status';
|
|||
|
|
statusButton.style.cssText = `
|
|||
|
|
background: #17a2b8;
|
|||
|
|
color: white;
|
|||
|
|
border: none;
|
|||
|
|
padding: 8px 12px;
|
|||
|
|
border-radius: 4px;
|
|||
|
|
cursor: pointer;
|
|||
|
|
font-size: 13px;
|
|||
|
|
font-weight: 500;
|
|||
|
|
transition: background-color 0.2s;
|
|||
|
|
`;
|
|||
|
|
|
|||
|
|
buttonContainer.appendChild(saveButton);
|
|||
|
|
buttonContainer.appendChild(resetButton);
|
|||
|
|
buttonContainer.appendChild(statusButton);
|
|||
|
|
|
|||
|
|
controlPanel.appendChild(title);
|
|||
|
|
controlPanel.appendChild(buttonContainer);
|
|||
|
|
|
|||
|
|
document.body.appendChild(controlPanel);
|
|||
|
|
|
|||
|
|
// Add event listeners
|
|||
|
|
document.getElementById('save-document').addEventListener('click', () => this.saveDocument());
|
|||
|
|
document.getElementById('reset-all').addEventListener('click', () => this.resetAllSections());
|
|||
|
|
document.getElementById('show-status').addEventListener('click', () => this.showStatus());
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
saveDocument() {
|
|||
|
|
try {
|
|||
|
|
const markdown = this.sectionManager.getDocumentMarkdown();
|
|||
|
|
|
|||
|
|
// Generate intelligent filename
|
|||
|
|
const filename = this.generateSaveFilename();
|
|||
|
|
|
|||
|
|
// Create a download link
|
|||
|
|
const blob = new Blob([markdown], { type: 'text/markdown' });
|
|||
|
|
const url = URL.createObjectURL(blob);
|
|||
|
|
const a = document.createElement('a');
|
|||
|
|
a.href = url;
|
|||
|
|
a.download = filename;
|
|||
|
|
document.body.appendChild(a);
|
|||
|
|
a.click();
|
|||
|
|
document.body.removeChild(a);
|
|||
|
|
URL.revokeObjectURL(url);
|
|||
|
|
|
|||
|
|
this.showMessage(`Document saved as ${filename}!`, 'success');
|
|||
|
|
console.log('Document saved:', filename, markdown.length, 'characters');
|
|||
|
|
} catch (error) {
|
|||
|
|
this.showMessage('Failed to save document: ' + error.message, 'error');
|
|||
|
|
console.error('Save failed:', error);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
resetAllSections() {
|
|||
|
|
if (!confirm('Reset all sections to their original content? This will lose all changes and cannot be undone.')) {
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
const sections = this.sectionManager.getAllSections();
|
|||
|
|
sections.forEach(section => {
|
|||
|
|
this.sectionManager.resetSection(section.id);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
this.showMessage('All sections reset to original content', 'info');
|
|||
|
|
console.log('All sections reset');
|
|||
|
|
} catch (error) {
|
|||
|
|
this.showMessage('Failed to reset sections: ' + error.message, 'error');
|
|||
|
|
console.error('Reset failed:', error);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
showStatus() {
|
|||
|
|
const sections = this.sectionManager.getSectionStatus();
|
|||
|
|
const totalSections = sections.length;
|
|||
|
|
const editedSections = sections.filter(s => s.hasChanges).length;
|
|||
|
|
const currentlyEditing = sections.filter(s => s.isEditing).length;
|
|||
|
|
|
|||
|
|
const statusHtml = `
|
|||
|
|
<h3>Document Status</h3>
|
|||
|
|
<p><strong>Total Sections:</strong> ${totalSections}</p>
|
|||
|
|
<p><strong>Modified Sections:</strong> ${editedSections}</p>
|
|||
|
|
<p><strong>Currently Editing:</strong> ${currentlyEditing}</p>
|
|||
|
|
<hr>
|
|||
|
|
<h4>Section Details:</h4>
|
|||
|
|
<ul>
|
|||
|
|
${sections.map(s => `
|
|||
|
|
<li>
|
|||
|
|
<strong>${s.id}</strong> (${s.type})
|
|||
|
|
- ${s.state}
|
|||
|
|
${s.hasChanges ? ' ✏️' : ''}
|
|||
|
|
${s.isEditing ? ' 🖊️' : ''}
|
|||
|
|
</li>
|
|||
|
|
`).join('')}
|
|||
|
|
</ul>
|
|||
|
|
`;
|
|||
|
|
|
|||
|
|
this.showModal('Document Status', statusHtml);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
showMessage(message, type = 'info') {
|
|||
|
|
const messageDiv = document.createElement('div');
|
|||
|
|
messageDiv.style.cssText = `
|
|||
|
|
position: fixed;
|
|||
|
|
top: 20px;
|
|||
|
|
left: 50%;
|
|||
|
|
transform: translateX(-50%);
|
|||
|
|
background: ${type === 'success' ? '#d4edda' : type === 'error' ? '#f8d7da' : '#d1ecf1'};
|
|||
|
|
color: ${type === 'success' ? '#155724' : type === 'error' ? '#721c24' : '#0c5460'};
|
|||
|
|
border: 1px solid ${type === 'success' ? '#c3e6cb' : type === 'error' ? '#f5c6cb' : '#bee5eb'};
|
|||
|
|
border-radius: 6px;
|
|||
|
|
padding: 12px 20px;
|
|||
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|||
|
|
font-size: 14px;
|
|||
|
|
z-index: 10001;
|
|||
|
|
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
|||
|
|
`;
|
|||
|
|
messageDiv.textContent = message;
|
|||
|
|
|
|||
|
|
document.body.appendChild(messageDiv);
|
|||
|
|
|
|||
|
|
setTimeout(() => {
|
|||
|
|
if (messageDiv.parentNode) {
|
|||
|
|
messageDiv.parentNode.removeChild(messageDiv);
|
|||
|
|
}
|
|||
|
|
}, 3000);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
showModal(title, content) {
|
|||
|
|
// Remove existing modal if present
|
|||
|
|
const existingModal = document.getElementById('markitect-modal');
|
|||
|
|
if (existingModal) {
|
|||
|
|
existingModal.remove();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const modalOverlay = document.createElement('div');
|
|||
|
|
modalOverlay.id = 'markitect-modal';
|
|||
|
|
modalOverlay.style.cssText = `
|
|||
|
|
position: fixed;
|
|||
|
|
top: 0;
|
|||
|
|
left: 0;
|
|||
|
|
width: 100%;
|
|||
|
|
height: 100%;
|
|||
|
|
background: rgba(0, 0, 0, 0.5);
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
justify-content: center;
|
|||
|
|
z-index: 10000;
|
|||
|
|
`;
|
|||
|
|
|
|||
|
|
const modal = document.createElement('div');
|
|||
|
|
modal.style.cssText = `
|
|||
|
|
background: white;
|
|||
|
|
border-radius: 8px;
|
|||
|
|
padding: 24px;
|
|||
|
|
max-width: 600px;
|
|||
|
|
max-height: 80vh;
|
|||
|
|
overflow-y: auto;
|
|||
|
|
box-shadow: 0 8px 32px rgba(0,0,0,0.3);
|
|||
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|||
|
|
`;
|
|||
|
|
|
|||
|
|
const closeBtn = document.createElement('button');
|
|||
|
|
closeBtn.textContent = '×';
|
|||
|
|
closeBtn.style.cssText = `
|
|||
|
|
float: right;
|
|||
|
|
background: none;
|
|||
|
|
border: none;
|
|||
|
|
font-size: 24px;
|
|||
|
|
cursor: pointer;
|
|||
|
|
color: #6c757d;
|
|||
|
|
margin: -8px -8px 0 0;
|
|||
|
|
`;
|
|||
|
|
|
|||
|
|
const modalContent = document.createElement('div');
|
|||
|
|
modalContent.innerHTML = `<h2>${title}</h2>${content}`;
|
|||
|
|
|
|||
|
|
function closeModal() {
|
|||
|
|
modalOverlay.remove();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
closeBtn.addEventListener('click', closeModal);
|
|||
|
|
modalOverlay.addEventListener('click', (e) => {
|
|||
|
|
if (e.target === modalOverlay) closeModal();
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
modal.appendChild(closeBtn);
|
|||
|
|
modal.appendChild(modalContent);
|
|||
|
|
modalOverlay.appendChild(modal);
|
|||
|
|
document.body.appendChild(modalOverlay);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
getDocumentMarkdown() {
|
|||
|
|
return this.sectionManager.getDocumentMarkdown();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
escapeRegex(str) {
|
|||
|
|
return str.replace(/[.*+?^${}()|[]\]/g, '\\\\$&');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
convertDataUrlToReference(markdown) {
|
|||
|
|
if (!window.markitectBase64References) {
|
|||
|
|
return markdown;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
let convertedMarkdown = markdown;
|
|||
|
|
|
|||
|
|
Object.entries(window.markitectBase64References).forEach(([refId, refData]) => {
|
|||
|
|
const dataUrl = refData.full_data_url;
|
|||
|
|
const escapedDataUrl = this.escapeRegex(dataUrl);
|
|||
|
|
const dataUrlPattern = new RegExp(`!\\[([^\\]]*)\\]\\(${escapedDataUrl}\\)`, 'g');
|
|||
|
|
convertedMarkdown = convertedMarkdown.replace(dataUrlPattern, `![$1][${refId}]`);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
return convertedMarkdown;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Setup real-time status tracking
|
|||
|
|
*/
|
|||
|
|
setupStatusTracking() {
|
|||
|
|
// Listen for status updates from SectionManager
|
|||
|
|
this.sectionManager.on('status-updated', (status) => {
|
|||
|
|
this.domRenderer.updateStatusDisplay(status);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// Start periodic status tracking
|
|||
|
|
this.sectionManager.startStatusTracking(2000); // Update every 2 seconds
|
|||
|
|
|
|||
|
|
// Initial status display
|
|||
|
|
this.sectionManager.updateGlobalStatus();
|
|||
|
|
|
|||
|
|
console.log('✓ Real-time status tracking initialized');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Generate intelligent save filename using 4-method fallback system
|
|||
|
|
* @returns {string} Generated filename with .md extension
|
|||
|
|
*/
|
|||
|
|
generateSaveFilename() {
|
|||
|
|
// Method 1: Original filename from options
|
|||
|
|
if (this.options.originalFilename) {
|
|||
|
|
return this.sanitizeFilename(this.options.originalFilename);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Method 2: Page title extraction
|
|||
|
|
const titleFilename = this.extractFilenameFromTitle();
|
|||
|
|
if (titleFilename) {
|
|||
|
|
return this.sanitizeFilename(titleFilename + '.md');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Method 3: URL pathname analysis
|
|||
|
|
const urlFilename = this.extractFilenameFromUrl();
|
|||
|
|
if (urlFilename) {
|
|||
|
|
return this.sanitizeFilename(urlFilename + '.md');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Method 4: First heading extraction
|
|||
|
|
const headingFilename = this.extractFilenameFromHeading();
|
|||
|
|
if (headingFilename) {
|
|||
|
|
return this.sanitizeFilename(headingFilename + '.md');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Method 5: Timestamp generation (fallback)
|
|||
|
|
return this.generateTimestampFilename();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Sanitize filename to be filesystem-safe
|
|||
|
|
* @param {string} filename - Raw filename
|
|||
|
|
* @returns {string} Sanitized filename
|
|||
|
|
*/
|
|||
|
|
sanitizeFilename(filename) {
|
|||
|
|
if (!filename) return 'document.md';
|
|||
|
|
|
|||
|
|
// Remove or replace filesystem-unsafe characters
|
|||
|
|
let sanitized = filename
|
|||
|
|
.replace(/[\/\\:*?"<>|]/g, '-') // Replace unsafe chars with dashes
|
|||
|
|
.replace(/\s+/g, '-') // Replace spaces with dashes
|
|||
|
|
.replace(/-+/g, '-') // Replace multiple dashes with single dash
|
|||
|
|
.replace(/^-|-$/g, '') // Remove leading/trailing dashes
|
|||
|
|
.trim();
|
|||
|
|
|
|||
|
|
// Ensure it ends with .md
|
|||
|
|
if (!sanitized.endsWith('.md')) {
|
|||
|
|
sanitized += '.md';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Ensure it's not empty
|
|||
|
|
if (sanitized === '.md') {
|
|||
|
|
sanitized = 'document.md';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return sanitized;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Extract filename from page title
|
|||
|
|
* @returns {string|null} Filename or null if not suitable
|
|||
|
|
*/
|
|||
|
|
extractFilenameFromTitle() {
|
|||
|
|
if (typeof document === 'undefined' || !document.title) {
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
let title = document.title.trim();
|
|||
|
|
|
|||
|
|
// Remove common website suffixes
|
|||
|
|
title = title
|
|||
|
|
.split('|')[0] // Remove " | Website"
|
|||
|
|
.split('-')[0] // Remove " - Website"
|
|||
|
|
.split('•')[0] // Remove " • Website"
|
|||
|
|
.trim();
|
|||
|
|
|
|||
|
|
if (title.length < 3 || title.length > 100) {
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return title;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Extract filename from URL pathname
|
|||
|
|
* @returns {string|null} Filename or null if not suitable
|
|||
|
|
*/
|
|||
|
|
extractFilenameFromUrl() {
|
|||
|
|
if (typeof window === 'undefined' || !window.location) {
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const pathname = window.location.pathname;
|
|||
|
|
if (!pathname || pathname === '/') {
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Get the last segment of the path
|
|||
|
|
const segments = pathname.split('/').filter(s => s.length > 0);
|
|||
|
|
if (segments.length === 0) {
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const lastSegment = segments[segments.length - 1];
|
|||
|
|
|
|||
|
|
// Remove file extensions
|
|||
|
|
const filenameBase = lastSegment.replace(/\.[^.]*$/, '');
|
|||
|
|
|
|||
|
|
if (filenameBase.length < 3 || filenameBase.length > 100) {
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return filenameBase;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Extract filename from first heading in markdown
|
|||
|
|
* @returns {string|null} Filename or null if no heading found
|
|||
|
|
*/
|
|||
|
|
extractFilenameFromHeading() {
|
|||
|
|
if (!this.originalMarkdown) {
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const lines = this.originalMarkdown.split('\n');
|
|||
|
|
|
|||
|
|
// Find first heading line
|
|||
|
|
for (const line of lines) {
|
|||
|
|
const trimmed = line.trim();
|
|||
|
|
if (/^#{1,6}\s/.test(trimmed)) {
|
|||
|
|
// Extract heading text (remove # symbols and trim)
|
|||
|
|
const headingText = trimmed.replace(/^#{1,6}\s*/, '').trim();
|
|||
|
|
|
|||
|
|
if (headingText.length >= 3 && headingText.length <= 100) {
|
|||
|
|
return headingText;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Generate timestamp-based filename as final fallback
|
|||
|
|
* @returns {string} Timestamp-based filename
|
|||
|
|
*/
|
|||
|
|
generateTimestampFilename() {
|
|||
|
|
const now = new Date();
|
|||
|
|
const timestamp = now.getFullYear().toString() +
|
|||
|
|
(now.getMonth() + 1).toString().padStart(2, '0') +
|
|||
|
|
now.getDate().toString().padStart(2, '0') + '-' +
|
|||
|
|
now.getHours().toString().padStart(2, '0') +
|
|||
|
|
now.getMinutes().toString().padStart(2, '0');
|
|||
|
|
|
|||
|
|
return `document-${timestamp}.md`;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Cleanup method to stop status tracking
|
|||
|
|
*/
|
|||
|
|
destroy() {
|
|||
|
|
if (this.sectionManager) {
|
|||
|
|
this.sectionManager.stopStatusTracking();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Initialize the clean editor system
|
|||
|
|
let markitectCleanEditor;
|
|||
|
|
|
|||
|
|
function initializeCleanEditor() {
|
|||
|
|
debug('1: initializeCleanEditor called', 'INIT');
|
|||
|
|
|
|||
|
|
const container = document.getElementById('markdown-content');
|
|||
|
|
if (!container) {
|
|||
|
|
debug('2: FAILED - Markdown content container not found', 'ERROR');
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
debug('3: Container found', 'INIT');
|
|||
|
|
|
|||
|
|
if (typeof window.MarkitectEditor === 'undefined') {
|
|||
|
|
debug('4: FAILED - MarkitectEditor not found', 'ERROR');
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
debug('5: MarkitectEditor found', 'INIT');
|
|||
|
|
|
|||
|
|
debug('6: Creating editor with content length: ' + markdownContentWithDogtag.length, 'INIT');
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
markitectCleanEditor = new window.MarkitectEditor.MarkitectCleanEditor(markdownContentWithDogtag, container, {
|
|||
|
|
cleanMarkdownContent: markdownContent,
|
|||
|
|
dogtagContent: dogtagContent
|
|||
|
|
});
|
|||
|
|
debug('7: Editor instance created', 'INIT');
|
|||
|
|
|
|||
|
|
window.markitectCleanEditor = markitectCleanEditor; // Make globally available
|
|||
|
|
debug('8: Editor made globally available', 'INIT');
|
|||
|
|
|
|||
|
|
const contentAfterInit = container.innerHTML;
|
|||
|
|
debug('9: Container has content: ' + (contentAfterInit.length > 0 ? 'YES (' + contentAfterInit.length + ' chars)' : 'NO'), 'INIT');
|
|||
|
|
|
|||
|
|
console.log('✅ Clean section editor initialized successfully');
|
|||
|
|
} catch (error) {
|
|||
|
|
debug('ERROR in editor creation: ' + error.message, 'ERROR');
|
|||
|
|
throw error;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Document scroll indicators
|
|||
|
|
function initializeScrollIndicators() {
|
|||
|
|
console.log('✅ Document scroll indicators initialized');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Export for module systems
|
|||
|
|
if (typeof module !== 'undefined' && module.exports) {
|
|||
|
|
module.exports = { EditState, SectionType, Section, SectionManager, DOMRenderer, MarkitectCleanEditor };
|
|||
|
|
global.EditState = EditState;
|
|||
|
|
global.SectionType = SectionType;
|
|||
|
|
global.Section = Section;
|
|||
|
|
} else {
|
|||
|
|
window.MarkitectEditor = { EditState, SectionType, Section, SectionManager, DOMRenderer, MarkitectCleanEditor };
|
|||
|
|
window.EditState = EditState;
|
|||
|
|
window.SectionType = SectionType;
|
|||
|
|
window.Section = Section;
|
|||
|
|
}
|