2025-11-02 10:01:11 +01:00
|
|
|
|
// 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'
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2025-11-03 19:32:39 +01:00
|
|
|
|
/**
|
|
|
|
|
|
* Reusable FloatingMenu Component
|
|
|
|
|
|
* Provides a consistent floating menu system for both text and image editing
|
|
|
|
|
|
*/
|
|
|
|
|
|
class FloatingMenu {
|
|
|
|
|
|
constructor(sectionId, type, renderer) {
|
|
|
|
|
|
this.sectionId = sectionId;
|
|
|
|
|
|
this.type = type; // 'text' or 'image'
|
|
|
|
|
|
this.renderer = renderer;
|
|
|
|
|
|
this.element = null;
|
|
|
|
|
|
this.dragHandle = null;
|
|
|
|
|
|
this.contentArea = null;
|
|
|
|
|
|
this.controlsArea = null;
|
|
|
|
|
|
this.isDragging = false;
|
|
|
|
|
|
this.isVisible = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Create and show the floating menu
|
|
|
|
|
|
* @param {HTMLElement} contentElement - Content to display in the menu
|
|
|
|
|
|
* @param {HTMLElement} controlsElement - Controls to display in the menu
|
|
|
|
|
|
* @returns {HTMLElement} The floating menu element
|
|
|
|
|
|
*/
|
|
|
|
|
|
show(contentElement, controlsElement) {
|
|
|
|
|
|
if (this.isVisible) this.hide();
|
|
|
|
|
|
|
|
|
|
|
|
const targetElement = this.renderer.findSectionElement(this.sectionId);
|
|
|
|
|
|
if (!targetElement) return null;
|
|
|
|
|
|
|
|
|
|
|
|
// Calculate positioning
|
|
|
|
|
|
const positioning = this._calculatePositioning(targetElement);
|
|
|
|
|
|
|
|
|
|
|
|
// Create menu container
|
|
|
|
|
|
this.element = this._createMenuContainer(positioning);
|
|
|
|
|
|
|
|
|
|
|
|
// Create drag handle
|
|
|
|
|
|
this.dragHandle = this._createDragHandle();
|
|
|
|
|
|
|
|
|
|
|
|
// Create content and controls areas
|
|
|
|
|
|
this.contentArea = this._createContentArea();
|
|
|
|
|
|
this.controlsArea = this._createControlsArea();
|
|
|
|
|
|
|
|
|
|
|
|
// Create wrapper for content that will take most of the space
|
|
|
|
|
|
const contentWrapper = document.createElement('div');
|
|
|
|
|
|
contentWrapper.style.cssText = `
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: 10px;
|
|
|
|
|
|
min-width: 0;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
`;
|
|
|
|
|
|
contentWrapper.appendChild(contentElement);
|
|
|
|
|
|
|
|
|
|
|
|
// Add content and controls to content area (side by side)
|
|
|
|
|
|
this.contentArea.appendChild(contentWrapper);
|
|
|
|
|
|
this.controlsArea.appendChild(controlsElement);
|
|
|
|
|
|
this.contentArea.appendChild(this.controlsArea);
|
|
|
|
|
|
|
|
|
|
|
|
// Assemble menu
|
|
|
|
|
|
this.element.appendChild(this.dragHandle);
|
|
|
|
|
|
this.element.appendChild(this.contentArea);
|
|
|
|
|
|
|
|
|
|
|
|
// Apply highlighting to target element
|
|
|
|
|
|
this._highlightTargetElement(targetElement);
|
|
|
|
|
|
|
|
|
|
|
|
// Make draggable
|
|
|
|
|
|
this._makeDraggable();
|
|
|
|
|
|
|
|
|
|
|
|
// Add to DOM
|
|
|
|
|
|
document.body.appendChild(this.element);
|
|
|
|
|
|
this.isVisible = true;
|
|
|
|
|
|
|
|
|
|
|
|
return this.element;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Hide and remove the floating menu
|
|
|
|
|
|
*/
|
|
|
|
|
|
hide() {
|
|
|
|
|
|
if (!this.isVisible) return;
|
|
|
|
|
|
|
|
|
|
|
|
// Stop editing state in the section manager
|
|
|
|
|
|
const section = this.renderer.sectionManager.sections.get(this.sectionId);
|
|
|
|
|
|
if (section && section.isEditing()) {
|
|
|
|
|
|
section.stopEditing();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Remove highlighting from target element
|
|
|
|
|
|
const targetElement = this.renderer.findSectionElement(this.sectionId);
|
|
|
|
|
|
if (targetElement) {
|
|
|
|
|
|
this._removeHighlighting(targetElement);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Remove section from editing set
|
|
|
|
|
|
this.renderer.editingSections.delete(this.sectionId);
|
|
|
|
|
|
|
|
|
|
|
|
// Remove from DOM
|
|
|
|
|
|
if (this.element && this.element.parentElement) {
|
|
|
|
|
|
this.element.remove();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
this.element = null;
|
|
|
|
|
|
this.dragHandle = null;
|
|
|
|
|
|
this.contentArea = null;
|
|
|
|
|
|
this.controlsArea = null;
|
|
|
|
|
|
this.isVisible = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Calculate optimal positioning for the floating menu
|
|
|
|
|
|
* @param {HTMLElement} targetElement - Element being edited
|
|
|
|
|
|
* @returns {Object} Positioning information
|
|
|
|
|
|
*/
|
|
|
|
|
|
_calculatePositioning(targetElement) {
|
|
|
|
|
|
const elementRect = targetElement.getBoundingClientRect();
|
|
|
|
|
|
const scrollX = window.pageXOffset || document.documentElement.scrollLeft;
|
|
|
|
|
|
const scrollY = window.pageYOffset || document.documentElement.scrollTop;
|
|
|
|
|
|
const viewportWidth = window.innerWidth;
|
|
|
|
|
|
|
|
|
|
|
|
// Calculate content width and positioning
|
|
|
|
|
|
const elementWidth = elementRect.width;
|
|
|
|
|
|
const controlsWidth = 180; // Width needed for controls on the right
|
|
|
|
|
|
const minMenuWidth = 400; // Increased minimum to accommodate side-by-side layout
|
|
|
|
|
|
const maxMenuWidth = Math.min(800, viewportWidth - 40);
|
|
|
|
|
|
|
|
|
|
|
|
// Always use integrated layout with controls on the right side of content
|
|
|
|
|
|
let menuWidth, menuLeft;
|
|
|
|
|
|
|
|
|
|
|
|
if (elementWidth + controlsWidth >= minMenuWidth && elementWidth + controlsWidth <= maxMenuWidth) {
|
|
|
|
|
|
// Use element width + controls width
|
|
|
|
|
|
menuWidth = elementWidth + controlsWidth;
|
|
|
|
|
|
} else if (elementWidth + controlsWidth > maxMenuWidth) {
|
|
|
|
|
|
// Use maximum width
|
|
|
|
|
|
menuWidth = maxMenuWidth;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// Use minimum width
|
|
|
|
|
|
menuWidth = minMenuWidth;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
menuLeft = elementRect.left + scrollX;
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
width: menuWidth,
|
|
|
|
|
|
left: menuLeft,
|
|
|
|
|
|
top: elementRect.top + scrollY,
|
|
|
|
|
|
height: Math.max(200, elementRect.height),
|
|
|
|
|
|
controlsWidth: controlsWidth
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Create the main menu container
|
|
|
|
|
|
* @param {Object} positioning - Positioning information
|
|
|
|
|
|
* @returns {HTMLElement} Menu container element
|
|
|
|
|
|
*/
|
|
|
|
|
|
_createMenuContainer(positioning) {
|
|
|
|
|
|
const container = document.createElement('div');
|
|
|
|
|
|
container.className = 'ui-edit-floating-menu';
|
|
|
|
|
|
container.dataset.sectionId = this.sectionId;
|
|
|
|
|
|
container.dataset.editType = this.type;
|
|
|
|
|
|
container.style.cssText = `
|
|
|
|
|
|
position: fixed;
|
|
|
|
|
|
top: ${positioning.top}px;
|
|
|
|
|
|
left: ${positioning.left}px;
|
|
|
|
|
|
width: ${positioning.width}px;
|
|
|
|
|
|
min-height: ${positioning.height}px;
|
|
|
|
|
|
max-height: 600px;
|
|
|
|
|
|
z-index: 10000;
|
|
|
|
|
|
background: rgba(255, 255, 255, 0.98);
|
|
|
|
|
|
border: 2px solid #007bff;
|
|
|
|
|
|
border-radius: 12px;
|
|
|
|
|
|
box-shadow: 0 8px 32px rgba(0, 123, 255, 0.25);
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
backdrop-filter: blur(10px);
|
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
|
|
// Store positioning info for controls layout
|
|
|
|
|
|
container._positioning = positioning;
|
|
|
|
|
|
|
|
|
|
|
|
return container;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Create the drag handle with X button
|
|
|
|
|
|
* @returns {HTMLElement} Drag handle element
|
|
|
|
|
|
*/
|
|
|
|
|
|
_createDragHandle() {
|
|
|
|
|
|
const handle = document.createElement('div');
|
|
|
|
|
|
handle.className = 'ui-edit-drag-handle';
|
|
|
|
|
|
handle.style.cssText = `
|
|
|
|
|
|
padding: 10px 15px;
|
|
|
|
|
|
background: #007bff;
|
|
|
|
|
|
color: white;
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
|
cursor: move;
|
|
|
|
|
|
user-select: none;
|
|
|
|
|
|
border-radius: 10px 10px 0 0;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
|
|
// Left side: icon and title
|
|
|
|
|
|
const leftContent = document.createElement('div');
|
|
|
|
|
|
leftContent.style.cssText = `
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
|
|
const icon = this.type === 'image' ? '🖼️' : '📝';
|
|
|
|
|
|
leftContent.innerHTML = `${icon} <span>Drag to Move • Editing ${this.type === 'image' ? 'Image' : 'Text'}</span>`;
|
|
|
|
|
|
|
|
|
|
|
|
// Right side: X button
|
|
|
|
|
|
const closeButton = document.createElement('button');
|
|
|
|
|
|
closeButton.className = 'ui-edit-close-button';
|
|
|
|
|
|
closeButton.innerHTML = '✕';
|
|
|
|
|
|
closeButton.style.cssText = `
|
|
|
|
|
|
background: none;
|
|
|
|
|
|
border: none;
|
|
|
|
|
|
color: white;
|
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
padding: 0;
|
|
|
|
|
|
width: 20px;
|
|
|
|
|
|
height: 20px;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
border-radius: 3px;
|
|
|
|
|
|
transition: background-color 0.2s ease;
|
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
|
|
// Hover effect for close button
|
|
|
|
|
|
closeButton.addEventListener('mouseenter', () => {
|
|
|
|
|
|
closeButton.style.backgroundColor = 'rgba(255, 255, 255, 0.2)';
|
|
|
|
|
|
});
|
|
|
|
|
|
closeButton.addEventListener('mouseleave', () => {
|
|
|
|
|
|
closeButton.style.backgroundColor = 'transparent';
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// Close functionality
|
|
|
|
|
|
closeButton.addEventListener('click', (e) => {
|
|
|
|
|
|
e.stopPropagation(); // Prevent dragging when clicking close
|
|
|
|
|
|
this.hide();
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
handle.appendChild(leftContent);
|
|
|
|
|
|
handle.appendChild(closeButton);
|
|
|
|
|
|
|
|
|
|
|
|
return handle;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Create the content area with horizontal layout
|
|
|
|
|
|
* @returns {HTMLElement} Content area element
|
|
|
|
|
|
*/
|
|
|
|
|
|
_createContentArea() {
|
|
|
|
|
|
const area = document.createElement('div');
|
|
|
|
|
|
area.className = 'ui-edit-content-area';
|
|
|
|
|
|
area.style.cssText = `
|
|
|
|
|
|
padding: 15px;
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: row;
|
|
|
|
|
|
gap: 15px;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
`;
|
|
|
|
|
|
return area;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Create the controls area for side-by-side layout
|
|
|
|
|
|
* @returns {HTMLElement} Controls area element
|
|
|
|
|
|
*/
|
|
|
|
|
|
_createControlsArea() {
|
|
|
|
|
|
const area = document.createElement('div');
|
|
|
|
|
|
area.className = 'ui-edit-controls-area';
|
|
|
|
|
|
area.style.cssText = `
|
|
|
|
|
|
width: 160px;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: 10px;
|
|
|
|
|
|
justify-content: flex-start;
|
|
|
|
|
|
align-items: stretch;
|
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
`;
|
|
|
|
|
|
return area;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Apply highlighting to the target element
|
|
|
|
|
|
* @param {HTMLElement} targetElement - Element to highlight
|
|
|
|
|
|
*/
|
|
|
|
|
|
_highlightTargetElement(targetElement) {
|
|
|
|
|
|
targetElement.style.outline = '3px solid #007bff';
|
|
|
|
|
|
targetElement.style.outlineOffset = '2px';
|
|
|
|
|
|
targetElement.style.backgroundColor = 'rgba(0, 123, 255, 0.08)';
|
|
|
|
|
|
targetElement.style.transition = 'all 0.2s ease';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Remove highlighting from the target element
|
|
|
|
|
|
* @param {HTMLElement} targetElement - Element to remove highlighting from
|
|
|
|
|
|
*/
|
|
|
|
|
|
_removeHighlighting(targetElement) {
|
|
|
|
|
|
targetElement.style.outline = '';
|
|
|
|
|
|
targetElement.style.outlineOffset = '';
|
|
|
|
|
|
targetElement.style.backgroundColor = '';
|
|
|
|
|
|
targetElement.style.transition = '';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Make the floating menu draggable
|
|
|
|
|
|
*/
|
|
|
|
|
|
_makeDraggable() {
|
|
|
|
|
|
let startX, startY, initialX, initialY;
|
|
|
|
|
|
|
|
|
|
|
|
this.dragHandle.addEventListener('mousedown', (e) => {
|
|
|
|
|
|
// Don't start dragging if clicking on the close button
|
|
|
|
|
|
if (e.target.closest('.ui-edit-close-button')) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
this.isDragging = true;
|
|
|
|
|
|
startX = e.clientX;
|
|
|
|
|
|
startY = e.clientY;
|
|
|
|
|
|
|
|
|
|
|
|
const rect = this.element.getBoundingClientRect();
|
|
|
|
|
|
initialX = rect.left;
|
|
|
|
|
|
initialY = rect.top;
|
|
|
|
|
|
|
|
|
|
|
|
this.element.style.cursor = 'grabbing';
|
|
|
|
|
|
this.dragHandle.style.cursor = 'grabbing';
|
|
|
|
|
|
|
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
document.addEventListener('mousemove', (e) => {
|
|
|
|
|
|
if (!this.isDragging) return;
|
|
|
|
|
|
|
|
|
|
|
|
const deltaX = e.clientX - startX;
|
|
|
|
|
|
const deltaY = e.clientY - startY;
|
|
|
|
|
|
|
|
|
|
|
|
const newX = initialX + deltaX;
|
|
|
|
|
|
const newY = initialY + deltaY;
|
|
|
|
|
|
|
|
|
|
|
|
// Keep within viewport bounds
|
|
|
|
|
|
const maxX = window.innerWidth - this.element.offsetWidth;
|
|
|
|
|
|
const maxY = window.innerHeight - this.element.offsetHeight;
|
|
|
|
|
|
|
|
|
|
|
|
this.element.style.left = Math.max(0, Math.min(newX, maxX)) + 'px';
|
|
|
|
|
|
this.element.style.top = Math.max(0, Math.min(newY, maxY)) + 'px';
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
document.addEventListener('mouseup', () => {
|
|
|
|
|
|
if (this.isDragging) {
|
|
|
|
|
|
this.isDragging = false;
|
|
|
|
|
|
this.element.style.cursor = 'move';
|
|
|
|
|
|
this.dragHandle.style.cursor = 'move';
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Global debug message storage - reset on page load
|
|
|
|
|
|
let debugMessages = [];
|
|
|
|
|
|
let debugPanelActive = false;
|
|
|
|
|
|
|
|
|
|
|
|
// Clear any messages that might have been captured during rendering
|
|
|
|
|
|
if (typeof window !== 'undefined') {
|
|
|
|
|
|
debugMessages = [];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-02 10:01:11 +01:00
|
|
|
|
function debug(message, category = 'INFO') {
|
2025-11-03 19:32:39 +01:00
|
|
|
|
const timestamp = new Date().toLocaleTimeString();
|
2025-11-02 10:01:11 +01:00
|
|
|
|
const prefix = `DEBUG ${category}:`;
|
2025-11-03 19:32:39 +01:00
|
|
|
|
const fullMessage = `[${timestamp}] ${prefix} ${message}`;
|
|
|
|
|
|
|
|
|
|
|
|
// Always store messages for the debug panel
|
|
|
|
|
|
debugMessages.push({
|
|
|
|
|
|
timestamp,
|
|
|
|
|
|
category,
|
|
|
|
|
|
message,
|
|
|
|
|
|
fullMessage
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// Keep only last 100 messages to prevent memory issues
|
|
|
|
|
|
if (debugMessages.length > 100) {
|
|
|
|
|
|
debugMessages = debugMessages.slice(-100);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Update debug panel if active
|
|
|
|
|
|
if (debugPanelActive) {
|
|
|
|
|
|
console.log('Debug panel is active, updating...'); // Debug log
|
|
|
|
|
|
if (window.updateDebugPanel) {
|
|
|
|
|
|
console.log('Calling window.updateDebugPanel'); // Debug log
|
|
|
|
|
|
window.updateDebugPanel();
|
|
|
|
|
|
} else {
|
|
|
|
|
|
console.log('window.updateDebugPanel not available'); // Debug log
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
console.log('Debug panel not active, debugPanelActive =', debugPanelActive); // Debug log
|
|
|
|
|
|
}
|
2025-11-02 10:01:11 +01:00
|
|
|
|
|
|
|
|
|
|
switch (DEBUG_MODE) {
|
|
|
|
|
|
case 'off':
|
2025-11-03 19:32:39 +01:00
|
|
|
|
// No debugging output to console
|
2025-11-02 10:01:11 +01:00
|
|
|
|
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');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-03 19:32:39 +01:00
|
|
|
|
// Global functions for debug panel
|
|
|
|
|
|
window.updateDebugPanel = function() {
|
|
|
|
|
|
// Find the DOMRenderer instance and call its updateDebugPanel method
|
|
|
|
|
|
if (window.currentDOMRenderer && typeof window.currentDOMRenderer.updateDebugPanel === 'function') {
|
|
|
|
|
|
window.currentDOMRenderer.updateDebugPanel();
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
window.clearDebugMessages = function() {
|
|
|
|
|
|
console.log('clearDebugMessages called, current messages:', debugMessages.length);
|
|
|
|
|
|
debugMessages = [];
|
|
|
|
|
|
console.log('Messages cleared, new length:', debugMessages.length);
|
|
|
|
|
|
if (debugPanelActive && window.currentDOMRenderer && typeof window.currentDOMRenderer.updateDebugPanel === 'function') {
|
|
|
|
|
|
console.log('Updating debug panel after clear');
|
|
|
|
|
|
window.currentDOMRenderer.updateDebugPanel();
|
|
|
|
|
|
} else {
|
|
|
|
|
|
console.log('Cannot update debug panel after clear:', {
|
|
|
|
|
|
debugPanelActive,
|
|
|
|
|
|
hasRenderer: !!window.currentDOMRenderer,
|
|
|
|
|
|
hasUpdateMethod: !!(window.currentDOMRenderer && window.currentDOMRenderer.updateDebugPanel)
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-11-02 10:01:11 +01:00
|
|
|
|
// Enums for clear state management (already defined above)
|
|
|
|
|
|
|
|
|
|
|
|
const SectionType = Object.freeze({
|
|
|
|
|
|
HEADING: 'heading',
|
|
|
|
|
|
PARAGRAPH: 'paragraph',
|
|
|
|
|
|
LIST: 'list',
|
|
|
|
|
|
CODE: 'code',
|
|
|
|
|
|
QUOTE: 'quote',
|
|
|
|
|
|
IMAGE: 'image',
|
2025-11-02 16:28:20 +01:00
|
|
|
|
TABLE: 'table',
|
|
|
|
|
|
HR: 'hr',
|
2025-11-02 10:01:11 +01:00
|
|
|
|
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();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-02 16:28:20 +01:00
|
|
|
|
/**
|
|
|
|
|
|
* Generate sophisticated section ID with hash-based algorithm
|
|
|
|
|
|
* @param {string} markdown - Section content
|
|
|
|
|
|
* @param {number} position - Position in document
|
|
|
|
|
|
* @param {string} strategy - ID generation strategy ('hash', 'timestamp', 'sequential', 'hierarchical')
|
|
|
|
|
|
* @param {string} parentId - Parent section ID for hierarchical strategy
|
|
|
|
|
|
* @returns {string} Generated section ID
|
|
|
|
|
|
*/
|
|
|
|
|
|
static generateId(markdown, position, strategy = 'hash', parentId = null) {
|
|
|
|
|
|
return this.generateIdWithStrategy(markdown, position, strategy, parentId);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Generate ID with specific strategy
|
|
|
|
|
|
* @param {string} markdown - Section content
|
|
|
|
|
|
* @param {number} position - Position in document
|
|
|
|
|
|
* @param {string} strategy - Generation strategy
|
|
|
|
|
|
* @param {string} parentId - Parent ID for hierarchical
|
|
|
|
|
|
* @returns {string} Generated ID
|
|
|
|
|
|
*/
|
|
|
|
|
|
static generateIdWithStrategy(markdown, position, strategy = 'hash', parentId = null) {
|
|
|
|
|
|
const sanitizedContent = this.sanitizeContentForId(markdown);
|
|
|
|
|
|
const normalizedContent = this.normalizeContentForHashing(sanitizedContent);
|
|
|
|
|
|
const sectionType = this.detectType(markdown);
|
|
|
|
|
|
|
|
|
|
|
|
switch (strategy) {
|
|
|
|
|
|
case 'timestamp':
|
|
|
|
|
|
return this.generateTimestampId(normalizedContent, position, sectionType);
|
|
|
|
|
|
case 'sequential':
|
|
|
|
|
|
return this.generateSequentialId(normalizedContent, position, sectionType);
|
|
|
|
|
|
case 'hierarchical':
|
|
|
|
|
|
return this.generateHierarchicalId(normalizedContent, position, parentId);
|
|
|
|
|
|
case 'hash':
|
|
|
|
|
|
default:
|
|
|
|
|
|
return this.generateAdvancedId(normalizedContent, position, sectionType);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Generate advanced hash-based ID with section type
|
|
|
|
|
|
* @param {string} content - Normalized content
|
|
|
|
|
|
* @param {number} position - Position
|
|
|
|
|
|
* @param {string} sectionType - Detected section type
|
|
|
|
|
|
* @returns {string} Advanced ID
|
|
|
|
|
|
*/
|
|
|
|
|
|
static generateAdvancedId(content, position, sectionType) {
|
|
|
|
|
|
const contentHash = this.generateCryptoHash(content);
|
|
|
|
|
|
const safeType = sectionType || 'paragraph';
|
|
|
|
|
|
const typePrefix = safeType.substring(0, 3); // First 3 chars of type
|
|
|
|
|
|
const positionHex = position.toString(16).padStart(2, '0');
|
|
|
|
|
|
|
|
|
|
|
|
return `section-${typePrefix}-${contentHash}-${positionHex}`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Generate cryptographic hash for content fingerprinting
|
|
|
|
|
|
* @param {string} content - Content to hash
|
|
|
|
|
|
* @returns {string} Hex hash string
|
|
|
|
|
|
*/
|
|
|
|
|
|
static generateCryptoHash(content) {
|
|
|
|
|
|
// Simple but effective hash function for browser compatibility
|
|
|
|
|
|
let hash = 0;
|
|
|
|
|
|
if (content.length === 0) return '00000000';
|
|
|
|
|
|
|
|
|
|
|
|
for (let i = 0; i < content.length; i++) {
|
|
|
|
|
|
const char = content.charCodeAt(i);
|
|
|
|
|
|
hash = ((hash << 5) - hash) + char;
|
|
|
|
|
|
hash = hash & hash; // Convert to 32-bit integer
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Convert to positive hex string
|
|
|
|
|
|
const hexHash = Math.abs(hash).toString(16).padStart(8, '0');
|
|
|
|
|
|
return hexHash.substring(0, 8);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Normalize content for consistent hashing
|
|
|
|
|
|
* @param {string} content - Raw content
|
|
|
|
|
|
* @returns {string} Normalized content
|
|
|
|
|
|
*/
|
|
|
|
|
|
static normalizeContentForHashing(content) {
|
|
|
|
|
|
if (!content || typeof content !== 'string') {
|
|
|
|
|
|
return '';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return content
|
|
|
|
|
|
.trim() // Remove leading/trailing whitespace
|
|
|
|
|
|
.replace(/\s+/g, ' ') // Normalize whitespace
|
|
|
|
|
|
.replace(/\r\n/g, '\n') // Normalize line endings
|
|
|
|
|
|
.toLowerCase(); // Case insensitive
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Sanitize content for safe ID generation
|
|
|
|
|
|
* @param {string} content - Raw content
|
|
|
|
|
|
* @returns {string} Sanitized content
|
|
|
|
|
|
*/
|
|
|
|
|
|
static sanitizeContentForId(content) {
|
|
|
|
|
|
if (!content || typeof content !== 'string') {
|
|
|
|
|
|
return '';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Remove potentially dangerous characters and HTML
|
|
|
|
|
|
return content
|
|
|
|
|
|
.replace(/<[^>]*>/g, '') // Remove HTML tags
|
|
|
|
|
|
.replace(/javascript:/gi, '') // Remove javascript: protocol
|
|
|
|
|
|
.replace(/[^\w\s\-_.#]/g, '') // Keep only safe characters
|
|
|
|
|
|
.trim();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Generate timestamp-based ID for temporal uniqueness
|
|
|
|
|
|
* @param {string} content - Content
|
|
|
|
|
|
* @param {number} position - Position
|
|
|
|
|
|
* @param {string} sectionType - Section type
|
|
|
|
|
|
* @returns {string} Timestamp-based ID
|
|
|
|
|
|
*/
|
|
|
|
|
|
static generateTimestampId(content, position = 0, sectionType = 'paragraph') {
|
|
|
|
|
|
const timestamp = Date.now().toString(36); // Base-36 timestamp
|
|
|
|
|
|
const contentSnippet = this.generateCryptoHash(content || '').substring(0, 4);
|
|
|
|
|
|
const safeType = sectionType || 'paragraph';
|
|
|
|
|
|
const typePrefix = safeType.substring(0, 3);
|
|
|
|
|
|
|
|
|
|
|
|
return `section-${typePrefix}-${contentSnippet}-${timestamp}`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Generate sequential ID
|
|
|
|
|
|
* @param {string} content - Content
|
|
|
|
|
|
* @param {number} position - Position
|
|
|
|
|
|
* @param {string} sectionType - Section type
|
|
|
|
|
|
* @returns {string} Sequential ID
|
|
|
|
|
|
*/
|
|
|
|
|
|
static generateSequentialId(content, position, sectionType = 'paragraph') {
|
|
|
|
|
|
const safeType = sectionType || 'paragraph';
|
|
|
|
|
|
const typePrefix = safeType.substring(0, 3);
|
|
|
|
|
|
const seqNumber = (position || 0).toString().padStart(3, '0');
|
|
|
|
|
|
const contentHash = this.generateCryptoHash(content || '').substring(0, 4);
|
|
|
|
|
|
|
|
|
|
|
|
return `section-${typePrefix}-seq${seqNumber}-${contentHash}`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Generate hierarchical ID for nested sections
|
|
|
|
|
|
* @param {string} content - Content
|
|
|
|
|
|
* @param {number} position - Position
|
|
|
|
|
|
* @param {string} parentId - Parent section ID
|
|
|
|
|
|
* @returns {string} Hierarchical ID
|
|
|
|
|
|
*/
|
|
|
|
|
|
static generateHierarchicalId(content, position, parentId = null) {
|
|
|
|
|
|
const contentHash = this.generateCryptoHash(content || '').substring(0, 6);
|
|
|
|
|
|
|
|
|
|
|
|
if (parentId) {
|
|
|
|
|
|
const childIndex = (position || 0).toString().padStart(2, '0');
|
|
|
|
|
|
return `${parentId}-child-${childIndex}-${contentHash}`;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
return `section-root-${position || 0}-${contentHash}`;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Detect ID collision
|
|
|
|
|
|
* @param {string} id - Proposed ID
|
|
|
|
|
|
* @param {Set} existingIds - Set of existing IDs
|
|
|
|
|
|
* @returns {boolean} True if collision detected
|
|
|
|
|
|
*/
|
|
|
|
|
|
static detectIdCollision(id, existingIds) {
|
|
|
|
|
|
return existingIds && existingIds.has(id);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Resolve ID collision by generating alternative
|
|
|
|
|
|
* @param {string} id - Colliding ID
|
|
|
|
|
|
* @param {Set} existingIds - Set of existing IDs
|
|
|
|
|
|
* @returns {string} Resolved unique ID
|
|
|
|
|
|
*/
|
|
|
|
|
|
static resolveIdCollision(id, existingIds) {
|
|
|
|
|
|
let counter = 1;
|
|
|
|
|
|
let resolvedId = `${id}-${counter}`;
|
|
|
|
|
|
|
|
|
|
|
|
while (existingIds.has(resolvedId)) {
|
|
|
|
|
|
counter++;
|
|
|
|
|
|
resolvedId = `${id}-${counter}`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return resolvedId;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Analyze section ID and extract metadata
|
|
|
|
|
|
* @param {string} id - Section ID to analyze
|
|
|
|
|
|
* @returns {Object} ID metadata
|
|
|
|
|
|
*/
|
|
|
|
|
|
static analyzeId(id) {
|
|
|
|
|
|
if (!id || !id.startsWith('section-')) {
|
|
|
|
|
|
return { id, valid: false };
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const parts = id.split('-');
|
|
|
|
|
|
const analysis = {
|
|
|
|
|
|
id,
|
|
|
|
|
|
valid: true,
|
|
|
|
|
|
prefix: parts[0], // 'section'
|
|
|
|
|
|
format: 'unknown'
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
if (parts.length >= 4) {
|
|
|
|
|
|
// Advanced format: section-{type}-{hash}-{position}
|
|
|
|
|
|
analysis.format = 'advanced';
|
|
|
|
|
|
analysis.type = parts[1];
|
|
|
|
|
|
analysis.hash = parts[2];
|
|
|
|
|
|
analysis.position = parseInt(parts[3], 16) || 0;
|
|
|
|
|
|
} else if (parts.length === 3 && parts[1] === 'root') {
|
|
|
|
|
|
// Hierarchical root format
|
|
|
|
|
|
analysis.format = 'hierarchical-root';
|
|
|
|
|
|
analysis.level = 'root';
|
|
|
|
|
|
analysis.position = parseInt(parts[2]) || 0;
|
|
|
|
|
|
} else if (id.includes('child')) {
|
|
|
|
|
|
// Hierarchical child format
|
|
|
|
|
|
analysis.format = 'hierarchical-child';
|
|
|
|
|
|
analysis.level = 'child';
|
|
|
|
|
|
} else if (id.includes('seq')) {
|
|
|
|
|
|
// Sequential format
|
|
|
|
|
|
analysis.format = 'sequential';
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// Basic or legacy format
|
|
|
|
|
|
analysis.format = 'basic';
|
|
|
|
|
|
analysis.hash = parts.slice(1).join('-');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return analysis;
|
2025-11-02 10:01:11 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
static detectType(markdown) {
|
2025-11-02 16:28:20 +01:00
|
|
|
|
if (!markdown || typeof markdown !== 'string') {
|
|
|
|
|
|
return SectionType.PARAGRAPH;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Don't trim the entire markdown - preserve leading indentation
|
|
|
|
|
|
const content = markdown.replace(/^\n+|\n+$/g, ''); // Only remove leading/trailing newlines
|
|
|
|
|
|
if (!content) {
|
|
|
|
|
|
return SectionType.PARAGRAPH;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const rawLines = content.split('\n');
|
|
|
|
|
|
const lines = rawLines.map(line => line.trim()).filter(line => line.length > 0);
|
|
|
|
|
|
if (lines.length === 0) {
|
|
|
|
|
|
return SectionType.PARAGRAPH;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// For other detection methods, use trimmed content
|
|
|
|
|
|
const trimmed = content.trim();
|
|
|
|
|
|
|
|
|
|
|
|
// Detection order matters - most specific first
|
|
|
|
|
|
|
|
|
|
|
|
// 1. Heading detection (must start with # and have space)
|
|
|
|
|
|
if (this.isHeading(trimmed)) {
|
|
|
|
|
|
return SectionType.HEADING;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 2. Code block detection
|
|
|
|
|
|
if (this.isCodeBlock(trimmed, lines, rawLines)) {
|
|
|
|
|
|
return SectionType.CODE;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 3. Table detection
|
|
|
|
|
|
if (this.isTable(trimmed, lines)) {
|
|
|
|
|
|
return SectionType.TABLE;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 4. Horizontal rule detection
|
|
|
|
|
|
if (this.isHorizontalRule(trimmed, lines)) {
|
|
|
|
|
|
return SectionType.HR;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 5. List detection
|
|
|
|
|
|
if (this.isList(trimmed, lines)) {
|
|
|
|
|
|
return SectionType.LIST;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 6. Quote detection
|
|
|
|
|
|
if (this.isQuote(trimmed, lines)) {
|
|
|
|
|
|
return SectionType.QUOTE;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 7. Image detection
|
|
|
|
|
|
if (this.isImage(trimmed)) {
|
|
|
|
|
|
return SectionType.IMAGE;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 8. Default to paragraph
|
2025-11-02 10:01:11 +01:00
|
|
|
|
return SectionType.PARAGRAPH;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-02 16:28:20 +01:00
|
|
|
|
/**
|
|
|
|
|
|
* Advanced type detection with confidence scores
|
|
|
|
|
|
* @param {string} markdown - Markdown content
|
|
|
|
|
|
* @returns {Object} Detection result with type, confidence, and alternatives
|
|
|
|
|
|
*/
|
|
|
|
|
|
static detectTypeWithConfidence(markdown) {
|
|
|
|
|
|
const scores = {
|
|
|
|
|
|
[SectionType.HEADING]: 0,
|
|
|
|
|
|
[SectionType.CODE]: 0,
|
|
|
|
|
|
[SectionType.TABLE]: 0,
|
|
|
|
|
|
[SectionType.HR]: 0,
|
|
|
|
|
|
[SectionType.LIST]: 0,
|
|
|
|
|
|
[SectionType.QUOTE]: 0,
|
|
|
|
|
|
[SectionType.IMAGE]: 0,
|
|
|
|
|
|
[SectionType.PARAGRAPH]: 0
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
if (!markdown || typeof markdown !== 'string') {
|
|
|
|
|
|
scores[SectionType.PARAGRAPH] = 1.0;
|
|
|
|
|
|
return this.formatDetectionResult(scores);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const content = markdown.replace(/^\n+|\n+$/g, '');
|
|
|
|
|
|
const trimmed = content.trim();
|
|
|
|
|
|
const rawLines = content.split('\n');
|
|
|
|
|
|
const lines = rawLines.map(line => line.trim()).filter(line => line.length > 0);
|
|
|
|
|
|
|
|
|
|
|
|
// Calculate confidence scores for each type
|
|
|
|
|
|
scores[SectionType.HEADING] = this.calculateHeadingScore(trimmed);
|
|
|
|
|
|
scores[SectionType.CODE] = this.calculateCodeScore(trimmed, lines, rawLines);
|
|
|
|
|
|
scores[SectionType.TABLE] = this.calculateTableScore(trimmed, lines);
|
|
|
|
|
|
scores[SectionType.HR] = this.calculateHRScore(trimmed, lines);
|
|
|
|
|
|
scores[SectionType.LIST] = this.calculateListScore(trimmed, lines);
|
|
|
|
|
|
scores[SectionType.QUOTE] = this.calculateQuoteScore(trimmed, lines);
|
|
|
|
|
|
scores[SectionType.IMAGE] = this.calculateImageScore(trimmed);
|
|
|
|
|
|
|
|
|
|
|
|
// Base paragraph score
|
|
|
|
|
|
scores[SectionType.PARAGRAPH] = 0.1;
|
|
|
|
|
|
|
|
|
|
|
|
return this.formatDetectionResult(scores);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Format detection result with primary type and alternatives
|
|
|
|
|
|
* @param {Object} scores - Confidence scores for each type
|
|
|
|
|
|
* @returns {Object} Formatted result
|
|
|
|
|
|
*/
|
|
|
|
|
|
static formatDetectionResult(scores) {
|
|
|
|
|
|
const sortedTypes = Object.entries(scores)
|
|
|
|
|
|
.sort(([,a], [,b]) => b - a)
|
|
|
|
|
|
.map(([type, score]) => ({ type, confidence: score }));
|
|
|
|
|
|
|
|
|
|
|
|
const primaryType = sortedTypes[0];
|
|
|
|
|
|
const alternatives = sortedTypes.slice(1, 4).filter(alt => alt.confidence > 0.1);
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
type: primaryType.type,
|
|
|
|
|
|
confidence: primaryType.confidence,
|
|
|
|
|
|
alternatives: alternatives
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Specific detection methods
|
|
|
|
|
|
|
|
|
|
|
|
static isHeading(trimmed) {
|
|
|
|
|
|
const headingPattern = /^#{1,6}\s+.+/;
|
|
|
|
|
|
return headingPattern.test(trimmed);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
static calculateHeadingScore(trimmed) {
|
|
|
|
|
|
if (/^#{1,6}\s+.+/.test(trimmed)) {
|
|
|
|
|
|
const hashCount = (trimmed.match(/^#+/) || [''])[0].length;
|
|
|
|
|
|
if (hashCount <= 6) {
|
|
|
|
|
|
return 0.95 - (hashCount - 1) * 0.05; // Higher score for lower level headings
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
static isCodeBlock(trimmed, lines, rawLines) {
|
|
|
|
|
|
// Fenced code blocks - check if starts with fence OR contains fence blocks
|
|
|
|
|
|
if (trimmed.startsWith('```') || trimmed.startsWith('~~~')) {
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Check for code blocks anywhere in the content for mixed content
|
|
|
|
|
|
if (trimmed.includes('```') || trimmed.includes('~~~')) {
|
|
|
|
|
|
const codeBlockPattern = /```[\s\S]*?```|~~~[\s\S]*?~~~/;
|
|
|
|
|
|
if (codeBlockPattern.test(trimmed)) {
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Indented code blocks (4+ spaces or 1+ tabs at start of every line)
|
|
|
|
|
|
if (rawLines && rawLines.length > 0) {
|
|
|
|
|
|
const nonEmptyRawLines = rawLines.filter(line => line.trim().length > 0);
|
|
|
|
|
|
const indentedLines = nonEmptyRawLines.filter(line =>
|
|
|
|
|
|
line.startsWith(' ') || line.startsWith('\t')
|
|
|
|
|
|
);
|
|
|
|
|
|
return indentedLines.length === nonEmptyRawLines.length && nonEmptyRawLines.length > 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
static calculateCodeScore(trimmed, lines, rawLines) {
|
|
|
|
|
|
let score = 0;
|
|
|
|
|
|
|
|
|
|
|
|
// Fenced code blocks at start
|
|
|
|
|
|
if (trimmed.startsWith('```') || trimmed.startsWith('~~~')) {
|
|
|
|
|
|
score = 0.95;
|
|
|
|
|
|
}
|
|
|
|
|
|
// Code blocks anywhere in content (mixed content)
|
|
|
|
|
|
else if (trimmed.includes('```') || trimmed.includes('~~~')) {
|
|
|
|
|
|
const codeBlockPattern = /```[\s\S]*?```|~~~[\s\S]*?~~~/;
|
|
|
|
|
|
if (codeBlockPattern.test(trimmed)) {
|
|
|
|
|
|
// Lower score for mixed content, but still significant
|
|
|
|
|
|
score = 0.7;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
// Indented code blocks
|
|
|
|
|
|
else if (rawLines && rawLines.length > 0) {
|
|
|
|
|
|
const nonEmptyRawLines = rawLines.filter(line => line.trim().length > 0);
|
|
|
|
|
|
const indentedLines = nonEmptyRawLines.filter(line =>
|
|
|
|
|
|
line.startsWith(' ') || line.startsWith('\t')
|
|
|
|
|
|
);
|
|
|
|
|
|
if (indentedLines.length === nonEmptyRawLines.length && nonEmptyRawLines.length > 0) {
|
|
|
|
|
|
score = 0.8;
|
|
|
|
|
|
} else if (indentedLines.length > 0) {
|
|
|
|
|
|
score = 0.3;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Boost score for code-like content
|
|
|
|
|
|
if (/[{}();]/.test(trimmed)) {
|
|
|
|
|
|
score += 0.1;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return Math.min(score, 1.0);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
static isTable(trimmed, lines) {
|
|
|
|
|
|
if (lines.length < 2) return false;
|
|
|
|
|
|
|
|
|
|
|
|
// Look for table structure with pipes
|
|
|
|
|
|
const hasTableSeparator = lines.some(line =>
|
|
|
|
|
|
/^\s*\|?.*?\|.*?\|?\s*$/.test(line) && line.includes('|')
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
const hasSeparatorRow = lines.some(line =>
|
|
|
|
|
|
/^\s*\|?\s*:?-+:?\s*(\|\s*:?-+:?\s*)*\|?\s*$/.test(line)
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
return hasTableSeparator && (hasSeparatorRow || this.looksLikeSimpleTable(lines));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
static looksLikeSimpleTable(lines) {
|
|
|
|
|
|
if (lines.length < 2) return false;
|
|
|
|
|
|
|
|
|
|
|
|
const pipeLines = lines.filter(line => line.includes('|'));
|
|
|
|
|
|
if (pipeLines.length < 2) return false;
|
|
|
|
|
|
|
|
|
|
|
|
// Check if lines have similar number of pipes
|
|
|
|
|
|
const pipeCounts = pipeLines.map(line => (line.match(/\|/g) || []).length);
|
|
|
|
|
|
const avgPipes = pipeCounts.reduce((a, b) => a + b, 0) / pipeCounts.length;
|
|
|
|
|
|
|
|
|
|
|
|
return pipeCounts.every(count => Math.abs(count - avgPipes) <= 1);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
static calculateTableScore(trimmed, lines) {
|
|
|
|
|
|
if (lines.length < 2) return 0;
|
|
|
|
|
|
|
|
|
|
|
|
let score = 0;
|
|
|
|
|
|
const pipeLines = lines.filter(line => line.includes('|'));
|
|
|
|
|
|
|
|
|
|
|
|
if (pipeLines.length >= 2) {
|
|
|
|
|
|
score = 0.3 + (pipeLines.length / lines.length) * 0.4;
|
|
|
|
|
|
|
|
|
|
|
|
// Boost for separator row
|
|
|
|
|
|
if (lines.some(line => /^\s*\|?\s*:?-+:?\s*(\|\s*:?-+:?\s*)*\|?\s*$/.test(line))) {
|
|
|
|
|
|
score += 0.3;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return Math.min(score, 1.0);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
static isHorizontalRule(trimmed, lines) {
|
|
|
|
|
|
if (lines.length !== 1) return false;
|
|
|
|
|
|
|
|
|
|
|
|
const line = lines[0];
|
|
|
|
|
|
|
|
|
|
|
|
// Three or more hyphens, asterisks, or underscores
|
|
|
|
|
|
const hrPatterns = [
|
|
|
|
|
|
/^-{3,}$/,
|
|
|
|
|
|
/^\*{3,}$/,
|
|
|
|
|
|
/^_{3,}$/,
|
|
|
|
|
|
/^- -( -)*$/,
|
|
|
|
|
|
/^\* \*( \*)*$/,
|
|
|
|
|
|
/^_ _( _)*$/
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
return hrPatterns.some(pattern => pattern.test(line));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
static calculateHRScore(trimmed, lines) {
|
|
|
|
|
|
if (lines.length === 1) {
|
|
|
|
|
|
const line = lines[0];
|
|
|
|
|
|
const hrPatterns = [
|
|
|
|
|
|
/^-{3,}$/, /^\*{3,}$/, /^_{3,}$/,
|
|
|
|
|
|
/^- -( -)*$/, /^\* \*( \*)*$/, /^_ _( _)*$/
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
if (hrPatterns.some(pattern => pattern.test(line))) {
|
|
|
|
|
|
return 0.9;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
static isList(trimmed, lines) {
|
|
|
|
|
|
if (lines.length === 0) return false;
|
|
|
|
|
|
|
|
|
|
|
|
const listItemPatterns = [
|
|
|
|
|
|
/^[-*+]\s+/, // Bullet lists
|
|
|
|
|
|
/^\d+[\.)]\s+/, // Numbered lists
|
|
|
|
|
|
/^[-*+]\s*\[([ x])\]\s+/ // Task lists
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
const listLines = lines.filter(line =>
|
|
|
|
|
|
listItemPatterns.some(pattern => pattern.test(line))
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
return listLines.length > 0 && listLines.length >= lines.length * 0.5;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
static calculateListScore(trimmed, lines) {
|
|
|
|
|
|
if (lines.length === 0) return 0;
|
|
|
|
|
|
|
|
|
|
|
|
const listItemPatterns = [
|
|
|
|
|
|
/^[-*+]\s+/,
|
|
|
|
|
|
/^\d+[\.)]\s+/,
|
|
|
|
|
|
/^[-*+]\s*\[([ x])\]\s+/
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
const listLines = lines.filter(line =>
|
|
|
|
|
|
listItemPatterns.some(pattern => pattern.test(line))
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
if (listLines.length === 0) return 0;
|
|
|
|
|
|
|
|
|
|
|
|
const ratio = listLines.length / lines.length;
|
|
|
|
|
|
return ratio * 0.9;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
static isQuote(trimmed, lines) {
|
|
|
|
|
|
if (lines.length === 0) return false;
|
|
|
|
|
|
|
|
|
|
|
|
const quoteLines = lines.filter(line => line.startsWith('>'));
|
|
|
|
|
|
return quoteLines.length > 0 && quoteLines.length >= lines.length * 0.5;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
static calculateQuoteScore(trimmed, lines) {
|
|
|
|
|
|
if (lines.length === 0) return 0;
|
|
|
|
|
|
|
|
|
|
|
|
const quoteLines = lines.filter(line => line.startsWith('>'));
|
|
|
|
|
|
if (quoteLines.length === 0) return 0;
|
|
|
|
|
|
|
|
|
|
|
|
const ratio = quoteLines.length / lines.length;
|
|
|
|
|
|
return ratio * 0.85;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
static isImage(trimmed) {
|
|
|
|
|
|
// Look for image syntax anywhere in the content
|
|
|
|
|
|
const imagePattern = /!\[.*?\]\([^)]+\)/;
|
|
|
|
|
|
return imagePattern.test(trimmed);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
static calculateImageScore(trimmed) {
|
|
|
|
|
|
const imagePattern = /!\[.*?\]\([^)]+\)/g;
|
|
|
|
|
|
const matches = trimmed.match(imagePattern);
|
|
|
|
|
|
|
|
|
|
|
|
if (matches) {
|
|
|
|
|
|
// Higher score for standalone images
|
|
|
|
|
|
const isStandalone = trimmed.replace(imagePattern, '').trim().length < 20;
|
|
|
|
|
|
return isStandalone ? 0.9 : 0.6;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-02 10:01:11 +01:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
2025-11-02 16:28:20 +01:00
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Re-detect and update the section type based on current content
|
|
|
|
|
|
* @param {string} content - Optional content to use for detection (defaults to currentMarkdown)
|
|
|
|
|
|
* @returns {string} The detected section type
|
|
|
|
|
|
*/
|
|
|
|
|
|
redetectType(content = null) {
|
|
|
|
|
|
const markdown = content || this.currentMarkdown;
|
|
|
|
|
|
const oldType = this.type;
|
|
|
|
|
|
this.type = Section.detectType(markdown);
|
|
|
|
|
|
|
|
|
|
|
|
// Emit type change event if type changed
|
|
|
|
|
|
if (oldType !== this.type) {
|
|
|
|
|
|
debug(`Section ${this.id} type changed from ${oldType} to ${this.type}`, 'TYPE_DETECTION');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return this.type;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Get detailed type detection information
|
|
|
|
|
|
* @param {string} content - Optional content to analyze
|
|
|
|
|
|
* @returns {Object} Detection result with confidence and alternatives
|
|
|
|
|
|
*/
|
|
|
|
|
|
getTypeAnalysis(content = null) {
|
|
|
|
|
|
const markdown = content || this.currentMarkdown;
|
|
|
|
|
|
return Section.detectTypeWithConfidence(markdown);
|
|
|
|
|
|
}
|
2025-11-02 10:01:11 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 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) {
|
2025-11-03 19:32:39 +01:00
|
|
|
|
debug('MANAGER: startEditing called for: ' + sectionId, 'MANAGER');
|
|
|
|
|
|
|
2025-11-02 10:01:11 +01:00
|
|
|
|
const section = this.sections.get(sectionId);
|
|
|
|
|
|
if (!section) {
|
|
|
|
|
|
throw new Error(`Section ${sectionId} not found`);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (section.isEditing()) {
|
2025-11-03 19:32:39 +01:00
|
|
|
|
debug('MANAGER: Section already in editing state: ' + sectionId, 'MANAGER');
|
2025-11-02 10:01:11 +01:00
|
|
|
|
return section.editingMarkdown;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-03 19:32:39 +01:00
|
|
|
|
debug('MANAGER: Starting edit for section: ' + sectionId, 'MANAGER');
|
2025-11-02 10:01:11 +01:00
|
|
|
|
const content = section.startEdit();
|
2025-11-03 19:32:39 +01:00
|
|
|
|
|
|
|
|
|
|
debug('MANAGER: About to emit edit-started event for: ' + sectionId, 'MANAGER');
|
2025-11-02 10:01:11 +01:00
|
|
|
|
this.emit('edit-started', { sectionId, content, section: section.getStatus() });
|
2025-11-03 19:32:39 +01:00
|
|
|
|
debug('MANAGER: Emitted edit-started event for: ' + sectionId, 'MANAGER');
|
|
|
|
|
|
|
2025-11-02 10:01:11 +01:00
|
|
|
|
return content;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
updateContent(sectionId, markdown) {
|
|
|
|
|
|
const section = this.sections.get(sectionId);
|
|
|
|
|
|
if (!section) {
|
|
|
|
|
|
throw new Error(`Section ${sectionId} not found`);
|
|
|
|
|
|
}
|
2025-11-02 16:28:20 +01:00
|
|
|
|
|
|
|
|
|
|
// Store old type for comparison
|
|
|
|
|
|
const oldType = section.type;
|
|
|
|
|
|
|
|
|
|
|
|
// Update content
|
2025-11-02 10:01:11 +01:00
|
|
|
|
section.updateContent(markdown);
|
2025-11-02 16:28:20 +01:00
|
|
|
|
|
|
|
|
|
|
// Automatically redetect type if content changed significantly
|
|
|
|
|
|
const newType = section.redetectType(markdown);
|
|
|
|
|
|
|
|
|
|
|
|
// Emit events
|
|
|
|
|
|
const eventData = {
|
|
|
|
|
|
sectionId,
|
|
|
|
|
|
markdown,
|
|
|
|
|
|
section: section.getStatus(),
|
|
|
|
|
|
typeChanged: oldType !== newType,
|
|
|
|
|
|
oldType,
|
|
|
|
|
|
newType
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
this.emit('content-updated', eventData);
|
|
|
|
|
|
|
|
|
|
|
|
if (oldType !== newType) {
|
|
|
|
|
|
this.emit('section-type-changed', {
|
|
|
|
|
|
sectionId,
|
|
|
|
|
|
oldType,
|
|
|
|
|
|
newType,
|
|
|
|
|
|
section: section.getStatus()
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
2025-11-02 10:01:11 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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());
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-02 16:28:20 +01:00
|
|
|
|
getDocumentStatus() {
|
|
|
|
|
|
const sections = Array.from(this.sections.values());
|
|
|
|
|
|
const editingSections = sections.filter(section => section.isEditing).length;
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
totalSections: sections.length,
|
|
|
|
|
|
editingSections: editingSections
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-02 10:01:11 +01:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-11-02 16:28:20 +01:00
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Bulk Operations for Concurrent Editing Sessions
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Accept changes for all currently editing sessions
|
|
|
|
|
|
* @returns {Array} Array of results from accept operations
|
|
|
|
|
|
*/
|
|
|
|
|
|
acceptAllEditingSessions() {
|
|
|
|
|
|
const editingSections = this.getAllSections().filter(section => section.isEditing());
|
|
|
|
|
|
const results = [];
|
|
|
|
|
|
|
|
|
|
|
|
editingSections.forEach(section => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const content = this.acceptChanges(section.id);
|
|
|
|
|
|
results.push({ sectionId: section.id, success: true, content });
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
results.push({ sectionId: section.id, success: false, error: error.message });
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
this.emit('bulk-accept-completed', { results, count: results.length });
|
|
|
|
|
|
return results;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Cancel changes for all currently editing sessions
|
|
|
|
|
|
* @returns {Array} Array of results from cancel operations
|
|
|
|
|
|
*/
|
|
|
|
|
|
cancelAllEditingSessions() {
|
|
|
|
|
|
const editingSections = this.getAllSections().filter(section => section.isEditing());
|
|
|
|
|
|
const results = [];
|
|
|
|
|
|
|
|
|
|
|
|
editingSections.forEach(section => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const content = this.cancelChanges(section.id);
|
|
|
|
|
|
results.push({ sectionId: section.id, success: true, content });
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
results.push({ sectionId: section.id, success: false, error: error.message });
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
this.emit('bulk-cancel-completed', { results, count: results.length });
|
|
|
|
|
|
return results;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Stop editing for all currently editing sessions (preserve changes as pending)
|
|
|
|
|
|
* @returns {Array} Array of results from stop operations
|
|
|
|
|
|
*/
|
|
|
|
|
|
stopAllEditingSessions() {
|
|
|
|
|
|
const editingSections = this.getAllSections().filter(section => section.isEditing());
|
|
|
|
|
|
const results = [];
|
|
|
|
|
|
|
|
|
|
|
|
editingSections.forEach(section => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const finalState = section.stopEditing();
|
|
|
|
|
|
results.push({ sectionId: section.id, success: true, finalState });
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
results.push({ sectionId: section.id, success: false, error: error.message });
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
this.emit('bulk-stop-completed', { results, count: results.length });
|
|
|
|
|
|
return results;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Get detailed status of all concurrent editing sessions
|
|
|
|
|
|
* @returns {Object} Detailed concurrent session information
|
|
|
|
|
|
*/
|
|
|
|
|
|
getConcurrentEditingStatus() {
|
|
|
|
|
|
const sections = this.getAllSections();
|
|
|
|
|
|
const editingSections = sections.filter(s => s.isEditing());
|
|
|
|
|
|
const modifiedSections = sections.filter(s => s.hasChanges());
|
|
|
|
|
|
const pendingSections = sections.filter(s => s.pendingMarkdown !== null);
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
totalSections: sections.length,
|
|
|
|
|
|
concurrentSessions: {
|
|
|
|
|
|
editing: editingSections.map(s => ({
|
|
|
|
|
|
id: s.id,
|
|
|
|
|
|
type: s.type,
|
|
|
|
|
|
hasUnsavedChanges: s.editingMarkdown !== s.currentMarkdown,
|
|
|
|
|
|
editingLength: s.editingMarkdown ? s.editingMarkdown.length : 0,
|
|
|
|
|
|
originalLength: s.originalMarkdown.length
|
|
|
|
|
|
})),
|
|
|
|
|
|
editingCount: editingSections.length,
|
|
|
|
|
|
pendingCount: pendingSections.length,
|
|
|
|
|
|
modifiedCount: modifiedSections.length
|
|
|
|
|
|
},
|
|
|
|
|
|
systemState: {
|
|
|
|
|
|
allowsConcurrentEditing: true,
|
|
|
|
|
|
maxConcurrentSessions: null, // No limit
|
|
|
|
|
|
activeSessionCount: editingSections.length
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Handle conflicts when multiple sections are edited simultaneously
|
|
|
|
|
|
* @param {string} sectionId - Primary section ID
|
|
|
|
|
|
* @param {Array} conflictingSectionIds - IDs of sections with potential conflicts
|
|
|
|
|
|
* @returns {Object} Conflict resolution result
|
|
|
|
|
|
*/
|
|
|
|
|
|
resolveEditingConflicts(sectionId, conflictingSectionIds = []) {
|
|
|
|
|
|
const primarySection = this.sections.get(sectionId);
|
|
|
|
|
|
if (!primarySection) {
|
|
|
|
|
|
throw new Error(`Primary section ${sectionId} not found`);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const conflicts = [];
|
|
|
|
|
|
const resolutions = [];
|
|
|
|
|
|
|
|
|
|
|
|
conflictingSectionIds.forEach(conflictId => {
|
|
|
|
|
|
const conflictSection = this.sections.get(conflictId);
|
|
|
|
|
|
if (conflictSection && conflictSection.isEditing()) {
|
|
|
|
|
|
// Check for content overlap or dependency conflicts
|
|
|
|
|
|
const hasConflict = this.detectContentConflict(primarySection, conflictSection);
|
|
|
|
|
|
|
|
|
|
|
|
if (hasConflict) {
|
|
|
|
|
|
conflicts.push({
|
|
|
|
|
|
sectionId: conflictId,
|
|
|
|
|
|
conflictType: 'content-overlap',
|
|
|
|
|
|
description: 'Sections may have overlapping content changes'
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// Auto-resolve by preserving both as separate sections
|
|
|
|
|
|
resolutions.push({
|
|
|
|
|
|
sectionId: conflictId,
|
|
|
|
|
|
resolution: 'preserve-separate',
|
|
|
|
|
|
action: 'maintain-current-state'
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
this.emit('conflicts-resolved', { sectionId, conflicts, resolutions });
|
|
|
|
|
|
return { conflicts, resolutions, resolved: true };
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Detect potential content conflicts between two sections
|
|
|
|
|
|
* @param {Section} section1 - First section
|
|
|
|
|
|
* @param {Section} section2 - Second section
|
|
|
|
|
|
* @returns {boolean} True if conflict detected
|
|
|
|
|
|
*/
|
|
|
|
|
|
detectContentConflict(section1, section2) {
|
|
|
|
|
|
// Simple conflict detection - check for similar content or headings
|
|
|
|
|
|
if (section1.type === 'heading' && section2.type === 'heading') {
|
|
|
|
|
|
const heading1 = section1.editingMarkdown || section1.currentMarkdown;
|
|
|
|
|
|
const heading2 = section2.editingMarkdown || section2.currentMarkdown;
|
|
|
|
|
|
|
|
|
|
|
|
// Check if headings are similar (potential duplicate)
|
|
|
|
|
|
const similarity = this.calculateContentSimilarity(heading1, heading2);
|
|
|
|
|
|
return similarity > 0.8; // 80% similarity threshold
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return false; // No conflicts detected for non-heading sections
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Calculate content similarity between two markdown strings
|
|
|
|
|
|
* @param {string} content1 - First content
|
|
|
|
|
|
* @param {string} content2 - Second content
|
|
|
|
|
|
* @returns {number} Similarity score (0-1)
|
|
|
|
|
|
*/
|
|
|
|
|
|
calculateContentSimilarity(content1, content2) {
|
|
|
|
|
|
if (!content1 || !content2) return 0;
|
|
|
|
|
|
|
|
|
|
|
|
const clean1 = content1.toLowerCase().replace(/[^a-z0-9\s]/g, '');
|
|
|
|
|
|
const clean2 = content2.toLowerCase().replace(/[^a-z0-9\s]/g, '');
|
|
|
|
|
|
|
|
|
|
|
|
const words1 = clean1.split(/\s+/).filter(w => w.length > 0);
|
|
|
|
|
|
const words2 = clean2.split(/\s+/).filter(w => w.length > 0);
|
|
|
|
|
|
|
|
|
|
|
|
if (words1.length === 0 && words2.length === 0) return 1;
|
|
|
|
|
|
if (words1.length === 0 || words2.length === 0) return 0;
|
|
|
|
|
|
|
|
|
|
|
|
const commonWords = words1.filter(word => words2.includes(word));
|
|
|
|
|
|
const totalWords = Math.max(words1.length, words2.length);
|
|
|
|
|
|
|
|
|
|
|
|
return commonWords.length / totalWords;
|
|
|
|
|
|
}
|
2025-11-02 10:01:11 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2025-11-02 16:28:20 +01:00
|
|
|
|
* DOM Renderer - Handles DOM interactions with Enhanced Event System
|
2025-11-02 10:01:11 +01:00
|
|
|
|
*/
|
|
|
|
|
|
class DOMRenderer {
|
|
|
|
|
|
constructor(sectionManager, container) {
|
|
|
|
|
|
this.sectionManager = sectionManager;
|
|
|
|
|
|
this.container = container;
|
|
|
|
|
|
this.editingSections = new Set();
|
2025-11-03 19:32:39 +01:00
|
|
|
|
this.currentFloatingMenu = null;
|
|
|
|
|
|
this.eventListenersAttached = false;
|
2025-11-02 10:01:11 +01:00
|
|
|
|
|
2025-11-02 16:28:20 +01:00
|
|
|
|
// Enhanced Event System - Track 6 event types
|
|
|
|
|
|
this.eventHistory = [];
|
|
|
|
|
|
this.eventStats = {
|
|
|
|
|
|
'section-click': 0,
|
|
|
|
|
|
'section-hover-enter': 0,
|
|
|
|
|
|
'section-hover-leave': 0,
|
|
|
|
|
|
'keyboard-shortcut': 0,
|
|
|
|
|
|
'section-drag-start': 0,
|
|
|
|
|
|
'section-drag-over': 0,
|
|
|
|
|
|
'section-drop': 0,
|
|
|
|
|
|
'section-focus-in': 0,
|
|
|
|
|
|
'section-focus-out': 0,
|
|
|
|
|
|
'section-context-menu': 0
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// Bind event handlers
|
2025-11-02 10:01:11 +01:00
|
|
|
|
this.handleSectionClick = this.handleSectionClick.bind(this);
|
2025-11-02 16:28:20 +01:00
|
|
|
|
this.handleSectionHover = this.handleSectionHover.bind(this);
|
2025-11-02 10:01:11 +01:00
|
|
|
|
this.handleAccept = this.handleAccept.bind(this);
|
|
|
|
|
|
this.handleCancel = this.handleCancel.bind(this);
|
|
|
|
|
|
this.handleReset = this.handleReset.bind(this);
|
|
|
|
|
|
this.handleKeydown = this.handleKeydown.bind(this);
|
2025-11-02 16:28:20 +01:00
|
|
|
|
this.handleDragStart = this.handleDragStart.bind(this);
|
|
|
|
|
|
this.handleDragOver = this.handleDragOver.bind(this);
|
|
|
|
|
|
this.handleDrop = this.handleDrop.bind(this);
|
|
|
|
|
|
this.handleFocus = this.handleFocus.bind(this);
|
|
|
|
|
|
this.handleContextMenu = this.handleContextMenu.bind(this);
|
2025-11-02 10:01:11 +01:00
|
|
|
|
|
|
|
|
|
|
this.setupEventListeners();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
setupEventListeners() {
|
|
|
|
|
|
this.sectionManager.on('sections-created', (data) => {
|
|
|
|
|
|
this.renderAllSections(data.sections);
|
|
|
|
|
|
});
|
|
|
|
|
|
this.sectionManager.on('edit-started', (data) => {
|
2025-11-03 19:32:39 +01:00
|
|
|
|
debug('EVENT: edit-started event received for: ' + data.sectionId, 'EVENT');
|
2025-11-02 10:01:11 +01:00
|
|
|
|
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) => {
|
2025-11-02 16:36:17 +01:00
|
|
|
|
this.hideEditor(data.sectionId);
|
|
|
|
|
|
this.updateSectionContent(data.sectionId, data.content);
|
2025-11-02 10:01:11 +01:00
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-02 16:28:20 +01:00
|
|
|
|
/**
|
|
|
|
|
|
* Track and log DOM events for analytics and debugging
|
|
|
|
|
|
* @param {string} eventType - Type of event
|
|
|
|
|
|
* @param {Object} eventData - Event data
|
|
|
|
|
|
*/
|
|
|
|
|
|
trackEvent(eventType, eventData) {
|
|
|
|
|
|
const timestamp = new Date().toISOString();
|
|
|
|
|
|
const eventRecord = {
|
|
|
|
|
|
type: eventType,
|
|
|
|
|
|
timestamp,
|
|
|
|
|
|
data: eventData
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// Add to history (keep last 1000 events)
|
|
|
|
|
|
this.eventHistory.push(eventRecord);
|
|
|
|
|
|
if (this.eventHistory.length > 1000) {
|
|
|
|
|
|
this.eventHistory.shift();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Update stats
|
|
|
|
|
|
if (this.eventStats.hasOwnProperty(eventType)) {
|
|
|
|
|
|
this.eventStats[eventType]++;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Emit to section manager for broader handling
|
|
|
|
|
|
this.sectionManager.emit(eventType, eventData);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Get event statistics for debugging
|
|
|
|
|
|
* @returns {Object} Event statistics
|
|
|
|
|
|
*/
|
|
|
|
|
|
getEventStats() {
|
|
|
|
|
|
return {
|
|
|
|
|
|
stats: { ...this.eventStats },
|
|
|
|
|
|
recentEvents: this.eventHistory.slice(-10),
|
|
|
|
|
|
totalEvents: this.eventHistory.length
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-02 10:01:11 +01:00
|
|
|
|
renderAllSections(sections) {
|
|
|
|
|
|
debug('21: renderAllSections called with ' + sections.length + ' sections', 'RENDER');
|
|
|
|
|
|
|
2025-11-02 16:28:20 +01:00
|
|
|
|
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');
|
|
|
|
|
|
|
2025-11-03 19:32:39 +01:00
|
|
|
|
// Enhanced DOM Event System - Setup all 6 event types with delegation (only once)
|
|
|
|
|
|
if (!this.eventListenersAttached) {
|
|
|
|
|
|
this.container.addEventListener('click', this.handleSectionClick);
|
|
|
|
|
|
this.container.addEventListener('mouseenter', this.handleSectionHover, true);
|
|
|
|
|
|
this.container.addEventListener('mouseleave', this.handleSectionHover, true);
|
|
|
|
|
|
this.container.addEventListener('keydown', this.handleKeydown);
|
|
|
|
|
|
this.container.addEventListener('dragstart', this.handleDragStart);
|
|
|
|
|
|
this.container.addEventListener('dragover', this.handleDragOver);
|
|
|
|
|
|
this.container.addEventListener('drop', this.handleDrop);
|
|
|
|
|
|
this.container.addEventListener('focusin', this.handleFocus);
|
|
|
|
|
|
this.container.addEventListener('focusout', this.handleFocus);
|
|
|
|
|
|
this.container.addEventListener('contextmenu', this.handleContextMenu);
|
|
|
|
|
|
|
|
|
|
|
|
this.eventListenersAttached = true;
|
|
|
|
|
|
debug('25: Enhanced event listeners attached for the first time', 'RENDER');
|
|
|
|
|
|
} else {
|
|
|
|
|
|
debug('25: Event listeners already attached, skipping', 'RENDER');
|
|
|
|
|
|
}
|
2025-11-02 16:28:20 +01:00
|
|
|
|
|
2025-11-03 19:32:39 +01:00
|
|
|
|
debug('25: Container content length: ' + this.container.innerHTML.length, 'RENDER');
|
2025-11-02 16:28:20 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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) {
|
2025-11-03 19:32:39 +01:00
|
|
|
|
debug('handleSectionClick: Click detected on target: ' + event.target.tagName + ' ' + (event.target.className || ''), 'CLICK');
|
2025-11-02 16:28:20 +01:00
|
|
|
|
|
|
|
|
|
|
// Don't handle clicks on form elements, buttons, or links
|
|
|
|
|
|
if (event.target.closest('textarea, button, input, a')) {
|
2025-11-03 19:32:39 +01:00
|
|
|
|
debug('handleSectionClick: Ignoring click on form element', 'CLICK');
|
2025-11-02 16:28:20 +01:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const sectionElement = event.target.closest('.ui-edit-section');
|
2025-11-03 19:32:39 +01:00
|
|
|
|
debug('handleSectionClick: Found section element: ' + (sectionElement ? sectionElement.getAttribute('data-section-id') : 'null'), 'CLICK');
|
2025-11-02 16:28:20 +01:00
|
|
|
|
if (!sectionElement) return;
|
|
|
|
|
|
|
|
|
|
|
|
const sectionId = sectionElement.getAttribute('data-section-id');
|
2025-11-03 19:32:39 +01:00
|
|
|
|
debug('handleSectionClick: Section ID: ' + sectionId, 'CLICK');
|
2025-11-02 16:28:20 +01:00
|
|
|
|
if (!sectionId) return;
|
|
|
|
|
|
|
|
|
|
|
|
// Track the click event
|
|
|
|
|
|
this.trackEvent('section-click', {
|
|
|
|
|
|
sectionId,
|
|
|
|
|
|
target: event.target.tagName.toLowerCase(),
|
|
|
|
|
|
event,
|
|
|
|
|
|
timestamp: Date.now()
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// Check if this section is already being edited
|
|
|
|
|
|
const section = this.sectionManager.sections.get(sectionId);
|
2025-11-03 19:32:39 +01:00
|
|
|
|
debug('handleSectionClick: Found section object: ' + (section ? 'YES' : 'NO'), 'CLICK');
|
|
|
|
|
|
|
2025-11-02 16:28:20 +01:00
|
|
|
|
if (section && section.isEditing()) {
|
2025-11-03 19:32:39 +01:00
|
|
|
|
debug('handleSectionClick: Section already being edited: ' + sectionId, 'CLICK');
|
2025-11-02 16:28:20 +01:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-03 19:32:39 +01:00
|
|
|
|
debug('handleSectionClick: About to start editing for section: ' + sectionId, 'CLICK');
|
|
|
|
|
|
|
2025-11-02 16:28:20 +01:00
|
|
|
|
try {
|
2025-11-03 19:32:39 +01:00
|
|
|
|
debug('handleSectionClick: Calling sectionManager.startEditing', 'CLICK');
|
2025-11-02 16:28:20 +01:00
|
|
|
|
this.sectionManager.startEditing(sectionId);
|
2025-11-03 19:32:39 +01:00
|
|
|
|
debug('handleSectionClick: Successfully called startEditing', 'CLICK');
|
2025-11-02 16:28:20 +01:00
|
|
|
|
} catch (error) {
|
2025-11-03 19:32:39 +01:00
|
|
|
|
debug('handleSectionClick: ERROR in startEditing: ' + error.message, 'ERROR');
|
2025-11-02 16:28:20 +01:00
|
|
|
|
console.error('Failed to start editing:', error);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Handle hover events (mouseenter/mouseleave)
|
|
|
|
|
|
* @param {Event} event - Mouse event
|
|
|
|
|
|
*/
|
|
|
|
|
|
handleSectionHover(event) {
|
|
|
|
|
|
const sectionElement = event.target.closest('.ui-edit-section');
|
|
|
|
|
|
if (!sectionElement) return;
|
|
|
|
|
|
|
|
|
|
|
|
const sectionId = sectionElement.getAttribute('data-section-id');
|
|
|
|
|
|
if (!sectionId) return;
|
|
|
|
|
|
|
|
|
|
|
|
const eventType = event.type === 'mouseenter' ? 'section-hover-enter' : 'section-hover-leave';
|
|
|
|
|
|
|
|
|
|
|
|
// Track the hover event
|
|
|
|
|
|
this.trackEvent(eventType, {
|
|
|
|
|
|
sectionId,
|
|
|
|
|
|
event,
|
|
|
|
|
|
timestamp: Date.now()
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Handle drag and drop events for section reordering
|
|
|
|
|
|
* @param {Event} event - Drag event
|
|
|
|
|
|
*/
|
|
|
|
|
|
handleDragStart(event) {
|
|
|
|
|
|
const sectionElement = event.target.closest('.ui-edit-section');
|
|
|
|
|
|
if (!sectionElement) return;
|
|
|
|
|
|
|
|
|
|
|
|
const sectionId = sectionElement.getAttribute('data-section-id');
|
|
|
|
|
|
if (!sectionId) return;
|
|
|
|
|
|
|
|
|
|
|
|
// Store the section ID being dragged
|
|
|
|
|
|
event.dataTransfer.setData('text/plain', sectionId);
|
|
|
|
|
|
event.dataTransfer.effectAllowed = 'move';
|
|
|
|
|
|
|
|
|
|
|
|
// Track the drag start event
|
|
|
|
|
|
this.trackEvent('section-drag-start', {
|
|
|
|
|
|
sectionId,
|
|
|
|
|
|
event,
|
|
|
|
|
|
timestamp: Date.now()
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
handleDragOver(event) {
|
|
|
|
|
|
const sectionElement = event.target.closest('.ui-edit-section');
|
|
|
|
|
|
if (!sectionElement) return;
|
|
|
|
|
|
|
|
|
|
|
|
const sectionId = sectionElement.getAttribute('data-section-id');
|
|
|
|
|
|
if (!sectionId) return;
|
|
|
|
|
|
|
|
|
|
|
|
event.preventDefault(); // Allow drop
|
|
|
|
|
|
event.dataTransfer.dropEffect = 'move';
|
|
|
|
|
|
|
|
|
|
|
|
// Track the drag over event
|
|
|
|
|
|
this.trackEvent('section-drag-over', {
|
|
|
|
|
|
sectionId,
|
|
|
|
|
|
event,
|
|
|
|
|
|
timestamp: Date.now()
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
handleDrop(event) {
|
|
|
|
|
|
const sectionElement = event.target.closest('.ui-edit-section');
|
|
|
|
|
|
if (!sectionElement) return;
|
|
|
|
|
|
|
|
|
|
|
|
const targetSectionId = sectionElement.getAttribute('data-section-id');
|
|
|
|
|
|
const draggedSectionId = event.dataTransfer.getData('text/plain');
|
|
|
|
|
|
|
|
|
|
|
|
if (!targetSectionId || !draggedSectionId || targetSectionId === draggedSectionId) return;
|
|
|
|
|
|
|
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
|
|
|
|
|
|
|
// Track the drop event
|
|
|
|
|
|
this.trackEvent('section-drop', {
|
|
|
|
|
|
draggedSectionId,
|
|
|
|
|
|
targetSectionId,
|
|
|
|
|
|
event,
|
|
|
|
|
|
timestamp: Date.now()
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// Emit section reorder event
|
|
|
|
|
|
this.sectionManager.emit('section-reorder', {
|
|
|
|
|
|
draggedSectionId,
|
|
|
|
|
|
targetSectionId,
|
|
|
|
|
|
action: 'reorder'
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Handle focus events for accessibility
|
|
|
|
|
|
* @param {Event} event - Focus event
|
|
|
|
|
|
*/
|
|
|
|
|
|
handleFocus(event) {
|
|
|
|
|
|
const sectionElement = event.target.closest('.ui-edit-section');
|
|
|
|
|
|
if (!sectionElement) return;
|
|
|
|
|
|
|
|
|
|
|
|
const sectionId = sectionElement.getAttribute('data-section-id');
|
|
|
|
|
|
if (!sectionId) return;
|
|
|
|
|
|
|
|
|
|
|
|
const eventType = event.type === 'focusin' ? 'section-focus-in' : 'section-focus-out';
|
|
|
|
|
|
|
|
|
|
|
|
// Track the focus event
|
|
|
|
|
|
this.trackEvent(eventType, {
|
|
|
|
|
|
sectionId,
|
|
|
|
|
|
target: event.target.tagName.toLowerCase(),
|
|
|
|
|
|
event,
|
|
|
|
|
|
timestamp: Date.now()
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Handle context menu events for right-click operations
|
|
|
|
|
|
* @param {Event} event - Context menu event
|
|
|
|
|
|
*/
|
|
|
|
|
|
handleContextMenu(event) {
|
|
|
|
|
|
const sectionElement = event.target.closest('.ui-edit-section');
|
|
|
|
|
|
if (!sectionElement) return;
|
|
|
|
|
|
|
|
|
|
|
|
const sectionId = sectionElement.getAttribute('data-section-id');
|
|
|
|
|
|
if (!sectionId) return;
|
2025-11-02 10:01:11 +01:00
|
|
|
|
|
2025-11-02 16:28:20 +01:00
|
|
|
|
// Track the context menu event
|
|
|
|
|
|
this.trackEvent('section-context-menu', {
|
|
|
|
|
|
sectionId,
|
|
|
|
|
|
x: event.clientX,
|
|
|
|
|
|
y: event.clientY,
|
|
|
|
|
|
event,
|
|
|
|
|
|
timestamp: Date.now()
|
2025-11-02 10:01:11 +01:00
|
|
|
|
});
|
|
|
|
|
|
|
2025-11-02 16:28:20 +01:00
|
|
|
|
// Prevent default context menu for sections
|
|
|
|
|
|
event.preventDefault();
|
2025-11-02 10:01:11 +01:00
|
|
|
|
|
2025-11-02 16:28:20 +01:00
|
|
|
|
// Show custom context menu
|
|
|
|
|
|
this.showSectionContextMenu(sectionId, event.clientX, event.clientY);
|
|
|
|
|
|
}
|
2025-11-02 10:01:11 +01:00
|
|
|
|
|
2025-11-02 16:28:20 +01:00
|
|
|
|
/**
|
|
|
|
|
|
* Show custom context menu for section operations
|
|
|
|
|
|
* @param {string} sectionId - Section ID
|
|
|
|
|
|
* @param {number} x - X coordinate
|
|
|
|
|
|
* @param {number} y - Y coordinate
|
|
|
|
|
|
*/
|
|
|
|
|
|
showSectionContextMenu(sectionId, x, y) {
|
|
|
|
|
|
// Remove existing context menu
|
|
|
|
|
|
const existingMenu = document.querySelector('.ui-edit-context-menu');
|
|
|
|
|
|
if (existingMenu) {
|
|
|
|
|
|
existingMenu.remove();
|
2025-11-02 10:01:11 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-02 16:28:20 +01:00
|
|
|
|
const menu = document.createElement('div');
|
|
|
|
|
|
menu.className = 'ui-edit-context-menu';
|
|
|
|
|
|
menu.style.cssText = `
|
|
|
|
|
|
position: fixed;
|
|
|
|
|
|
left: ${x}px;
|
|
|
|
|
|
top: ${y}px;
|
|
|
|
|
|
background: white;
|
|
|
|
|
|
border: 1px solid #ccc;
|
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
|
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
|
|
|
|
|
z-index: 10000;
|
|
|
|
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
min-width: 150px;
|
|
|
|
|
|
`;
|
2025-11-02 10:01:11 +01:00
|
|
|
|
|
2025-11-02 16:28:20 +01:00
|
|
|
|
const section = this.sectionManager.sections.get(sectionId);
|
|
|
|
|
|
const isEditing = section && section.isEditing();
|
|
|
|
|
|
|
|
|
|
|
|
const menuItems = [
|
|
|
|
|
|
{ text: 'Edit Section', action: () => this.sectionManager.startEditing(sectionId), disabled: isEditing },
|
|
|
|
|
|
{ text: 'Copy Section', action: () => this.copySectionToClipboard(sectionId), disabled: false },
|
|
|
|
|
|
{ text: 'Delete Section', action: () => this.deleteSection(sectionId), disabled: isEditing },
|
|
|
|
|
|
{ text: '—', action: null, disabled: false }, // Separator
|
|
|
|
|
|
{ text: 'Move Up', action: () => this.moveSectionUp(sectionId), disabled: false },
|
|
|
|
|
|
{ text: 'Move Down', action: () => this.moveSectionDown(sectionId), disabled: false }
|
|
|
|
|
|
];
|
2025-11-02 10:01:11 +01:00
|
|
|
|
|
2025-11-02 16:28:20 +01:00
|
|
|
|
menuItems.forEach(item => {
|
|
|
|
|
|
if (item.text === '—') {
|
|
|
|
|
|
const separator = document.createElement('div');
|
|
|
|
|
|
separator.style.cssText = 'height: 1px; background: #eee; margin: 4px 0;';
|
|
|
|
|
|
menu.appendChild(separator);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
const menuItem = document.createElement('div');
|
|
|
|
|
|
menuItem.textContent = item.text;
|
|
|
|
|
|
menuItem.style.cssText = `
|
|
|
|
|
|
padding: 8px 12px;
|
|
|
|
|
|
cursor: ${item.disabled ? 'not-allowed' : 'pointer'};
|
|
|
|
|
|
color: ${item.disabled ? '#999' : '#333'};
|
|
|
|
|
|
background: ${item.disabled ? 'transparent' : 'white'};
|
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
|
|
if (!item.disabled) {
|
|
|
|
|
|
menuItem.addEventListener('mouseenter', () => {
|
|
|
|
|
|
menuItem.style.background = '#f0f0f0';
|
|
|
|
|
|
});
|
|
|
|
|
|
menuItem.addEventListener('mouseleave', () => {
|
|
|
|
|
|
menuItem.style.background = 'white';
|
|
|
|
|
|
});
|
|
|
|
|
|
menuItem.addEventListener('click', () => {
|
|
|
|
|
|
item.action();
|
|
|
|
|
|
menu.remove();
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
2025-11-02 10:01:11 +01:00
|
|
|
|
|
2025-11-02 16:28:20 +01:00
|
|
|
|
menu.appendChild(menuItem);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
2025-11-02 10:01:11 +01:00
|
|
|
|
|
2025-11-02 16:28:20 +01:00
|
|
|
|
document.body.appendChild(menu);
|
2025-11-02 10:01:11 +01:00
|
|
|
|
|
2025-11-02 16:28:20 +01:00
|
|
|
|
// Remove menu when clicking elsewhere
|
|
|
|
|
|
const removeMenu = (e) => {
|
|
|
|
|
|
if (!menu.contains(e.target)) {
|
|
|
|
|
|
menu.remove();
|
|
|
|
|
|
document.removeEventListener('click', removeMenu);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
setTimeout(() => document.addEventListener('click', removeMenu), 100);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Context menu actions
|
|
|
|
|
|
*/
|
|
|
|
|
|
copySectionToClipboard(sectionId) {
|
2025-11-02 10:01:11 +01:00
|
|
|
|
const section = this.sectionManager.sections.get(sectionId);
|
2025-11-02 16:28:20 +01:00
|
|
|
|
if (section) {
|
|
|
|
|
|
navigator.clipboard.writeText(section.currentMarkdown).then(() => {
|
|
|
|
|
|
console.log('Section copied to clipboard');
|
|
|
|
|
|
});
|
2025-11-02 10:01:11 +01:00
|
|
|
|
}
|
2025-11-02 16:28:20 +01:00
|
|
|
|
}
|
2025-11-02 10:01:11 +01:00
|
|
|
|
|
2025-11-02 16:28:20 +01:00
|
|
|
|
deleteSection(sectionId) {
|
|
|
|
|
|
if (confirm('Are you sure you want to delete this section?')) {
|
|
|
|
|
|
this.sectionManager.sections.delete(sectionId);
|
|
|
|
|
|
const element = this.findSectionElement(sectionId);
|
|
|
|
|
|
if (element) {
|
|
|
|
|
|
element.remove();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
moveSectionUp(sectionId) {
|
|
|
|
|
|
const element = this.findSectionElement(sectionId);
|
|
|
|
|
|
if (element && element.previousElementSibling) {
|
|
|
|
|
|
element.parentNode.insertBefore(element, element.previousElementSibling);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
moveSectionDown(sectionId) {
|
|
|
|
|
|
const element = this.findSectionElement(sectionId);
|
|
|
|
|
|
if (element && element.nextElementSibling) {
|
|
|
|
|
|
element.parentNode.insertBefore(element.nextElementSibling, element);
|
2025-11-02 10:01:11 +01:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
showEditor(sectionId, content) {
|
2025-11-03 19:32:39 +01:00
|
|
|
|
debug('showEditor: called for section: ' + sectionId, 'EDITOR');
|
|
|
|
|
|
|
2025-11-02 10:01:11 +01:00
|
|
|
|
const element = this.findSectionElement(sectionId);
|
2025-11-03 19:32:39 +01:00
|
|
|
|
debug('showEditor: Found element: ' + (element ? 'YES' : 'NO'), 'EDITOR');
|
2025-11-02 10:01:11 +01:00
|
|
|
|
if (!element) return;
|
|
|
|
|
|
|
2025-11-03 19:32:39 +01:00
|
|
|
|
debug('showEditor: About to hide current editor', 'EDITOR');
|
2025-11-02 10:01:11 +01:00
|
|
|
|
this.hideCurrentEditor();
|
2025-11-03 19:32:39 +01:00
|
|
|
|
debug('showEditor: Hidden current editor', 'EDITOR');
|
2025-11-02 10:01:11 +01:00
|
|
|
|
|
|
|
|
|
|
const section = this.sectionManager.sections.get(sectionId);
|
|
|
|
|
|
const isImageSection = section && section.isImage();
|
|
|
|
|
|
|
|
|
|
|
|
if (isImageSection) {
|
|
|
|
|
|
this.showImageEditor(sectionId, section);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-03 19:32:39 +01:00
|
|
|
|
// Create content area for text editing (optimized for side-by-side layout)
|
|
|
|
|
|
const editorContent = document.createElement('div');
|
|
|
|
|
|
editorContent.className = 'ui-edit-editor-content';
|
|
|
|
|
|
editorContent.style.cssText = `
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: 12px;
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
min-width: 0;
|
|
|
|
|
|
`;
|
2025-11-02 10:01:11 +01:00
|
|
|
|
|
|
|
|
|
|
const textarea = document.createElement('textarea');
|
|
|
|
|
|
textarea.className = 'ui-edit-textarea ui-edit-textarea-main';
|
|
|
|
|
|
textarea.value = content;
|
|
|
|
|
|
textarea.style.cssText = `
|
|
|
|
|
|
flex: 1;
|
2025-11-03 19:32:39 +01:00
|
|
|
|
min-height: 200px;
|
|
|
|
|
|
width: 100%;
|
2025-11-02 10:01:11 +01:00
|
|
|
|
font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace;
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
line-height: 1.5;
|
|
|
|
|
|
padding: 12px;
|
2025-11-03 19:32:39 +01:00
|
|
|
|
border: 1px solid #ddd;
|
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
resize: none;
|
2025-11-02 10:01:11 +01:00
|
|
|
|
outline: none;
|
|
|
|
|
|
background: white;
|
|
|
|
|
|
color: #333;
|
2025-11-03 19:32:39 +01:00
|
|
|
|
box-sizing: border-box;
|
2025-11-02 10:01:11 +01:00
|
|
|
|
`;
|
|
|
|
|
|
|
2025-11-03 19:32:39 +01:00
|
|
|
|
editorContent.appendChild(textarea);
|
|
|
|
|
|
|
2025-11-02 10:01:11 +01:00
|
|
|
|
// Setup auto-resize functionality
|
|
|
|
|
|
this.setupAutoResize(textarea);
|
|
|
|
|
|
|
2025-11-03 19:32:39 +01:00
|
|
|
|
// Create controls (optimized for side panel layout)
|
2025-11-02 10:01:11 +01:00
|
|
|
|
const controls = document.createElement('div');
|
|
|
|
|
|
controls.className = 'ui-edit-controls';
|
|
|
|
|
|
controls.style.cssText = `
|
|
|
|
|
|
display: flex;
|
2025-11-03 19:32:39 +01:00
|
|
|
|
flex-direction: column;
|
2025-11-02 10:01:11 +01:00
|
|
|
|
gap: 8px;
|
2025-11-03 19:32:39 +01:00
|
|
|
|
width: 100%;
|
2025-11-02 10:01:11 +01:00
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
|
|
const acceptBtn = this.createButton('Accept', 'ui-edit-button-accept', this.handleAccept);
|
|
|
|
|
|
const cancelBtn = this.createButton('Cancel', 'ui-edit-button-cancel', this.handleCancel);
|
2025-11-03 19:32:39 +01:00
|
|
|
|
const resetBtn = this.createButton('Reset', 'ui-edit-button-reset', this.handleReset);
|
|
|
|
|
|
|
|
|
|
|
|
// Style buttons for side panel layout
|
|
|
|
|
|
[acceptBtn, cancelBtn, resetBtn].forEach(btn => {
|
|
|
|
|
|
btn.style.cssText += `
|
|
|
|
|
|
padding: 8px 12px;
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
|
border: none;
|
|
|
|
|
|
color: white;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
transition: all 0.2s ease;
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
`;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
acceptBtn.style.background = '#28a745';
|
|
|
|
|
|
cancelBtn.style.background = '#dc3545';
|
|
|
|
|
|
resetBtn.style.background = '#fd7e14';
|
2025-11-02 10:01:11 +01:00
|
|
|
|
|
|
|
|
|
|
controls.appendChild(acceptBtn);
|
|
|
|
|
|
controls.appendChild(cancelBtn);
|
2025-11-03 19:32:39 +01:00
|
|
|
|
controls.appendChild(resetBtn);
|
2025-11-02 10:01:11 +01:00
|
|
|
|
|
2025-11-03 19:32:39 +01:00
|
|
|
|
// Create unified floating menu using new component
|
|
|
|
|
|
this.currentFloatingMenu = new FloatingMenu(sectionId, 'text', this);
|
|
|
|
|
|
const floatingMenu = this.currentFloatingMenu.show(editorContent, controls);
|
|
|
|
|
|
if (!floatingMenu) return;
|
2025-11-02 10:01:11 +01:00
|
|
|
|
|
|
|
|
|
|
textarea.focus();
|
|
|
|
|
|
this.editingSections.add(sectionId);
|
|
|
|
|
|
|
|
|
|
|
|
textarea.addEventListener('input', () => {
|
|
|
|
|
|
this.sectionManager.updateContent(sectionId, textarea.value);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// Add keyboard shortcuts
|
|
|
|
|
|
textarea.addEventListener('keydown', this.handleKeydown);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-03 19:32:39 +01:00
|
|
|
|
/**
|
|
|
|
|
|
* Create a unified floating edit menu
|
|
|
|
|
|
* @param {string} sectionId - The section being edited
|
|
|
|
|
|
* @param {string} type - 'text' or 'image'
|
|
|
|
|
|
* @param {HTMLElement} contentElement - The content area to add to the menu
|
|
|
|
|
|
* @param {HTMLElement} controlsElement - The controls/buttons to add to the menu
|
|
|
|
|
|
* @returns {HTMLElement} The floating menu element
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
2025-11-02 10:01:11 +01:00
|
|
|
|
/**
|
|
|
|
|
|
* 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();
|
|
|
|
|
|
|
2025-11-02 16:57:30 +01:00
|
|
|
|
// Track staging state for this editor
|
|
|
|
|
|
const stagingState = {
|
|
|
|
|
|
originalMarkdown: section.currentMarkdown,
|
|
|
|
|
|
currentAltText: '',
|
|
|
|
|
|
currentImageSrc: '',
|
|
|
|
|
|
stagedImageSrc: null,
|
|
|
|
|
|
stagedAltText: null,
|
|
|
|
|
|
hasChanges: false
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-11-03 19:32:39 +01:00
|
|
|
|
// Create image editor content area (optimized for side-by-side layout)
|
|
|
|
|
|
const editorContent = document.createElement('div');
|
|
|
|
|
|
editorContent.className = 'ui-edit-image-content';
|
|
|
|
|
|
editorContent.style.cssText = `
|
2025-11-02 10:01:11 +01:00
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
2025-11-03 19:32:39 +01:00
|
|
|
|
gap: 15px;
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
min-width: 0;
|
2025-11-02 10:01:11 +01:00
|
|
|
|
`;
|
|
|
|
|
|
|
2025-11-02 16:57:30 +01:00
|
|
|
|
// Parse markdown to extract image info
|
|
|
|
|
|
const imageMatch = section.currentMarkdown.match(/!\[(.*?)\]\((.*?)\)/);
|
|
|
|
|
|
if (imageMatch) {
|
|
|
|
|
|
const [, altText, imageSrc] = imageMatch;
|
|
|
|
|
|
stagingState.currentAltText = altText;
|
|
|
|
|
|
stagingState.currentImageSrc = imageSrc;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-03 19:32:39 +01:00
|
|
|
|
// Image preview with drop zone for floating menu
|
2025-11-02 10:01:11 +01:00
|
|
|
|
const imagePreview = document.createElement('div');
|
|
|
|
|
|
imagePreview.className = 'ui-edit-image-preview';
|
|
|
|
|
|
imagePreview.style.cssText = `
|
2025-11-03 19:32:39 +01:00
|
|
|
|
width: 100%;
|
|
|
|
|
|
height: 180px;
|
2025-11-02 10:01:11 +01:00
|
|
|
|
text-align: center;
|
|
|
|
|
|
background: white;
|
2025-11-03 19:32:39 +01:00
|
|
|
|
padding: 12px;
|
|
|
|
|
|
border-radius: 8px;
|
2025-11-02 16:57:30 +01:00
|
|
|
|
border: 2px dashed #007bff;
|
|
|
|
|
|
transition: all 0.3s ease;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
position: relative;
|
2025-11-03 19:32:39 +01:00
|
|
|
|
box-sizing: border-box;
|
|
|
|
|
|
overflow: hidden;
|
2025-11-02 10:01:11 +01:00
|
|
|
|
`;
|
|
|
|
|
|
|
2025-11-02 16:57:30 +01:00
|
|
|
|
// Function to update image preview
|
|
|
|
|
|
const updateImagePreview = (imageSrc, altText) => {
|
|
|
|
|
|
imagePreview.innerHTML = '';
|
|
|
|
|
|
|
|
|
|
|
|
if (imageSrc) {
|
|
|
|
|
|
const img = document.createElement('img');
|
|
|
|
|
|
img.src = imageSrc;
|
|
|
|
|
|
img.alt = altText || '';
|
|
|
|
|
|
img.style.cssText = `
|
|
|
|
|
|
max-width: 100%;
|
|
|
|
|
|
max-height: 250px;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
|
|
|
|
|
`;
|
|
|
|
|
|
imagePreview.appendChild(img);
|
|
|
|
|
|
|
|
|
|
|
|
// Add overlay for drop zone
|
|
|
|
|
|
const overlay = document.createElement('div');
|
|
|
|
|
|
overlay.className = 'drop-overlay';
|
|
|
|
|
|
overlay.style.cssText = `
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
top: 0;
|
|
|
|
|
|
left: 0;
|
|
|
|
|
|
right: 0;
|
|
|
|
|
|
bottom: 0;
|
|
|
|
|
|
background: rgba(0, 123, 255, 0.1);
|
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
|
display: none;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
color: #007bff;
|
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
|
font-size: 18px;
|
|
|
|
|
|
`;
|
|
|
|
|
|
overlay.textContent = '📁 Drop new image here';
|
|
|
|
|
|
imagePreview.appendChild(overlay);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// Show drop zone placeholder
|
|
|
|
|
|
const placeholder = document.createElement('div');
|
|
|
|
|
|
placeholder.style.cssText = `
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
color: #6c757d;
|
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
|
`;
|
|
|
|
|
|
placeholder.innerHTML = `
|
|
|
|
|
|
<div style="font-size: 48px; margin-bottom: 12px;">📁</div>
|
|
|
|
|
|
<div style="margin-bottom: 8px;"><strong>Drop image here or click to select</strong></div>
|
|
|
|
|
|
<div style="font-size: 14px;">Supports JPG, PNG, GIF, WebP</div>
|
|
|
|
|
|
`;
|
|
|
|
|
|
imagePreview.appendChild(placeholder);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// Initialize preview
|
|
|
|
|
|
updateImagePreview(stagingState.currentImageSrc, stagingState.currentAltText);
|
|
|
|
|
|
|
|
|
|
|
|
// File input for image selection
|
|
|
|
|
|
const fileInput = document.createElement('input');
|
|
|
|
|
|
fileInput.type = 'file';
|
|
|
|
|
|
fileInput.accept = 'image/*';
|
|
|
|
|
|
fileInput.style.display = 'none';
|
|
|
|
|
|
|
|
|
|
|
|
// Function to handle image file selection
|
|
|
|
|
|
const handleImageFile = (file) => {
|
|
|
|
|
|
if (file && file.type.startsWith('image/')) {
|
|
|
|
|
|
const reader = new FileReader();
|
|
|
|
|
|
reader.onload = (event) => {
|
|
|
|
|
|
stagingState.stagedImageSrc = event.target.result;
|
|
|
|
|
|
stagingState.hasChanges = true;
|
|
|
|
|
|
updateImagePreview(stagingState.stagedImageSrc, altTextInput.value);
|
|
|
|
|
|
updateChangeIndicator();
|
|
|
|
|
|
};
|
|
|
|
|
|
reader.readAsDataURL(file);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// Drag and drop functionality
|
|
|
|
|
|
imagePreview.addEventListener('dragover', (e) => {
|
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
imagePreview.style.borderColor = '#28a745';
|
|
|
|
|
|
imagePreview.style.backgroundColor = '#f8fff8';
|
|
|
|
|
|
const overlay = imagePreview.querySelector('.drop-overlay');
|
|
|
|
|
|
if (overlay) overlay.style.display = 'flex';
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
imagePreview.addEventListener('dragleave', (e) => {
|
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
imagePreview.style.borderColor = '#007bff';
|
|
|
|
|
|
imagePreview.style.backgroundColor = 'white';
|
|
|
|
|
|
const overlay = imagePreview.querySelector('.drop-overlay');
|
|
|
|
|
|
if (overlay) overlay.style.display = 'none';
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
imagePreview.addEventListener('drop', (e) => {
|
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
imagePreview.style.borderColor = '#007bff';
|
|
|
|
|
|
imagePreview.style.backgroundColor = 'white';
|
|
|
|
|
|
const overlay = imagePreview.querySelector('.drop-overlay');
|
|
|
|
|
|
if (overlay) overlay.style.display = 'none';
|
|
|
|
|
|
|
|
|
|
|
|
const files = e.dataTransfer.files;
|
|
|
|
|
|
if (files.length > 0) {
|
|
|
|
|
|
handleImageFile(files[0]);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// Click to select file
|
|
|
|
|
|
imagePreview.addEventListener('click', () => {
|
|
|
|
|
|
fileInput.click();
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
fileInput.addEventListener('change', (e) => {
|
|
|
|
|
|
if (e.target.files.length > 0) {
|
|
|
|
|
|
handleImageFile(e.target.files[0]);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
2025-11-02 10:01:11 +01:00
|
|
|
|
|
2025-11-03 19:32:39 +01:00
|
|
|
|
// Alt text editor for floating menu
|
2025-11-02 10:01:11 +01:00
|
|
|
|
const altTextContainer = document.createElement('div');
|
2025-11-03 19:32:39 +01:00
|
|
|
|
altTextContainer.className = 'ui-edit-alt-text-container';
|
|
|
|
|
|
altTextContainer.style.cssText = `
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
`;
|
|
|
|
|
|
|
2025-11-02 10:01:11 +01:00
|
|
|
|
const altTextLabel = document.createElement('label');
|
2025-11-03 19:32:39 +01:00
|
|
|
|
altTextLabel.textContent = 'Alt Text Description:';
|
|
|
|
|
|
altTextLabel.style.cssText = `
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
color: #333;
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
`;
|
2025-11-02 10:01:11 +01:00
|
|
|
|
|
|
|
|
|
|
const altTextInput = document.createElement('input');
|
|
|
|
|
|
altTextInput.type = 'text';
|
2025-11-02 16:57:30 +01:00
|
|
|
|
altTextInput.value = stagingState.currentAltText;
|
2025-11-02 10:01:11 +01:00
|
|
|
|
altTextInput.style.cssText = `
|
|
|
|
|
|
width: 100%;
|
2025-11-03 19:32:39 +01:00
|
|
|
|
padding: 10px 12px;
|
|
|
|
|
|
border: 1px solid #ddd;
|
|
|
|
|
|
border-radius: 6px;
|
2025-11-02 10:01:11 +01:00
|
|
|
|
font-size: 14px;
|
2025-11-03 19:32:39 +01:00
|
|
|
|
box-sizing: border-box;
|
|
|
|
|
|
outline: none;
|
|
|
|
|
|
transition: border-color 0.2s ease;
|
2025-11-02 10:01:11 +01:00
|
|
|
|
`;
|
|
|
|
|
|
|
2025-11-03 19:32:39 +01:00
|
|
|
|
altTextInput.addEventListener('focus', () => {
|
|
|
|
|
|
altTextInput.style.borderColor = '#007bff';
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
altTextInput.addEventListener('blur', () => {
|
|
|
|
|
|
altTextInput.style.borderColor = '#ddd';
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2025-11-02 16:57:30 +01:00
|
|
|
|
// Track alt text changes
|
|
|
|
|
|
altTextInput.addEventListener('input', () => {
|
|
|
|
|
|
stagingState.stagedAltText = altTextInput.value;
|
|
|
|
|
|
stagingState.hasChanges = altTextInput.value !== stagingState.currentAltText || stagingState.stagedImageSrc !== null;
|
|
|
|
|
|
updateChangeIndicator();
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2025-11-02 10:01:11 +01:00
|
|
|
|
// Add keyboard shortcuts to alt text input
|
|
|
|
|
|
altTextInput.addEventListener('keydown', this.handleKeydown);
|
|
|
|
|
|
|
|
|
|
|
|
altTextContainer.appendChild(altTextLabel);
|
|
|
|
|
|
altTextContainer.appendChild(altTextInput);
|
|
|
|
|
|
|
2025-11-03 19:32:39 +01:00
|
|
|
|
// Change indicator for floating menu
|
2025-11-02 16:57:30 +01:00
|
|
|
|
const changeIndicator = document.createElement('div');
|
|
|
|
|
|
changeIndicator.className = 'change-indicator';
|
|
|
|
|
|
changeIndicator.style.cssText = `
|
|
|
|
|
|
padding: 8px 12px;
|
|
|
|
|
|
background: #fff3cd;
|
|
|
|
|
|
border: 1px solid #ffeaa7;
|
2025-11-03 19:32:39 +01:00
|
|
|
|
border-radius: 6px;
|
2025-11-02 16:57:30 +01:00
|
|
|
|
color: #856404;
|
2025-11-03 19:32:39 +01:00
|
|
|
|
font-size: 12px;
|
2025-11-02 16:57:30 +01:00
|
|
|
|
text-align: center;
|
|
|
|
|
|
display: none;
|
2025-11-03 19:32:39 +01:00
|
|
|
|
font-weight: 500;
|
2025-11-02 16:57:30 +01:00
|
|
|
|
`;
|
|
|
|
|
|
changeIndicator.textContent = '⚠️ You have unsaved changes';
|
2025-11-02 10:01:11 +01:00
|
|
|
|
|
2025-11-02 16:57:30 +01:00
|
|
|
|
const updateChangeIndicator = () => {
|
|
|
|
|
|
if (stagingState.hasChanges) {
|
|
|
|
|
|
changeIndicator.style.display = 'block';
|
|
|
|
|
|
} else {
|
|
|
|
|
|
changeIndicator.style.display = 'none';
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
2025-11-02 10:01:11 +01:00
|
|
|
|
|
2025-11-03 19:32:39 +01:00
|
|
|
|
// Responsive controls container with alt text
|
2025-11-02 10:01:11 +01:00
|
|
|
|
const editorControls = document.createElement('div');
|
|
|
|
|
|
editorControls.className = 'ui-edit-controls';
|
|
|
|
|
|
editorControls.style.cssText = `
|
|
|
|
|
|
display: flex;
|
2025-11-03 19:32:39 +01:00
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: 12px;
|
|
|
|
|
|
min-width: 180px;
|
|
|
|
|
|
padding: 12px;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
align-items: stretch;
|
2025-11-02 10:01:11 +01:00
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
|
|
const acceptBtn = this.createButton('✓ Accept', 'ui-edit-accept', (e) => {
|
2025-11-02 16:57:30 +01:00
|
|
|
|
// Apply staged changes only when accept is clicked
|
|
|
|
|
|
if (stagingState.hasChanges) {
|
|
|
|
|
|
let newMarkdown = stagingState.originalMarkdown;
|
|
|
|
|
|
|
|
|
|
|
|
// Apply image source change if staged
|
|
|
|
|
|
if (stagingState.stagedImageSrc !== null) {
|
|
|
|
|
|
const currentImageMatch = newMarkdown.match(/!\[(.*?)\]\((.*?)\)/);
|
|
|
|
|
|
if (currentImageMatch) {
|
|
|
|
|
|
newMarkdown = newMarkdown.replace(
|
|
|
|
|
|
/!\[(.*?)\]\((.*?)\)/,
|
|
|
|
|
|
`![${currentImageMatch[1]}](${stagingState.stagedImageSrc})`
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Apply alt text change if staged
|
|
|
|
|
|
if (stagingState.stagedAltText !== null) {
|
|
|
|
|
|
newMarkdown = newMarkdown.replace(
|
|
|
|
|
|
/!\[(.*?)\]/,
|
|
|
|
|
|
`![${stagingState.stagedAltText}]`
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Update section with final changes
|
2025-11-02 10:01:11 +01:00
|
|
|
|
this.sectionManager.updateContent(sectionId, newMarkdown);
|
2025-11-02 16:43:41 +01:00
|
|
|
|
this.updateSectionContent(sectionId, newMarkdown);
|
2025-11-02 10:01:11 +01:00
|
|
|
|
}
|
2025-11-02 16:43:41 +01:00
|
|
|
|
|
|
|
|
|
|
// Accept changes and hide editor
|
|
|
|
|
|
this.sectionManager.acceptChanges(sectionId);
|
|
|
|
|
|
this.hideEditor(sectionId);
|
|
|
|
|
|
});
|
2025-11-02 16:57:30 +01:00
|
|
|
|
|
2025-11-02 16:43:41 +01:00
|
|
|
|
const cancelBtn = this.createButton('✗ Cancel', 'ui-edit-cancel', (e) => {
|
2025-11-02 16:57:30 +01:00
|
|
|
|
// Discard all staged changes and hide editor
|
2025-11-02 16:43:41 +01:00
|
|
|
|
this.sectionManager.cancelChanges(sectionId);
|
|
|
|
|
|
this.hideEditor(sectionId);
|
|
|
|
|
|
});
|
2025-11-02 16:57:30 +01:00
|
|
|
|
|
2025-11-02 16:43:41 +01:00
|
|
|
|
const resetBtn = this.createButton('↺ Reset', 'ui-edit-reset', (e) => {
|
2025-11-02 17:07:42 +01:00
|
|
|
|
// Reset section to original content (like reset all does)
|
|
|
|
|
|
this.sectionManager.resetSection(sectionId);
|
|
|
|
|
|
|
|
|
|
|
|
// Get the reset section to update staging state with original content
|
|
|
|
|
|
const resetSection = this.sectionManager.sections.get(sectionId);
|
|
|
|
|
|
if (resetSection) {
|
|
|
|
|
|
// Update DOM immediately to show reset content
|
|
|
|
|
|
this.updateSectionContent(sectionId, resetSection.currentMarkdown);
|
|
|
|
|
|
|
|
|
|
|
|
// Parse original image info from reset content
|
|
|
|
|
|
const originalImageMatch = resetSection.currentMarkdown.match(/!\[(.*?)\]\((.*?)\)/);
|
|
|
|
|
|
if (originalImageMatch) {
|
|
|
|
|
|
const [, originalAltText, originalImageSrc] = originalImageMatch;
|
|
|
|
|
|
|
|
|
|
|
|
// Update staging state to reflect original content
|
|
|
|
|
|
stagingState.originalMarkdown = resetSection.currentMarkdown;
|
|
|
|
|
|
stagingState.currentAltText = originalAltText;
|
|
|
|
|
|
stagingState.currentImageSrc = originalImageSrc;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Clear any staged changes
|
2025-11-02 16:57:30 +01:00
|
|
|
|
stagingState.stagedImageSrc = null;
|
|
|
|
|
|
stagingState.stagedAltText = null;
|
|
|
|
|
|
stagingState.hasChanges = false;
|
2025-11-02 16:43:41 +01:00
|
|
|
|
|
2025-11-02 16:57:30 +01:00
|
|
|
|
// Reset alt text input to original
|
|
|
|
|
|
altTextInput.value = stagingState.currentAltText;
|
|
|
|
|
|
|
|
|
|
|
|
// Reset preview to original
|
|
|
|
|
|
updateImagePreview(stagingState.currentImageSrc, stagingState.currentAltText);
|
|
|
|
|
|
updateChangeIndicator();
|
2025-11-02 10:01:11 +01:00
|
|
|
|
});
|
|
|
|
|
|
|
2025-11-03 19:32:39 +01:00
|
|
|
|
// Assemble content for floating menu
|
|
|
|
|
|
editorContent.appendChild(imagePreview);
|
|
|
|
|
|
editorContent.appendChild(altTextContainer);
|
|
|
|
|
|
editorContent.appendChild(changeIndicator);
|
|
|
|
|
|
editorContent.appendChild(fileInput);
|
|
|
|
|
|
|
|
|
|
|
|
// Create controls for side panel layout
|
|
|
|
|
|
const controls = document.createElement('div');
|
|
|
|
|
|
controls.className = 'ui-edit-controls';
|
|
|
|
|
|
controls.style.cssText = `
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
|
|
// Style buttons for side panel layout
|
|
|
|
|
|
[acceptBtn, cancelBtn, resetBtn].forEach(btn => {
|
|
|
|
|
|
btn.style.cssText = `
|
|
|
|
|
|
padding: 8px 12px;
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
|
border: none;
|
|
|
|
|
|
color: white;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
transition: all 0.2s ease;
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
`;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2025-11-02 10:01:11 +01:00
|
|
|
|
acceptBtn.style.background = '#28a745';
|
|
|
|
|
|
cancelBtn.style.background = '#dc3545';
|
|
|
|
|
|
resetBtn.style.background = '#fd7e14';
|
|
|
|
|
|
|
2025-11-03 19:32:39 +01:00
|
|
|
|
controls.appendChild(acceptBtn);
|
|
|
|
|
|
controls.appendChild(cancelBtn);
|
|
|
|
|
|
controls.appendChild(resetBtn);
|
2025-11-02 10:01:11 +01:00
|
|
|
|
|
2025-11-03 19:32:39 +01:00
|
|
|
|
// Create unified floating menu using new component
|
|
|
|
|
|
this.currentFloatingMenu = new FloatingMenu(sectionId, 'image', this);
|
|
|
|
|
|
const floatingMenu = this.currentFloatingMenu.show(editorContent, controls);
|
|
|
|
|
|
if (!floatingMenu) return;
|
2025-11-02 10:01:11 +01:00
|
|
|
|
|
|
|
|
|
|
altTextInput.focus();
|
|
|
|
|
|
this.editingSections.add(sectionId);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Image manipulation methods
|
2025-11-02 16:57:30 +01:00
|
|
|
|
* Note: Image replacement is now integrated into the main image editor with drag & drop
|
2025-11-02 10:01:11 +01:00
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
|
|
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) {
|
2025-11-03 19:32:39 +01:00
|
|
|
|
event.preventDefault();
|
|
|
|
|
|
event.stopPropagation();
|
|
|
|
|
|
|
2025-11-02 10:01:11 +01:00
|
|
|
|
const sectionId = this.getCurrentEditingSectionId(event.target);
|
|
|
|
|
|
if (sectionId) {
|
|
|
|
|
|
this.sectionManager.acceptChanges(sectionId);
|
2025-11-03 19:32:39 +01:00
|
|
|
|
this.hideEditor(sectionId);
|
2025-11-02 10:01:11 +01:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
handleCancel(event) {
|
2025-11-03 19:32:39 +01:00
|
|
|
|
event.preventDefault();
|
|
|
|
|
|
event.stopPropagation();
|
|
|
|
|
|
|
2025-11-02 10:01:11 +01:00
|
|
|
|
const sectionId = this.getCurrentEditingSectionId(event.target);
|
|
|
|
|
|
if (sectionId) {
|
|
|
|
|
|
this.sectionManager.cancelChanges(sectionId);
|
2025-11-03 19:32:39 +01:00
|
|
|
|
this.hideEditor(sectionId);
|
2025-11-02 10:01:11 +01:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
handleReset(event) {
|
2025-11-03 19:32:39 +01:00
|
|
|
|
event.preventDefault();
|
|
|
|
|
|
event.stopPropagation();
|
|
|
|
|
|
|
2025-11-02 10:01:11 +01:00
|
|
|
|
const sectionId = this.getCurrentEditingSectionId(event.target);
|
|
|
|
|
|
if (sectionId) {
|
|
|
|
|
|
this.sectionManager.resetSection(sectionId);
|
2025-11-03 19:32:39 +01:00
|
|
|
|
|
|
|
|
|
|
// Update DOM to show reset content
|
|
|
|
|
|
const section = this.sectionManager.sections.get(sectionId);
|
|
|
|
|
|
if (section) {
|
|
|
|
|
|
this.updateSectionContent(sectionId, section.currentMarkdown);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
this.hideEditor(sectionId);
|
2025-11-02 10:01:11 +01:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
handleKeydown(event) {
|
|
|
|
|
|
// Enhanced keyboard shortcuts for section editing
|
2025-11-02 16:28:20 +01:00
|
|
|
|
const textarea = event.target.closest('textarea, input');
|
2025-11-02 10:01:11 +01:00
|
|
|
|
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;
|
|
|
|
|
|
|
2025-11-02 16:28:20 +01:00
|
|
|
|
let shortcutKey = '';
|
|
|
|
|
|
let shortcutAction = '';
|
|
|
|
|
|
|
2025-11-02 10:01:11 +01:00
|
|
|
|
// Handle keyboard shortcuts
|
|
|
|
|
|
if (event.ctrlKey || event.metaKey) {
|
|
|
|
|
|
switch (event.key) {
|
|
|
|
|
|
case 'Enter':
|
|
|
|
|
|
event.preventDefault();
|
2025-11-02 16:28:20 +01:00
|
|
|
|
shortcutKey = (event.ctrlKey ? 'ctrl' : 'cmd') + '+enter';
|
|
|
|
|
|
shortcutAction = 'accept';
|
2025-11-02 10:01:11 +01:00
|
|
|
|
this.sectionManager.acceptChanges(sectionId);
|
|
|
|
|
|
debug('Keyboard shortcut: Ctrl+Enter - accepted changes for section ' + sectionId, 'KEYBOARD');
|
|
|
|
|
|
break;
|
|
|
|
|
|
case 'Escape':
|
|
|
|
|
|
event.preventDefault();
|
2025-11-02 16:28:20 +01:00
|
|
|
|
shortcutKey = (event.ctrlKey ? 'ctrl' : 'cmd') + '+escape';
|
|
|
|
|
|
shortcutAction = 'cancel';
|
2025-11-02 10:01:11 +01:00
|
|
|
|
this.sectionManager.cancelChanges(sectionId);
|
|
|
|
|
|
debug('Keyboard shortcut: Ctrl+Escape - cancelled changes for section ' + sectionId, 'KEYBOARD');
|
|
|
|
|
|
break;
|
2025-11-02 16:28:20 +01:00
|
|
|
|
case 's':
|
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
|
shortcutKey = (event.ctrlKey ? 'ctrl' : 'cmd') + '+s';
|
|
|
|
|
|
shortcutAction = 'save';
|
|
|
|
|
|
this.sectionManager.acceptChanges(sectionId);
|
|
|
|
|
|
debug('Keyboard shortcut: Ctrl+S - saved changes for section ' + sectionId, 'KEYBOARD');
|
|
|
|
|
|
break;
|
2025-11-02 10:01:11 +01:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Handle plain Escape (without Ctrl)
|
|
|
|
|
|
if (event.key === 'Escape' && !event.ctrlKey && !event.metaKey) {
|
|
|
|
|
|
event.preventDefault();
|
2025-11-02 16:28:20 +01:00
|
|
|
|
shortcutKey = 'escape';
|
|
|
|
|
|
shortcutAction = 'cancel';
|
2025-11-02 10:01:11 +01:00
|
|
|
|
this.sectionManager.cancelChanges(sectionId);
|
|
|
|
|
|
debug('Keyboard shortcut: Escape - cancelled changes for section ' + sectionId, 'KEYBOARD');
|
|
|
|
|
|
}
|
2025-11-02 16:28:20 +01:00
|
|
|
|
|
|
|
|
|
|
// Track keyboard shortcut events
|
|
|
|
|
|
if (shortcutKey && shortcutAction) {
|
|
|
|
|
|
this.trackEvent('keyboard-shortcut', {
|
|
|
|
|
|
sectionId,
|
|
|
|
|
|
shortcut: shortcutKey,
|
|
|
|
|
|
action: shortcutAction,
|
|
|
|
|
|
key: event.key,
|
|
|
|
|
|
ctrlKey: event.ctrlKey,
|
|
|
|
|
|
metaKey: event.metaKey,
|
|
|
|
|
|
event,
|
|
|
|
|
|
timestamp: Date.now()
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
2025-11-02 10:01:11 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
getCurrentEditingSectionId(button) {
|
2025-11-03 19:32:39 +01:00
|
|
|
|
// Check if button is in a floating menu
|
|
|
|
|
|
const floatingMenu = button.closest('.ui-edit-floating-menu');
|
|
|
|
|
|
if (floatingMenu) {
|
|
|
|
|
|
return floatingMenu.dataset.sectionId;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Fallback to old overlay container method
|
|
|
|
|
|
const editorContainer = button.closest('.ui-edit-editor-container, .ui-edit-image-editor-container, .ui-edit-overlay-container');
|
2025-11-02 10:01:11 +01:00
|
|
|
|
if (!editorContainer) return null;
|
|
|
|
|
|
|
|
|
|
|
|
const sectionElement = editorContainer.parentElement;
|
|
|
|
|
|
return sectionElement ? sectionElement.getAttribute('data-section-id') : null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
hideEditor(sectionId) {
|
|
|
|
|
|
const element = this.findSectionElement(sectionId);
|
|
|
|
|
|
|
2025-11-03 19:32:39 +01:00
|
|
|
|
// Remove floating menu if it exists
|
|
|
|
|
|
const floatingMenu = document.querySelector(`.ui-edit-floating-menu[data-section-id="${sectionId}"]`);
|
|
|
|
|
|
if (floatingMenu) {
|
|
|
|
|
|
floatingMenu.remove();
|
2025-11-02 16:49:58 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-03 19:32:39 +01:00
|
|
|
|
// Remove element highlighting
|
|
|
|
|
|
if (element) {
|
|
|
|
|
|
element.style.outline = '';
|
|
|
|
|
|
element.style.outlineOffset = '';
|
|
|
|
|
|
element.style.backgroundColor = '';
|
2025-11-02 16:49:58 +01:00
|
|
|
|
|
2025-11-03 19:32:39 +01:00
|
|
|
|
// Remove any old editor UI containers from the DOM
|
|
|
|
|
|
const textEditorContainer = element.querySelector('.ui-edit-editor-container');
|
|
|
|
|
|
if (textEditorContainer) {
|
|
|
|
|
|
textEditorContainer.remove();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const imageEditorContainer = element.querySelector('.ui-edit-image-editor-container');
|
|
|
|
|
|
if (imageEditorContainer) {
|
|
|
|
|
|
imageEditorContainer.remove();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Remove overlay containers and restore original content if needed
|
|
|
|
|
|
const overlayContainer = element.querySelector('.ui-edit-overlay-container');
|
|
|
|
|
|
if (overlayContainer) {
|
|
|
|
|
|
const originalContent = overlayContainer.dataset.originalContent;
|
|
|
|
|
|
overlayContainer.remove();
|
|
|
|
|
|
|
|
|
|
|
|
// Restore element's original positioning
|
|
|
|
|
|
element.style.position = '';
|
|
|
|
|
|
|
|
|
|
|
|
// If original content was stored, restore it
|
|
|
|
|
|
if (originalContent) {
|
|
|
|
|
|
element.innerHTML = originalContent;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// Otherwise, refresh the section content
|
|
|
|
|
|
const section = this.sectionManager.sections.get(sectionId);
|
|
|
|
|
|
if (section) {
|
|
|
|
|
|
this.updateSectionContent(sectionId, section.currentMarkdown);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const section = this.sectionManager.sections.get(sectionId);
|
|
|
|
|
|
if (section) {
|
|
|
|
|
|
this.updateSectionContent(sectionId, section.currentMarkdown);
|
|
|
|
|
|
}
|
2025-11-02 10:01:11 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
this.editingSections.delete(sectionId);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
hideCurrentEditor() {
|
2025-11-03 19:32:39 +01:00
|
|
|
|
debug('EDITOR: hideCurrentEditor called', 'EDITOR');
|
|
|
|
|
|
|
|
|
|
|
|
// Hide FloatingMenu if it exists
|
|
|
|
|
|
if (this.currentFloatingMenu) {
|
|
|
|
|
|
debug('EDITOR: Hiding currentFloatingMenu', 'EDITOR');
|
|
|
|
|
|
this.currentFloatingMenu.hide();
|
|
|
|
|
|
this.currentFloatingMenu = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Hide any legacy editors
|
2025-11-02 10:01:11 +01:00
|
|
|
|
this.editingSections.forEach(sectionId => {
|
|
|
|
|
|
const element = this.findSectionElement(sectionId);
|
|
|
|
|
|
if (element && element.querySelector('.ui-edit-editor-container')) {
|
|
|
|
|
|
this.hideEditor(sectionId);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
2025-11-03 19:32:39 +01:00
|
|
|
|
|
|
|
|
|
|
// CRUCIAL FIX: Ensure ALL sections are reset from editing state
|
|
|
|
|
|
this.sectionManager.sections.forEach((section, sectionId) => {
|
|
|
|
|
|
if (section.isEditing()) {
|
|
|
|
|
|
debug('EDITOR: Force stopping editing for stuck section: ' + sectionId, 'EDITOR');
|
|
|
|
|
|
section.stopEditing();
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// Clear the editing sections set
|
|
|
|
|
|
this.editingSections.clear();
|
|
|
|
|
|
|
|
|
|
|
|
// Clean up any floating menu elements
|
|
|
|
|
|
const floatingMenus = document.querySelectorAll('.ui-edit-floating-menu');
|
|
|
|
|
|
floatingMenus.forEach(menu => menu.remove());
|
|
|
|
|
|
|
|
|
|
|
|
debug('EDITOR: hideCurrentEditor completed', 'EDITOR');
|
2025-11-02 10:01:11 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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>`;
|
|
|
|
|
|
}
|
2025-11-02 16:28:20 +01:00
|
|
|
|
|
|
|
|
|
|
this.setupSectionElement(element);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
findSectionElement(sectionId) {
|
|
|
|
|
|
return this.container.querySelector(`[data-section-id="${sectionId}"]`);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
setupSectionElement(element) {
|
|
|
|
|
|
element.className = 'ui-edit-section';
|
|
|
|
|
|
element.draggable = true; // Enable drag and drop
|
|
|
|
|
|
element.tabIndex = 0; // Make focusable for accessibility
|
|
|
|
|
|
element.style.cssText = `
|
|
|
|
|
|
margin: 16px 0;
|
|
|
|
|
|
padding: 12px;
|
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
transition: all 0.2s ease;
|
|
|
|
|
|
border: 2px solid transparent;
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
|
|
// Add drag handle indicator
|
|
|
|
|
|
const dragHandle = document.createElement('div');
|
|
|
|
|
|
dragHandle.className = 'ui-edit-drag-handle';
|
|
|
|
|
|
dragHandle.innerHTML = '⋮⋮';
|
|
|
|
|
|
dragHandle.style.cssText = `
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
left: -8px;
|
|
|
|
|
|
top: 50%;
|
|
|
|
|
|
transform: translateY(-50%);
|
|
|
|
|
|
color: #ccc;
|
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
|
line-height: 1;
|
|
|
|
|
|
cursor: grab;
|
|
|
|
|
|
opacity: 0;
|
|
|
|
|
|
transition: opacity 0.2s ease;
|
|
|
|
|
|
user-select: none;
|
|
|
|
|
|
`;
|
|
|
|
|
|
element.appendChild(dragHandle);
|
|
|
|
|
|
|
|
|
|
|
|
element.removeEventListener('mouseenter', element._mouseenterHandler);
|
|
|
|
|
|
element.removeEventListener('mouseleave', element._mouseleaveHandler);
|
|
|
|
|
|
|
|
|
|
|
|
element._mouseenterHandler = () => {
|
|
|
|
|
|
element.style.backgroundColor = 'rgba(0, 0, 0, 0.02)';
|
|
|
|
|
|
element.style.borderColor = 'rgba(0, 0, 0, 0.1)';
|
|
|
|
|
|
dragHandle.style.opacity = '1';
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
element._mouseleaveHandler = () => {
|
|
|
|
|
|
element.style.backgroundColor = '';
|
|
|
|
|
|
element.style.borderColor = 'transparent';
|
|
|
|
|
|
dragHandle.style.opacity = '0';
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
element.addEventListener('mouseenter', element._mouseenterHandler);
|
|
|
|
|
|
element.addEventListener('mouseleave', element._mouseleaveHandler);
|
|
|
|
|
|
|
|
|
|
|
|
// Enhanced accessibility
|
|
|
|
|
|
element.setAttribute('role', 'article');
|
|
|
|
|
|
element.setAttribute('aria-label', 'Editable section');
|
|
|
|
|
|
element.setAttribute('title', 'Click to edit, right-click for options, drag to reorder');
|
|
|
|
|
|
|
|
|
|
|
|
// Apply comprehensive styling enhancements
|
|
|
|
|
|
this.applyComprehensiveStyling(element);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Apply comprehensive styling enhancements to section elements
|
|
|
|
|
|
* @param {HTMLElement} element - The section element
|
|
|
|
|
|
*/
|
|
|
|
|
|
applyComprehensiveStyling(element) {
|
|
|
|
|
|
if (!element || !element.dataset.sectionId) return;
|
|
|
|
|
|
|
|
|
|
|
|
const sectionId = element.dataset.sectionId;
|
|
|
|
|
|
const section = this.sectionManager.sections.get(sectionId);
|
|
|
|
|
|
if (!section) return;
|
|
|
|
|
|
|
|
|
|
|
|
// Apply type-specific styling
|
|
|
|
|
|
this.applyTypeSpecificStyling(element, section);
|
|
|
|
|
|
|
|
|
|
|
|
// Apply state-based styling
|
|
|
|
|
|
this.applyStateStyling(element, section);
|
|
|
|
|
|
|
|
|
|
|
|
// Apply responsive design
|
|
|
|
|
|
this.applyResponsiveStyling(element);
|
|
|
|
|
|
|
|
|
|
|
|
// Apply accessibility enhancements
|
|
|
|
|
|
this.enhanceAccessibility(element, section);
|
|
|
|
|
|
|
|
|
|
|
|
// Apply length-based styling
|
|
|
|
|
|
this.applyLengthBasedStyling(element, section);
|
|
|
|
|
|
|
|
|
|
|
|
// Apply performance-optimized transitions
|
|
|
|
|
|
this.applyOptimizedTransitions(element);
|
|
|
|
|
|
|
|
|
|
|
|
// Apply CSS custom properties
|
|
|
|
|
|
this.applyCSSCustomProperties(element, section);
|
|
|
|
|
|
|
|
|
|
|
|
// Apply theme support
|
|
|
|
|
|
this.applySectionTheme(element, 'light'); // Default theme
|
|
|
|
|
|
|
|
|
|
|
|
// Apply content analysis styling
|
|
|
|
|
|
this.analyzeContentForStyling(element, section);
|
|
|
|
|
|
|
|
|
|
|
|
// Apply CSS reset and normalization
|
|
|
|
|
|
this.applyCSSReset(element);
|
|
|
|
|
|
|
|
|
|
|
|
// Setup animation support
|
|
|
|
|
|
this.setupAnimationSupport(element);
|
|
|
|
|
|
|
|
|
|
|
|
// Apply print-friendly styling
|
|
|
|
|
|
this.applyPrintStyling(element);
|
|
|
|
|
|
|
|
|
|
|
|
// Integrate with existing systems
|
|
|
|
|
|
this.integrateWithMessageSystem(element);
|
|
|
|
|
|
this.integrateWithControlPanel(element);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Apply type-specific styling to section elements
|
|
|
|
|
|
* @param {HTMLElement} element - Section element
|
|
|
|
|
|
* @param {Section} section - Section object
|
|
|
|
|
|
*/
|
|
|
|
|
|
applyTypeSpecificStyling(element, section) {
|
|
|
|
|
|
// Add base class
|
|
|
|
|
|
element.classList.add('markitect-section-editable');
|
|
|
|
|
|
|
|
|
|
|
|
// Add type-specific class
|
|
|
|
|
|
const typeClass = `markitect-section-${section.type || 'paragraph'}`;
|
|
|
|
|
|
element.classList.add(typeClass);
|
|
|
|
|
|
|
|
|
|
|
|
// Set data attributes
|
|
|
|
|
|
element.dataset.sectionType = section.type || 'paragraph';
|
|
|
|
|
|
element.dataset.sectionId = section.id;
|
|
|
|
|
|
|
|
|
|
|
|
// Type-specific styling
|
|
|
|
|
|
const typeStyles = {
|
|
|
|
|
|
heading: {
|
|
|
|
|
|
borderLeft: '4px solid #007acc',
|
|
|
|
|
|
backgroundColor: 'rgba(0, 122, 204, 0.02)',
|
|
|
|
|
|
fontWeight: '600'
|
|
|
|
|
|
},
|
|
|
|
|
|
code: {
|
|
|
|
|
|
borderLeft: '4px solid #28a745',
|
|
|
|
|
|
backgroundColor: 'rgba(40, 167, 69, 0.02)',
|
|
|
|
|
|
fontFamily: 'monospace'
|
|
|
|
|
|
},
|
|
|
|
|
|
list: {
|
|
|
|
|
|
borderLeft: '4px solid #ffc107',
|
|
|
|
|
|
backgroundColor: 'rgba(255, 193, 7, 0.02)'
|
|
|
|
|
|
},
|
|
|
|
|
|
quote: {
|
|
|
|
|
|
borderLeft: '4px solid #6f42c1',
|
|
|
|
|
|
backgroundColor: 'rgba(111, 66, 193, 0.02)',
|
|
|
|
|
|
fontStyle: 'italic'
|
|
|
|
|
|
},
|
|
|
|
|
|
image: {
|
|
|
|
|
|
borderLeft: '4px solid #fd7e14',
|
|
|
|
|
|
backgroundColor: 'rgba(253, 126, 20, 0.02)',
|
|
|
|
|
|
textAlign: 'center'
|
|
|
|
|
|
},
|
|
|
|
|
|
table: {
|
|
|
|
|
|
borderLeft: '4px solid #20c997',
|
|
|
|
|
|
backgroundColor: 'rgba(32, 201, 151, 0.02)'
|
|
|
|
|
|
},
|
|
|
|
|
|
hr: {
|
|
|
|
|
|
borderLeft: '4px solid #6c757d',
|
|
|
|
|
|
backgroundColor: 'rgba(108, 117, 125, 0.02)',
|
|
|
|
|
|
minHeight: '20px'
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const styles = typeStyles[section.type] || typeStyles.paragraph || {};
|
|
|
|
|
|
Object.assign(element.style, styles);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Apply state-based styling
|
|
|
|
|
|
* @param {HTMLElement} element - Section element
|
|
|
|
|
|
* @param {Section} section - Section object
|
|
|
|
|
|
*/
|
|
|
|
|
|
applyStateStyling(element, section) {
|
|
|
|
|
|
// Remove existing state classes
|
|
|
|
|
|
element.classList.remove('section-original', 'section-editing', 'section-modified', 'section-saved');
|
|
|
|
|
|
|
|
|
|
|
|
// Apply current state class
|
|
|
|
|
|
if (section.isEditing()) {
|
|
|
|
|
|
element.classList.add('section-editing');
|
|
|
|
|
|
element.style.backgroundColor = 'rgba(0, 122, 204, 0.1)';
|
|
|
|
|
|
element.style.borderColor = '#007acc';
|
|
|
|
|
|
} else if (section.hasChanges()) {
|
|
|
|
|
|
element.classList.add('section-modified');
|
|
|
|
|
|
element.style.backgroundColor = 'rgba(255, 193, 7, 0.1)';
|
|
|
|
|
|
element.style.borderColor = '#ffc107';
|
|
|
|
|
|
} else if (section.state === 'saved') {
|
|
|
|
|
|
element.classList.add('section-saved');
|
|
|
|
|
|
element.style.backgroundColor = 'rgba(40, 167, 69, 0.1)';
|
|
|
|
|
|
element.style.borderColor = '#28a745';
|
|
|
|
|
|
} else {
|
|
|
|
|
|
element.classList.add('section-original');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Apply responsive design styling
|
|
|
|
|
|
* @param {HTMLElement} element - Section element
|
|
|
|
|
|
*/
|
|
|
|
|
|
applyResponsiveStyling(element) {
|
|
|
|
|
|
element.classList.add('section-responsive');
|
|
|
|
|
|
|
|
|
|
|
|
// Responsive styles
|
|
|
|
|
|
element.style.cssText += `
|
|
|
|
|
|
max-width: 100%;
|
|
|
|
|
|
min-width: 0;
|
|
|
|
|
|
overflow-wrap: break-word;
|
|
|
|
|
|
word-wrap: break-word;
|
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
|
|
// Responsive behavior based on viewport
|
|
|
|
|
|
const updateResponsiveStyles = () => {
|
|
|
|
|
|
const width = window.innerWidth;
|
|
|
|
|
|
if (width < 768) {
|
|
|
|
|
|
// Mobile styles
|
|
|
|
|
|
element.style.margin = '8px 0';
|
|
|
|
|
|
element.style.padding = '8px';
|
|
|
|
|
|
element.style.fontSize = '14px';
|
|
|
|
|
|
} else if (width < 1024) {
|
|
|
|
|
|
// Tablet styles
|
|
|
|
|
|
element.style.margin = '12px 0';
|
|
|
|
|
|
element.style.padding = '10px';
|
|
|
|
|
|
element.style.fontSize = '15px';
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// Desktop styles
|
|
|
|
|
|
element.style.margin = '16px 0';
|
|
|
|
|
|
element.style.padding = '12px';
|
|
|
|
|
|
element.style.fontSize = '16px';
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
updateResponsiveStyles();
|
|
|
|
|
|
window.addEventListener('resize', updateResponsiveStyles);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Enhance accessibility features
|
|
|
|
|
|
* @param {HTMLElement} element - Section element
|
|
|
|
|
|
* @param {Section} section - Section object
|
|
|
|
|
|
*/
|
|
|
|
|
|
enhanceAccessibility(element, section) {
|
|
|
|
|
|
// Enhanced ARIA attributes
|
|
|
|
|
|
element.setAttribute('aria-describedby', `section-desc-${section.id}`);
|
|
|
|
|
|
element.setAttribute('aria-labelledby', `section-label-${section.id}`);
|
|
|
|
|
|
|
|
|
|
|
|
// Screen reader support
|
|
|
|
|
|
const srOnly = document.createElement('span');
|
|
|
|
|
|
srOnly.className = 'sr-only';
|
|
|
|
|
|
srOnly.id = `section-desc-${section.id}`;
|
|
|
|
|
|
srOnly.textContent = `${section.type} section with ${section.currentMarkdown.length} characters`;
|
|
|
|
|
|
srOnly.style.cssText = `
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
width: 1px;
|
|
|
|
|
|
height: 1px;
|
|
|
|
|
|
padding: 0;
|
|
|
|
|
|
margin: -1px;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
clip: rect(0,0,0,0);
|
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
|
border: 0;
|
|
|
|
|
|
`;
|
|
|
|
|
|
element.appendChild(srOnly);
|
|
|
|
|
|
|
|
|
|
|
|
// Enhanced keyboard navigation
|
|
|
|
|
|
element.tabIndex = 0;
|
|
|
|
|
|
element.setAttribute('aria-keyshortcuts', 'Enter Space');
|
|
|
|
|
|
|
|
|
|
|
|
// Focus enhancement
|
|
|
|
|
|
element.addEventListener('focus', () => {
|
|
|
|
|
|
element.style.outline = '2px solid #007acc';
|
|
|
|
|
|
element.style.outlineOffset = '2px';
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
element.addEventListener('blur', () => {
|
|
|
|
|
|
element.style.outline = '';
|
|
|
|
|
|
element.style.outlineOffset = '';
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Apply length-based visual indicators
|
|
|
|
|
|
* @param {HTMLElement} element - Section element
|
|
|
|
|
|
* @param {Section} section - Section object
|
|
|
|
|
|
*/
|
|
|
|
|
|
applyLengthBasedStyling(element, section) {
|
|
|
|
|
|
const length = section.currentMarkdown.length;
|
|
|
|
|
|
|
|
|
|
|
|
// Remove existing length classes
|
|
|
|
|
|
element.classList.remove('section-short', 'section-medium', 'section-long');
|
|
|
|
|
|
|
|
|
|
|
|
if (length < 100) {
|
|
|
|
|
|
element.classList.add('section-short');
|
|
|
|
|
|
element.style.minHeight = '40px';
|
|
|
|
|
|
} else if (length < 500) {
|
|
|
|
|
|
element.classList.add('section-medium');
|
|
|
|
|
|
element.style.minHeight = '60px';
|
|
|
|
|
|
} else {
|
|
|
|
|
|
element.classList.add('section-long');
|
|
|
|
|
|
element.style.minHeight = '80px';
|
|
|
|
|
|
element.style.maxHeight = '400px';
|
|
|
|
|
|
element.style.overflowY = 'auto';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Word count indicator
|
|
|
|
|
|
const wordCount = section.currentMarkdown.split(/\s+/).length;
|
|
|
|
|
|
element.dataset.wordCount = wordCount.toString();
|
|
|
|
|
|
element.setAttribute('title', element.getAttribute('title') + ` (${wordCount} words)`);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Apply performance-optimized CSS transitions
|
|
|
|
|
|
* @param {HTMLElement} element - Section element
|
|
|
|
|
|
*/
|
|
|
|
|
|
applyOptimizedTransitions(element) {
|
|
|
|
|
|
// GPU-accelerated transitions
|
|
|
|
|
|
element.style.willChange = 'transform, opacity';
|
|
|
|
|
|
element.style.transform = 'translateZ(0)'; // Force GPU layer
|
|
|
|
|
|
element.style.transition = 'all 0.2s cubic-bezier(0.4, 0, 0.2, 1)';
|
|
|
|
|
|
|
|
|
|
|
|
// Hover enhancements
|
|
|
|
|
|
element.classList.add('section-hoverable');
|
|
|
|
|
|
|
|
|
|
|
|
const optimizedMouseEnter = () => {
|
|
|
|
|
|
element.style.transform = 'translateZ(0) scale(1.01)';
|
|
|
|
|
|
element.style.boxShadow = '0 4px 12px rgba(0, 0, 0, 0.1)';
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const optimizedMouseLeave = () => {
|
|
|
|
|
|
element.style.transform = 'translateZ(0) scale(1)';
|
|
|
|
|
|
element.style.boxShadow = '';
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
element.addEventListener('mouseenter', optimizedMouseEnter);
|
|
|
|
|
|
element.addEventListener('mouseleave', optimizedMouseLeave);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Apply CSS custom properties for advanced styling
|
|
|
|
|
|
* @param {HTMLElement} element - Section element
|
|
|
|
|
|
* @param {Section} section - Section object
|
|
|
|
|
|
*/
|
|
|
|
|
|
applyCSSCustomProperties(element, section) {
|
|
|
|
|
|
// CSS variables for dynamic theming
|
|
|
|
|
|
element.style.setProperty('--section-primary-color', '#007acc');
|
|
|
|
|
|
element.style.setProperty('--section-background', 'rgba(248, 249, 250, 0.5)');
|
|
|
|
|
|
element.style.setProperty('--section-border-radius', '6px');
|
|
|
|
|
|
element.style.setProperty('--section-padding', '12px');
|
|
|
|
|
|
element.style.setProperty('--section-margin', '16px 0');
|
|
|
|
|
|
element.style.setProperty('--section-transition', 'all 0.2s ease');
|
|
|
|
|
|
|
|
|
|
|
|
// Type-specific CSS variables
|
|
|
|
|
|
const typeColors = {
|
|
|
|
|
|
heading: '#007acc',
|
|
|
|
|
|
code: '#28a745',
|
|
|
|
|
|
list: '#ffc107',
|
|
|
|
|
|
quote: '#6f42c1',
|
|
|
|
|
|
image: '#fd7e14',
|
|
|
|
|
|
table: '#20c997',
|
|
|
|
|
|
hr: '#6c757d'
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
element.style.setProperty('--section-type-color', typeColors[section.type] || '#6c757d');
|
|
|
|
|
|
element.dataset.cssVariables = 'true';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Apply theme-based styling
|
|
|
|
|
|
* @param {HTMLElement} element - Section element
|
|
|
|
|
|
* @param {string} theme - Theme name ('light', 'dark', 'high-contrast')
|
|
|
|
|
|
*/
|
|
|
|
|
|
applySectionTheme(element, theme) {
|
|
|
|
|
|
element.dataset.theme = theme;
|
|
|
|
|
|
element.classList.remove('theme-light', 'theme-dark', 'theme-high-contrast');
|
|
|
|
|
|
element.classList.add(`theme-${theme}`);
|
|
|
|
|
|
|
|
|
|
|
|
const themes = {
|
|
|
|
|
|
light: {
|
|
|
|
|
|
'--section-background': 'rgba(248, 249, 250, 0.5)',
|
|
|
|
|
|
'--section-text-color': '#212529',
|
|
|
|
|
|
'--section-border-color': 'rgba(0, 0, 0, 0.1)',
|
|
|
|
|
|
'--section-hover-bg': 'rgba(0, 0, 0, 0.02)'
|
|
|
|
|
|
},
|
|
|
|
|
|
dark: {
|
|
|
|
|
|
'--section-background': 'rgba(33, 37, 41, 0.8)',
|
|
|
|
|
|
'--section-text-color': '#f8f9fa',
|
|
|
|
|
|
'--section-border-color': 'rgba(255, 255, 255, 0.2)',
|
|
|
|
|
|
'--section-hover-bg': 'rgba(255, 255, 255, 0.05)'
|
|
|
|
|
|
},
|
|
|
|
|
|
'high-contrast': {
|
|
|
|
|
|
'--section-background': '#ffffff',
|
|
|
|
|
|
'--section-text-color': '#000000',
|
|
|
|
|
|
'--section-border-color': '#000000',
|
|
|
|
|
|
'--section-hover-bg': '#f0f0f0'
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const themeStyles = themes[theme] || themes.light;
|
|
|
|
|
|
Object.entries(themeStyles).forEach(([property, value]) => {
|
|
|
|
|
|
element.style.setProperty(property, value);
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Analyze content for styling purposes
|
|
|
|
|
|
* @param {HTMLElement} element - Section element
|
|
|
|
|
|
* @param {Section} section - Section object
|
|
|
|
|
|
*/
|
|
|
|
|
|
analyzeContentForStyling(element, section) {
|
|
|
|
|
|
const content = section.currentMarkdown.toLowerCase();
|
|
|
|
|
|
|
|
|
|
|
|
// Content-based classes
|
|
|
|
|
|
if (content.includes('```') || content.includes('`')) {
|
|
|
|
|
|
element.classList.add('contains-code');
|
|
|
|
|
|
}
|
|
|
|
|
|
if (content.includes('$$') || content.includes('\\(') || content.includes('\\[')) {
|
|
|
|
|
|
element.classList.add('contains-math');
|
|
|
|
|
|
}
|
|
|
|
|
|
if (content.includes('http') || content.includes('[') && content.includes(']')) {
|
|
|
|
|
|
element.classList.add('contains-links');
|
|
|
|
|
|
}
|
|
|
|
|
|
if (content.includes('![')) {
|
|
|
|
|
|
element.classList.add('contains-images');
|
|
|
|
|
|
}
|
|
|
|
|
|
if (content.includes('|') && content.includes('-')) {
|
|
|
|
|
|
element.classList.add('contains-tables');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Priority content indicators
|
|
|
|
|
|
if (content.includes('important') || content.includes('warning') || content.includes('!')) {
|
|
|
|
|
|
element.classList.add('priority-high');
|
|
|
|
|
|
element.style.borderLeftWidth = '6px';
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Apply CSS reset and normalization
|
|
|
|
|
|
* @param {HTMLElement} element - Section element
|
|
|
|
|
|
*/
|
|
|
|
|
|
applyCSSReset(element) {
|
|
|
|
|
|
element.classList.add('css-reset');
|
|
|
|
|
|
|
|
|
|
|
|
// Normalize box model
|
|
|
|
|
|
element.style.boxSizing = 'border-box';
|
|
|
|
|
|
element.style.margin = '16px 0';
|
|
|
|
|
|
element.style.padding = '12px';
|
|
|
|
|
|
|
|
|
|
|
|
// Reset browser defaults
|
|
|
|
|
|
const allChildren = element.querySelectorAll('*');
|
|
|
|
|
|
allChildren.forEach(child => {
|
|
|
|
|
|
child.style.boxSizing = 'border-box';
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Setup animation support for state transitions
|
|
|
|
|
|
* @param {HTMLElement} element - Section element
|
|
|
|
|
|
*/
|
|
|
|
|
|
setupAnimationSupport(element) {
|
|
|
|
|
|
element.classList.add('animation-ready');
|
|
|
|
|
|
|
|
|
|
|
|
// Animation utility method
|
|
|
|
|
|
element._animate = (animationType) => {
|
|
|
|
|
|
this.animateSectionTransition(element, animationType);
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Animate section transitions
|
|
|
|
|
|
* @param {HTMLElement} element - Section element
|
|
|
|
|
|
* @param {string} animationType - Type of animation
|
|
|
|
|
|
*/
|
|
|
|
|
|
animateSectionTransition(element, animationType) {
|
|
|
|
|
|
element.classList.add('section-animating');
|
|
|
|
|
|
|
|
|
|
|
|
switch (animationType) {
|
|
|
|
|
|
case 'enter':
|
|
|
|
|
|
element.classList.add('transition-entering');
|
|
|
|
|
|
element.style.opacity = '0';
|
|
|
|
|
|
element.style.transform = 'translateY(-10px)';
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
element.style.opacity = '1';
|
|
|
|
|
|
element.style.transform = 'translateY(0)';
|
|
|
|
|
|
}, 50);
|
|
|
|
|
|
break;
|
|
|
|
|
|
case 'leave':
|
|
|
|
|
|
element.classList.add('transition-leaving');
|
|
|
|
|
|
element.style.opacity = '0';
|
|
|
|
|
|
element.style.transform = 'translateY(10px)';
|
|
|
|
|
|
break;
|
|
|
|
|
|
case 'highlight':
|
|
|
|
|
|
element.style.backgroundColor = 'rgba(255, 193, 7, 0.3)';
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
element.style.backgroundColor = '';
|
|
|
|
|
|
}, 1000);
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
element.classList.remove('section-animating', 'transition-entering', 'transition-leaving');
|
|
|
|
|
|
}, 300);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Apply print-friendly styling
|
|
|
|
|
|
* @param {HTMLElement} element - Section element
|
|
|
|
|
|
*/
|
|
|
|
|
|
applyPrintStyling(element) {
|
|
|
|
|
|
element.classList.add('print-friendly');
|
|
|
|
|
|
element.dataset.printOptimized = 'true';
|
|
|
|
|
|
|
|
|
|
|
|
// Print-specific styles via CSS
|
|
|
|
|
|
const printStyles = document.createElement('style');
|
|
|
|
|
|
printStyles.textContent = `
|
|
|
|
|
|
@media print {
|
|
|
|
|
|
.print-friendly {
|
|
|
|
|
|
break-inside: avoid;
|
|
|
|
|
|
margin: 8px 0;
|
|
|
|
|
|
padding: 8px;
|
|
|
|
|
|
border: 1px solid #000;
|
|
|
|
|
|
background: white !important;
|
|
|
|
|
|
color: black !important;
|
|
|
|
|
|
box-shadow: none !important;
|
|
|
|
|
|
transform: none !important;
|
|
|
|
|
|
}
|
|
|
|
|
|
.ui-edit-drag-handle,
|
|
|
|
|
|
.sr-only {
|
|
|
|
|
|
display: none !important;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
`;
|
|
|
|
|
|
if (!document.querySelector('#section-print-styles')) {
|
|
|
|
|
|
printStyles.id = 'section-print-styles';
|
|
|
|
|
|
document.head.appendChild(printStyles);
|
|
|
|
|
|
}
|
2025-11-02 10:01:11 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-02 16:28:20 +01:00
|
|
|
|
/**
|
|
|
|
|
|
* Update dynamic styles based on section state
|
|
|
|
|
|
* @param {HTMLElement} element - Section element
|
|
|
|
|
|
* @param {Object} newStyles - Style updates
|
|
|
|
|
|
*/
|
|
|
|
|
|
updateSectionDynamicStyles(element, newStyles) {
|
|
|
|
|
|
Object.entries(newStyles).forEach(([property, value]) => {
|
|
|
|
|
|
if (property.startsWith('--')) {
|
|
|
|
|
|
element.style.setProperty(property, value);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
element.style[property] = value;
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
2025-11-02 10:01:11 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-02 16:28:20 +01:00
|
|
|
|
/**
|
|
|
|
|
|
* Integrate with message system styling
|
|
|
|
|
|
* @param {HTMLElement} element - Section element
|
|
|
|
|
|
*/
|
|
|
|
|
|
integrateWithMessageSystem(element) {
|
|
|
|
|
|
// Add message system integration class
|
|
|
|
|
|
element.classList.add('message-system-ready');
|
2025-11-02 10:01:11 +01:00
|
|
|
|
|
2025-11-02 16:28:20 +01:00
|
|
|
|
// Store reference for message positioning
|
|
|
|
|
|
element.dataset.messageAnchor = 'true';
|
|
|
|
|
|
}
|
2025-11-02 10:01:11 +01:00
|
|
|
|
|
2025-11-02 16:28:20 +01:00
|
|
|
|
/**
|
|
|
|
|
|
* Integrate with control panel styling
|
|
|
|
|
|
* @param {HTMLElement} element - Section element
|
|
|
|
|
|
*/
|
|
|
|
|
|
integrateWithControlPanel(element) {
|
|
|
|
|
|
// Add control panel integration class
|
|
|
|
|
|
element.classList.add('control-panel-aware');
|
|
|
|
|
|
|
|
|
|
|
|
// Adjust positioning to avoid overlap with control panel
|
|
|
|
|
|
const adjustForControlPanel = () => {
|
|
|
|
|
|
const controlPanel = document.getElementById('markitect-global-controls');
|
|
|
|
|
|
if (controlPanel) {
|
|
|
|
|
|
const rect = controlPanel.getBoundingClientRect();
|
|
|
|
|
|
const elementRect = element.getBoundingClientRect();
|
|
|
|
|
|
|
|
|
|
|
|
// Avoid overlap with control panel
|
|
|
|
|
|
if (elementRect.right > rect.left && elementRect.top < rect.bottom) {
|
|
|
|
|
|
element.style.marginRight = `${rect.width + 20}px`;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-11-02 10:01:11 +01:00
|
|
|
|
};
|
|
|
|
|
|
|
2025-11-02 16:28:20 +01:00
|
|
|
|
// Check for overlap periodically
|
|
|
|
|
|
setTimeout(adjustForControlPanel, 100);
|
2025-11-02 10:01:11 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 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
|
|
|
|
|
|
*/
|
2025-11-02 21:19:16 +01:00
|
|
|
|
// createStatusPanel method removed - floating status panel no longer needed
|
|
|
|
|
|
// Status information is displayed in the control panel menu instead
|
2025-11-02 10:01:11 +01:00
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Update status display with current status information
|
|
|
|
|
|
* @param {Object} status - Status object from SectionManager
|
|
|
|
|
|
*/
|
|
|
|
|
|
updateStatusDisplay(status) {
|
2025-11-02 21:19:16 +01:00
|
|
|
|
// Status information is now only displayed in the control panel menu
|
|
|
|
|
|
// No floating status panel needed since status is integrated into the menu
|
|
|
|
|
|
return;
|
2025-11-02 10:01:11 +01:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 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);
|
2025-11-03 19:32:39 +01:00
|
|
|
|
|
|
|
|
|
|
// Set global reference for debug panel
|
|
|
|
|
|
window.currentDOMRenderer = this.domRenderer;
|
|
|
|
|
|
|
|
|
|
|
|
// Initialize debug system for this page load
|
|
|
|
|
|
this.initializeDebugSystem();
|
|
|
|
|
|
|
2025-11-02 10:01:11 +01:00
|
|
|
|
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;
|
|
|
|
|
|
`;
|
|
|
|
|
|
|
2025-11-03 19:32:39 +01:00
|
|
|
|
// Debug button
|
|
|
|
|
|
const debugButton = document.createElement('button');
|
|
|
|
|
|
debugButton.id = 'toggle-debug';
|
|
|
|
|
|
debugButton.textContent = '🔍 Debug';
|
|
|
|
|
|
debugButton.style.cssText = `
|
|
|
|
|
|
background: #6c757d;
|
|
|
|
|
|
color: white;
|
|
|
|
|
|
border: none;
|
|
|
|
|
|
padding: 8px 12px;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
margin-left: 8px;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
transition: background-color 0.2s;
|
|
|
|
|
|
`;
|
|
|
|
|
|
|
2025-11-02 10:01:11 +01:00
|
|
|
|
buttonContainer.appendChild(saveButton);
|
|
|
|
|
|
buttonContainer.appendChild(resetButton);
|
|
|
|
|
|
buttonContainer.appendChild(statusButton);
|
2025-11-03 19:32:39 +01:00
|
|
|
|
buttonContainer.appendChild(debugButton);
|
|
|
|
|
|
|
|
|
|
|
|
// Debug messages container
|
|
|
|
|
|
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;
|
|
|
|
|
|
`;
|
2025-11-02 10:01:11 +01:00
|
|
|
|
|
|
|
|
|
|
controlPanel.appendChild(title);
|
|
|
|
|
|
controlPanel.appendChild(buttonContainer);
|
2025-11-03 19:32:39 +01:00
|
|
|
|
controlPanel.appendChild(debugContainer);
|
2025-11-02 10:01:11 +01:00
|
|
|
|
|
|
|
|
|
|
document.body.appendChild(controlPanel);
|
|
|
|
|
|
|
|
|
|
|
|
// Add event listeners
|
|
|
|
|
|
document.getElementById('save-document').addEventListener('click', () => this.saveDocument());
|
|
|
|
|
|
document.getElementById('reset-all').addEventListener('click', () => this.resetAllSections());
|
2025-11-02 16:28:20 +01:00
|
|
|
|
document.getElementById('show-status').addEventListener('click', () => this.showDocumentStatus());
|
2025-11-03 19:32:39 +01:00
|
|
|
|
document.getElementById('toggle-debug').addEventListener('click', () => this.toggleDebugPanel());
|
2025-11-02 16:28:20 +01:00
|
|
|
|
|
|
|
|
|
|
// Store reference for enhanced control panel methods
|
|
|
|
|
|
this.controlPanel = controlPanel;
|
|
|
|
|
|
|
|
|
|
|
|
// Initialize enhanced features
|
|
|
|
|
|
this.setupControlPanelEnhancements();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Enhanced method to create floating control panel (for TDD compatibility)
|
|
|
|
|
|
* @returns {HTMLElement} The control panel element
|
|
|
|
|
|
*/
|
|
|
|
|
|
createFloatingControlPanel() {
|
|
|
|
|
|
// If control panel already exists, return it
|
|
|
|
|
|
if (this.controlPanel) {
|
|
|
|
|
|
return this.controlPanel;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Create enhanced control panel
|
|
|
|
|
|
this.addGlobalControls();
|
|
|
|
|
|
return this.controlPanel;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Setup enhanced control panel features
|
|
|
|
|
|
*/
|
|
|
|
|
|
setupControlPanelEnhancements() {
|
|
|
|
|
|
if (!this.controlPanel) return;
|
|
|
|
|
|
|
|
|
|
|
|
// Add draggable functionality
|
|
|
|
|
|
this.makeControlPanelDraggable();
|
|
|
|
|
|
|
|
|
|
|
|
// Add collapsible functionality
|
|
|
|
|
|
this.addCollapsibleFeature();
|
|
|
|
|
|
|
|
|
|
|
|
// Add statistics display
|
|
|
|
|
|
this.addStatisticsDisplay();
|
|
|
|
|
|
|
|
|
|
|
|
// Setup keyboard shortcuts
|
|
|
|
|
|
this.setupControlPanelKeyboard();
|
|
|
|
|
|
|
|
|
|
|
|
// Apply responsive design
|
|
|
|
|
|
this.setupResponsiveControlPanel();
|
|
|
|
|
|
|
|
|
|
|
|
// Load user preferences
|
|
|
|
|
|
this.loadControlPanelPreferences();
|
|
|
|
|
|
|
|
|
|
|
|
// Setup animations
|
|
|
|
|
|
this.setupControlPanelAnimations();
|
|
|
|
|
|
|
|
|
|
|
|
// Apply default theme
|
|
|
|
|
|
this.setControlPanelTheme('light');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Make control panel draggable
|
|
|
|
|
|
*/
|
|
|
|
|
|
makeControlPanelDraggable() {
|
|
|
|
|
|
if (!this.controlPanel) return;
|
|
|
|
|
|
|
|
|
|
|
|
const title = this.controlPanel.querySelector('div');
|
|
|
|
|
|
if (title) {
|
|
|
|
|
|
title.style.cursor = 'move';
|
|
|
|
|
|
title.draggable = true;
|
|
|
|
|
|
|
|
|
|
|
|
let isDragging = false;
|
|
|
|
|
|
let startX, startY, initialX, initialY;
|
|
|
|
|
|
|
|
|
|
|
|
title.addEventListener('mousedown', (e) => {
|
|
|
|
|
|
isDragging = true;
|
|
|
|
|
|
startX = e.clientX;
|
|
|
|
|
|
startY = e.clientY;
|
|
|
|
|
|
initialX = this.controlPanel.offsetLeft;
|
|
|
|
|
|
initialY = this.controlPanel.offsetTop;
|
|
|
|
|
|
title.style.userSelect = 'none';
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
document.addEventListener('mousemove', (e) => {
|
|
|
|
|
|
if (!isDragging) return;
|
|
|
|
|
|
|
|
|
|
|
|
const deltaX = e.clientX - startX;
|
|
|
|
|
|
const deltaY = e.clientY - startY;
|
|
|
|
|
|
|
|
|
|
|
|
this.controlPanel.style.left = `${initialX + deltaX}px`;
|
|
|
|
|
|
this.controlPanel.style.top = `${initialY + deltaY}px`;
|
|
|
|
|
|
this.controlPanel.style.right = 'auto'; // Override right positioning
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
document.addEventListener('mouseup', () => {
|
|
|
|
|
|
if (isDragging) {
|
|
|
|
|
|
isDragging = false;
|
|
|
|
|
|
title.style.userSelect = '';
|
|
|
|
|
|
this.saveControlPanelPreferences();
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Add collapsible/expandable functionality
|
|
|
|
|
|
*/
|
|
|
|
|
|
addCollapsibleFeature() {
|
|
|
|
|
|
if (!this.controlPanel) return;
|
|
|
|
|
|
|
|
|
|
|
|
const title = this.controlPanel.querySelector('div');
|
|
|
|
|
|
const buttonContainer = this.controlPanel.querySelector('div:last-child');
|
|
|
|
|
|
|
|
|
|
|
|
if (title && buttonContainer) {
|
|
|
|
|
|
// Add toggle button
|
|
|
|
|
|
const toggleBtn = document.createElement('span');
|
|
|
|
|
|
toggleBtn.textContent = '▼';
|
|
|
|
|
|
toggleBtn.className = 'panel-toggle';
|
|
|
|
|
|
toggleBtn.style.cssText = `
|
|
|
|
|
|
float: right;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
transition: transform 0.3s ease;
|
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
|
|
title.appendChild(toggleBtn);
|
|
|
|
|
|
|
|
|
|
|
|
// Toggle functionality
|
|
|
|
|
|
let isCollapsed = false;
|
|
|
|
|
|
toggleBtn.addEventListener('click', () => {
|
|
|
|
|
|
this.toggleControlPanel();
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Toggle control panel collapsed state
|
|
|
|
|
|
*/
|
|
|
|
|
|
toggleControlPanel() {
|
|
|
|
|
|
if (!this.controlPanel) return;
|
|
|
|
|
|
|
|
|
|
|
|
const buttonContainer = this.controlPanel.querySelector('div:last-child');
|
|
|
|
|
|
const toggleBtn = this.controlPanel.querySelector('.panel-toggle');
|
|
|
|
|
|
|
|
|
|
|
|
if (buttonContainer && toggleBtn) {
|
|
|
|
|
|
const isCollapsed = buttonContainer.style.display === 'none';
|
|
|
|
|
|
|
|
|
|
|
|
if (isCollapsed) {
|
|
|
|
|
|
buttonContainer.style.display = 'flex';
|
|
|
|
|
|
toggleBtn.textContent = '▼';
|
|
|
|
|
|
toggleBtn.style.transform = 'rotate(0deg)';
|
|
|
|
|
|
this.controlPanel.classList.remove('collapsed');
|
|
|
|
|
|
} else {
|
|
|
|
|
|
buttonContainer.style.display = 'none';
|
|
|
|
|
|
toggleBtn.textContent = '▶';
|
|
|
|
|
|
toggleBtn.style.transform = 'rotate(-90deg)';
|
|
|
|
|
|
this.controlPanel.classList.add('collapsed');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
this.saveControlPanelPreferences();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Add real-time statistics display
|
|
|
|
|
|
*/
|
|
|
|
|
|
addStatisticsDisplay() {
|
|
|
|
|
|
if (!this.controlPanel) return;
|
|
|
|
|
|
|
|
|
|
|
|
const statsDiv = document.createElement('div');
|
|
|
|
|
|
statsDiv.className = 'control-panel-stats';
|
|
|
|
|
|
statsDiv.style.cssText = `
|
|
|
|
|
|
margin-top: 8px;
|
|
|
|
|
|
padding-top: 8px;
|
|
|
|
|
|
border-top: 1px solid #dee2e6;
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
color: #6c757d;
|
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
|
|
// Insert before button container
|
|
|
|
|
|
const buttonContainer = this.controlPanel.querySelector('div:last-child');
|
|
|
|
|
|
this.controlPanel.insertBefore(statsDiv, buttonContainer);
|
|
|
|
|
|
|
|
|
|
|
|
// Update stats periodically
|
|
|
|
|
|
this.updateControlPanelStats();
|
|
|
|
|
|
setInterval(() => this.updateControlPanelStats(), 3000);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Update control panel statistics
|
|
|
|
|
|
*/
|
|
|
|
|
|
updateControlPanelStats() {
|
|
|
|
|
|
const statsDiv = this.controlPanel?.querySelector('.control-panel-stats');
|
|
|
|
|
|
if (!statsDiv) return;
|
|
|
|
|
|
|
|
|
|
|
|
const status = this.sectionManager.getDocumentStatus();
|
|
|
|
|
|
const eventStats = this.domRenderer.getEventStats();
|
|
|
|
|
|
|
|
|
|
|
|
statsDiv.innerHTML = `
|
|
|
|
|
|
<div class="stat-item"><strong>${status.totalSections}</strong> sections</div>
|
|
|
|
|
|
<div class="stat-item"><strong>${status.editingSections}</strong> editing</div>
|
|
|
|
|
|
<div class="stat-item"><strong>${eventStats.totalEvents}</strong> events</div>
|
|
|
|
|
|
`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Setup keyboard shortcuts for control panel
|
|
|
|
|
|
*/
|
|
|
|
|
|
setupControlPanelKeyboard() {
|
|
|
|
|
|
document.addEventListener('keydown', (e) => {
|
|
|
|
|
|
this.handleControlPanelKeyboard(e);
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Handle control panel keyboard shortcuts
|
|
|
|
|
|
* @param {KeyboardEvent} e - Keyboard event
|
|
|
|
|
|
*/
|
|
|
|
|
|
handleControlPanelKeyboard(e) {
|
|
|
|
|
|
// Ctrl+P: Toggle panel
|
|
|
|
|
|
if (e.ctrlKey && e.key === 'p') {
|
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
this.toggleControlPanel();
|
|
|
|
|
|
this.domRenderer.trackEvent('keyboard-shortcut', { shortcut: 'ctrl+p', action: 'toggle-panel' });
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Ctrl+S: Save document
|
|
|
|
|
|
if (e.ctrlKey && e.key === 's') {
|
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
this.saveDocument();
|
|
|
|
|
|
this.domRenderer.trackEvent('keyboard-shortcut', { shortcut: 'ctrl+s', action: 'save' });
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Ctrl+Shift+S: Show status
|
|
|
|
|
|
if (e.ctrlKey && e.shiftKey && e.key === 'S') {
|
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
this.showDocumentStatus();
|
|
|
|
|
|
this.domRenderer.trackEvent('keyboard-shortcut', { shortcut: 'ctrl+shift+s', action: 'status' });
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Setup responsive design for control panel
|
|
|
|
|
|
*/
|
|
|
|
|
|
setupResponsiveControlPanel() {
|
|
|
|
|
|
this.adjustControlPanelForViewport(window.innerWidth);
|
|
|
|
|
|
|
|
|
|
|
|
window.addEventListener('resize', () => {
|
|
|
|
|
|
this.adjustControlPanelForViewport(window.innerWidth);
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Adjust control panel for different viewport sizes
|
|
|
|
|
|
* @param {number} width - Viewport width
|
|
|
|
|
|
*/
|
|
|
|
|
|
adjustControlPanelForViewport(width) {
|
|
|
|
|
|
if (!this.controlPanel) return;
|
|
|
|
|
|
|
|
|
|
|
|
if (width < 768) {
|
|
|
|
|
|
// Mobile layout
|
|
|
|
|
|
this.controlPanel.style.cssText += `
|
|
|
|
|
|
top: 10px;
|
|
|
|
|
|
right: 10px;
|
|
|
|
|
|
left: auto;
|
|
|
|
|
|
min-width: 150px;
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
`;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// Desktop layout
|
|
|
|
|
|
this.controlPanel.style.cssText += `
|
|
|
|
|
|
top: 20px;
|
|
|
|
|
|
right: 20px;
|
|
|
|
|
|
left: auto;
|
|
|
|
|
|
min-width: 200px;
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
`;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Save control panel preferences to localStorage
|
|
|
|
|
|
*/
|
|
|
|
|
|
saveControlPanelPreferences() {
|
|
|
|
|
|
if (!this.controlPanel) return;
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const preferences = {
|
|
|
|
|
|
top: this.controlPanel.style.top,
|
|
|
|
|
|
left: this.controlPanel.style.left,
|
|
|
|
|
|
right: this.controlPanel.style.right,
|
|
|
|
|
|
collapsed: this.controlPanel.classList.contains('collapsed'),
|
|
|
|
|
|
theme: this.controlPanel.dataset.theme || 'light'
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
localStorage.setItem('markitect-control-panel-prefs', JSON.stringify(preferences));
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.warn('Could not save control panel preferences:', error);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Load control panel preferences from localStorage
|
|
|
|
|
|
*/
|
|
|
|
|
|
loadControlPanelPreferences() {
|
|
|
|
|
|
if (!this.controlPanel) return;
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const saved = localStorage.getItem('markitect-control-panel-prefs');
|
|
|
|
|
|
if (saved) {
|
|
|
|
|
|
const preferences = JSON.parse(saved);
|
|
|
|
|
|
|
|
|
|
|
|
if (preferences.top) this.controlPanel.style.top = preferences.top;
|
|
|
|
|
|
if (preferences.left) this.controlPanel.style.left = preferences.left;
|
|
|
|
|
|
if (preferences.right) this.controlPanel.style.right = preferences.right;
|
|
|
|
|
|
|
|
|
|
|
|
if (preferences.collapsed) {
|
|
|
|
|
|
this.toggleControlPanel();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (preferences.theme) {
|
|
|
|
|
|
this.setControlPanelTheme(preferences.theme);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.warn('Could not load control panel preferences:', error);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Setup control panel animations
|
|
|
|
|
|
*/
|
|
|
|
|
|
setupControlPanelAnimations() {
|
|
|
|
|
|
if (!this.controlPanel) return;
|
|
|
|
|
|
|
|
|
|
|
|
this.controlPanel.style.transition = 'all 0.3s ease';
|
|
|
|
|
|
this.animateControlPanel('fadeIn');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Animate control panel
|
|
|
|
|
|
* @param {string} animation - Animation type
|
|
|
|
|
|
*/
|
|
|
|
|
|
animateControlPanel(animation) {
|
|
|
|
|
|
if (!this.controlPanel) return;
|
|
|
|
|
|
|
|
|
|
|
|
switch (animation) {
|
|
|
|
|
|
case 'fadeIn':
|
|
|
|
|
|
this.controlPanel.style.opacity = '0';
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
this.controlPanel.style.opacity = '1';
|
|
|
|
|
|
}, 100);
|
|
|
|
|
|
break;
|
|
|
|
|
|
case 'slideIn':
|
|
|
|
|
|
this.controlPanel.style.transform = 'translateX(100%)';
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
this.controlPanel.style.transform = 'translateX(0)';
|
|
|
|
|
|
}, 100);
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Set control panel theme
|
|
|
|
|
|
* @param {string} theme - Theme name ('light', 'dark')
|
|
|
|
|
|
*/
|
|
|
|
|
|
setControlPanelTheme(theme) {
|
|
|
|
|
|
if (!this.controlPanel) return;
|
|
|
|
|
|
|
|
|
|
|
|
this.controlPanel.dataset.theme = theme;
|
|
|
|
|
|
|
|
|
|
|
|
const themes = {
|
|
|
|
|
|
light: {
|
|
|
|
|
|
background: 'rgba(248, 249, 250, 0.95)',
|
|
|
|
|
|
border: '#dee2e6',
|
|
|
|
|
|
text: '#495057',
|
|
|
|
|
|
buttonColors: {
|
|
|
|
|
|
save: '#28a745',
|
|
|
|
|
|
reset: '#ffc107',
|
|
|
|
|
|
status: '#17a2b8'
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
dark: {
|
|
|
|
|
|
background: 'rgba(33, 37, 41, 0.95)',
|
|
|
|
|
|
border: '#495057',
|
|
|
|
|
|
text: '#f8f9fa',
|
|
|
|
|
|
buttonColors: {
|
|
|
|
|
|
save: '#198754',
|
|
|
|
|
|
reset: '#fd7e14',
|
|
|
|
|
|
status: '#0dcaf0'
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const selectedTheme = themes[theme] || themes.light;
|
|
|
|
|
|
|
|
|
|
|
|
this.controlPanel.style.background = selectedTheme.background;
|
|
|
|
|
|
this.controlPanel.style.borderColor = selectedTheme.border;
|
|
|
|
|
|
this.controlPanel.style.color = selectedTheme.text;
|
|
|
|
|
|
|
|
|
|
|
|
// Update button colors
|
|
|
|
|
|
const buttons = this.controlPanel.querySelectorAll('button');
|
|
|
|
|
|
buttons.forEach((button, index) => {
|
|
|
|
|
|
const colors = Object.values(selectedTheme.buttonColors);
|
|
|
|
|
|
if (colors[index]) {
|
|
|
|
|
|
button.style.backgroundColor = colors[index];
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
this.saveControlPanelPreferences();
|
2025-11-02 10:01:11 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-02 16:28:20 +01:00
|
|
|
|
showMessage(message, type = 'info', options = {}) {
|
|
|
|
|
|
// Enhanced professional message system with color-coded positioning
|
|
|
|
|
|
const {
|
|
|
|
|
|
position = 'top-center',
|
|
|
|
|
|
duration = 3000,
|
|
|
|
|
|
dismissible = true,
|
|
|
|
|
|
icon = true,
|
|
|
|
|
|
animation = true
|
|
|
|
|
|
} = options;
|
|
|
|
|
|
|
2025-11-02 10:01:11 +01:00
|
|
|
|
const messageDiv = document.createElement('div');
|
2025-11-02 16:28:20 +01:00
|
|
|
|
messageDiv.className = `markitect-message markitect-message-${type}`;
|
|
|
|
|
|
|
|
|
|
|
|
// Get positioning styles
|
|
|
|
|
|
const positionStyles = this.getMessagePositionStyles(position);
|
|
|
|
|
|
|
|
|
|
|
|
// Get color scheme for message type
|
|
|
|
|
|
const colors = this.getMessageColors(type);
|
|
|
|
|
|
|
|
|
|
|
|
// Create icon if enabled
|
|
|
|
|
|
const iconHtml = icon ? this.getMessageIcon(type) : '';
|
|
|
|
|
|
|
2025-11-02 10:01:11 +01:00
|
|
|
|
messageDiv.style.cssText = `
|
|
|
|
|
|
position: fixed;
|
2025-11-02 16:28:20 +01:00
|
|
|
|
${positionStyles}
|
|
|
|
|
|
background: ${colors.background};
|
|
|
|
|
|
color: ${colors.text};
|
|
|
|
|
|
border: 1px solid ${colors.border};
|
|
|
|
|
|
border-left: 4px solid ${colors.accent};
|
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
padding: 16px 20px;
|
|
|
|
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
2025-11-02 10:01:11 +01:00
|
|
|
|
font-size: 14px;
|
2025-11-02 16:28:20 +01:00
|
|
|
|
font-weight: 500;
|
2025-11-02 10:01:11 +01:00
|
|
|
|
z-index: 10001;
|
2025-11-02 16:28:20 +01:00
|
|
|
|
box-shadow: 0 8px 24px rgba(0,0,0,0.12), 0 4px 8px rgba(0,0,0,0.08);
|
|
|
|
|
|
backdrop-filter: blur(8px);
|
|
|
|
|
|
max-width: 400px;
|
|
|
|
|
|
min-width: 200px;
|
|
|
|
|
|
transform: ${animation ? 'translateY(-20px)' : 'none'};
|
|
|
|
|
|
opacity: ${animation ? '0' : '1'};
|
|
|
|
|
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
|
|
|
|
cursor: ${dismissible ? 'pointer' : 'default'};
|
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
|
|
// Set message content with icon
|
|
|
|
|
|
messageDiv.innerHTML = `
|
|
|
|
|
|
<div style="display: flex; align-items: flex-start; gap: 12px;">
|
|
|
|
|
|
${iconHtml}
|
|
|
|
|
|
<div style="flex: 1; line-height: 1.4;">
|
|
|
|
|
|
${message}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
${dismissible ? '<div style="margin-left: auto; opacity: 0.7; font-size: 16px; line-height: 1;">×</div>' : ''}
|
|
|
|
|
|
</div>
|
2025-11-02 10:01:11 +01:00
|
|
|
|
`;
|
|
|
|
|
|
|
2025-11-02 16:28:20 +01:00
|
|
|
|
// Add to DOM and handle stacking
|
2025-11-02 10:01:11 +01:00
|
|
|
|
document.body.appendChild(messageDiv);
|
2025-11-02 16:28:20 +01:00
|
|
|
|
this.stackMessages();
|
2025-11-02 10:01:11 +01:00
|
|
|
|
|
2025-11-02 16:28:20 +01:00
|
|
|
|
// Animation entrance
|
|
|
|
|
|
if (animation) {
|
|
|
|
|
|
requestAnimationFrame(() => {
|
|
|
|
|
|
messageDiv.style.transform = 'translateY(0)';
|
|
|
|
|
|
messageDiv.style.opacity = '1';
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Auto-dismiss functionality
|
|
|
|
|
|
const autoRemove = () => {
|
2025-11-02 10:01:11 +01:00
|
|
|
|
if (messageDiv.parentNode) {
|
2025-11-02 16:28:20 +01:00
|
|
|
|
messageDiv.style.transform = 'translateY(-20px)';
|
|
|
|
|
|
messageDiv.style.opacity = '0';
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
if (messageDiv.parentNode) {
|
|
|
|
|
|
messageDiv.parentNode.removeChild(messageDiv);
|
|
|
|
|
|
this.stackMessages();
|
|
|
|
|
|
}
|
|
|
|
|
|
}, 300);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// Manual dismiss functionality
|
|
|
|
|
|
if (dismissible) {
|
|
|
|
|
|
messageDiv.addEventListener('click', autoRemove);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Auto-dismiss timer
|
|
|
|
|
|
if (duration > 0) {
|
|
|
|
|
|
setTimeout(autoRemove, duration);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return messageDiv;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Get position styles for message positioning
|
|
|
|
|
|
* @param {string} position - Position identifier
|
|
|
|
|
|
* @returns {string} CSS positioning styles
|
|
|
|
|
|
*/
|
|
|
|
|
|
getMessagePositionStyles(position) {
|
|
|
|
|
|
const positions = {
|
|
|
|
|
|
'top-left': 'top: 20px; left: 20px;',
|
|
|
|
|
|
'top-center': 'top: 20px; left: 50%; transform: translateX(-50%);',
|
|
|
|
|
|
'top-right': 'top: 20px; right: 20px;',
|
|
|
|
|
|
'center': 'top: 50%; left: 50%; transform: translate(-50%, -50%);',
|
|
|
|
|
|
'bottom-left': 'bottom: 20px; left: 20px;',
|
|
|
|
|
|
'bottom-center': 'bottom: 20px; left: 50%; transform: translateX(-50%);',
|
|
|
|
|
|
'bottom-right': 'bottom: 20px; right: 20px;'
|
|
|
|
|
|
};
|
|
|
|
|
|
return positions[position] || positions['top-center'];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Get color scheme for message type
|
|
|
|
|
|
* @param {string} type - Message type
|
|
|
|
|
|
* @returns {Object} Color scheme object
|
|
|
|
|
|
*/
|
|
|
|
|
|
getMessageColors(type) {
|
|
|
|
|
|
const schemes = {
|
|
|
|
|
|
success: {
|
|
|
|
|
|
background: '#d4edda',
|
|
|
|
|
|
text: '#155724',
|
|
|
|
|
|
border: '#c3e6cb',
|
|
|
|
|
|
accent: '#28a745'
|
|
|
|
|
|
},
|
|
|
|
|
|
error: {
|
|
|
|
|
|
background: '#f8d7da',
|
|
|
|
|
|
text: '#721c24',
|
|
|
|
|
|
border: '#f5c6cb',
|
|
|
|
|
|
accent: '#dc3545'
|
|
|
|
|
|
},
|
|
|
|
|
|
warning: {
|
|
|
|
|
|
background: '#fff3cd',
|
|
|
|
|
|
text: '#856404',
|
|
|
|
|
|
border: '#ffeaa7',
|
|
|
|
|
|
accent: '#ffc107'
|
|
|
|
|
|
},
|
|
|
|
|
|
info: {
|
|
|
|
|
|
background: '#d1ecf1',
|
|
|
|
|
|
text: '#0c5460',
|
|
|
|
|
|
border: '#bee5eb',
|
|
|
|
|
|
accent: '#17a2b8'
|
|
|
|
|
|
},
|
|
|
|
|
|
debug: {
|
|
|
|
|
|
background: '#e2e3e5',
|
|
|
|
|
|
text: '#383d41',
|
|
|
|
|
|
border: '#d6d8db',
|
|
|
|
|
|
accent: '#6c757d'
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
return schemes[type] || schemes.info;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Get icon HTML for message type
|
|
|
|
|
|
* @param {string} type - Message type
|
|
|
|
|
|
* @returns {string} Icon HTML
|
|
|
|
|
|
*/
|
|
|
|
|
|
getMessageIcon(type) {
|
|
|
|
|
|
const icons = {
|
|
|
|
|
|
success: '<div style="color: #28a745; font-size: 18px; font-weight: bold;">✓</div>',
|
|
|
|
|
|
error: '<div style="color: #dc3545; font-size: 18px; font-weight: bold;">✕</div>',
|
|
|
|
|
|
warning: '<div style="color: #ffc107; font-size: 18px; font-weight: bold;">⚠</div>',
|
|
|
|
|
|
info: '<div style="color: #17a2b8; font-size: 18px; font-weight: bold;">ℹ</div>',
|
|
|
|
|
|
debug: '<div style="color: #6c757d; font-size: 18px; font-weight: bold;">🐛</div>'
|
|
|
|
|
|
};
|
|
|
|
|
|
return icons[type] || icons.info;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Stack messages to prevent overlap
|
|
|
|
|
|
*/
|
|
|
|
|
|
stackMessages() {
|
|
|
|
|
|
const messages = Array.from(document.querySelectorAll('.markitect-message')).filter(el =>
|
|
|
|
|
|
el.style.display !== 'none' && el.parentNode
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
// Group messages by position
|
|
|
|
|
|
const messageGroups = {
|
|
|
|
|
|
'top': [],
|
|
|
|
|
|
'center': [],
|
|
|
|
|
|
'bottom': []
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
messages.forEach(msg => {
|
|
|
|
|
|
const styles = msg.style;
|
|
|
|
|
|
if (styles.top && styles.top !== 'auto' && styles.top !== '') {
|
|
|
|
|
|
messageGroups.top.push(msg);
|
|
|
|
|
|
} else if (styles.bottom && styles.bottom !== 'auto' && styles.bottom !== '') {
|
|
|
|
|
|
messageGroups.bottom.push(msg);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
messageGroups.center.push(msg);
|
2025-11-02 10:01:11 +01:00
|
|
|
|
}
|
2025-11-02 16:28:20 +01:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// Stack top messages downward
|
|
|
|
|
|
let topOffset = 20;
|
|
|
|
|
|
messageGroups.top.forEach(msg => {
|
|
|
|
|
|
msg.style.top = `${topOffset}px`;
|
|
|
|
|
|
topOffset += msg.offsetHeight + 12;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// Stack bottom messages upward
|
|
|
|
|
|
let bottomOffset = 20;
|
|
|
|
|
|
messageGroups.bottom.forEach(msg => {
|
|
|
|
|
|
msg.style.bottom = `${bottomOffset}px`;
|
|
|
|
|
|
bottomOffset += msg.offsetHeight + 12;
|
|
|
|
|
|
});
|
2025-11-02 10:01:11 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-02 16:28:20 +01:00
|
|
|
|
/**
|
|
|
|
|
|
* Show comprehensive document status dialog with detailed statistics
|
|
|
|
|
|
*/
|
|
|
|
|
|
showDocumentStatus() {
|
|
|
|
|
|
const status = this.sectionManager.getDocumentStatus();
|
|
|
|
|
|
const eventStats = this.domRenderer.getEventStats();
|
|
|
|
|
|
const sections = this.sectionManager.getAllSections();
|
|
|
|
|
|
|
|
|
|
|
|
// Calculate additional statistics
|
|
|
|
|
|
const sectionTypes = {};
|
|
|
|
|
|
const sectionSizes = { small: 0, medium: 0, large: 0 };
|
|
|
|
|
|
let totalCharacters = 0;
|
|
|
|
|
|
let averageLength = 0;
|
|
|
|
|
|
|
|
|
|
|
|
sections.forEach(section => {
|
|
|
|
|
|
const type = section.type || 'paragraph';
|
|
|
|
|
|
sectionTypes[type] = (sectionTypes[type] || 0) + 1;
|
|
|
|
|
|
|
|
|
|
|
|
const length = section.currentMarkdown.length;
|
|
|
|
|
|
totalCharacters += length;
|
|
|
|
|
|
|
|
|
|
|
|
if (length < 100) sectionSizes.small++;
|
|
|
|
|
|
else if (length < 500) sectionSizes.medium++;
|
|
|
|
|
|
else sectionSizes.large++;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (sections.length > 0) {
|
|
|
|
|
|
averageLength = Math.round(totalCharacters / sections.length);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Create comprehensive status HTML
|
|
|
|
|
|
const statusHtml = `
|
|
|
|
|
|
<div style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; line-height: 1.6;">
|
|
|
|
|
|
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin-bottom: 24px;">
|
|
|
|
|
|
<!-- Document Overview -->
|
|
|
|
|
|
<div style="background: #f8f9fa; padding: 16px; border-radius: 8px; border-left: 4px solid #007acc;">
|
|
|
|
|
|
<h3 style="margin: 0 0 12px 0; color: #007acc; font-size: 16px;">📄 Document Overview</h3>
|
|
|
|
|
|
<div style="display: grid; gap: 8px;">
|
|
|
|
|
|
<div><strong>Total Sections:</strong> ${status.totalSections}</div>
|
|
|
|
|
|
<div><strong>Total Characters:</strong> ${totalCharacters.toLocaleString()}</div>
|
|
|
|
|
|
<div><strong>Average Section Length:</strong> ${averageLength} chars</div>
|
|
|
|
|
|
<div><strong>Document Status:</strong>
|
|
|
|
|
|
<span style="color: ${status.hasUnsavedChanges ? '#f39c12' : '#27ae60'}; font-weight: bold;">
|
|
|
|
|
|
${status.hasUnsavedChanges ? '⚠️ Has Changes' : '✅ All Saved'}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Section States -->
|
|
|
|
|
|
<div style="background: #f8f9fa; padding: 16px; border-radius: 8px; border-left: 4px solid #27ae60;">
|
|
|
|
|
|
<h3 style="margin: 0 0 12px 0; color: #27ae60; font-size: 16px;">📝 Section States</h3>
|
|
|
|
|
|
<div style="display: grid; gap: 8px;">
|
|
|
|
|
|
<div><strong>Currently Editing:</strong> ${status.editingSections} sections</div>
|
|
|
|
|
|
<div><strong>Modified (Unsaved):</strong> ${status.modifiedSections} sections</div>
|
|
|
|
|
|
<div><strong>Saved:</strong> ${status.savedSections} sections</div>
|
|
|
|
|
|
<div><strong>Original:</strong> ${status.totalSections - status.modifiedSections - status.savedSections} sections</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin-bottom: 24px;">
|
|
|
|
|
|
<!-- Section Types -->
|
|
|
|
|
|
<div style="background: #f8f9fa; padding: 16px; border-radius: 8px; border-left: 4px solid #8e44ad;">
|
|
|
|
|
|
<h3 style="margin: 0 0 12px 0; color: #8e44ad; font-size: 16px;">🏷️ Section Types</h3>
|
|
|
|
|
|
<div style="display: grid; gap: 6px; font-size: 14px;">
|
|
|
|
|
|
${Object.entries(sectionTypes).map(([type, count]) =>
|
|
|
|
|
|
`<div><strong>${type.charAt(0).toUpperCase() + type.slice(1)}:</strong> ${count}</div>`
|
|
|
|
|
|
).join('')}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Section Sizes -->
|
|
|
|
|
|
<div style="background: #f8f9fa; padding: 16px; border-radius: 8px; border-left: 4px solid #e67e22;">
|
|
|
|
|
|
<h3 style="margin: 0 0 12px 0; color: #e67e22; font-size: 16px;">📏 Section Sizes</h3>
|
|
|
|
|
|
<div style="display: grid; gap: 6px; font-size: 14px;">
|
|
|
|
|
|
<div><strong>Small (<100 chars):</strong> ${sectionSizes.small}</div>
|
|
|
|
|
|
<div><strong>Medium (100-500 chars):</strong> ${sectionSizes.medium}</div>
|
|
|
|
|
|
<div><strong>Large (>500 chars):</strong> ${sectionSizes.large}</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Event Statistics -->
|
|
|
|
|
|
<div style="background: #f8f9fa; padding: 16px; border-radius: 8px; border-left: 4px solid #3498db; margin-bottom: 20px;">
|
|
|
|
|
|
<h3 style="margin: 0 0 12px 0; color: #3498db; font-size: 16px;">⚡ Event Statistics</h3>
|
|
|
|
|
|
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 12px; font-size: 14px;">
|
|
|
|
|
|
<div><strong>Total Events:</strong> ${eventStats.totalEvents}</div>
|
|
|
|
|
|
<div><strong>Section Clicks:</strong> ${eventStats.stats['section-click'] || 0}</div>
|
|
|
|
|
|
<div><strong>Hover Events:</strong> ${(eventStats.stats['section-hover-enter'] || 0) + (eventStats.stats['section-hover-leave'] || 0)}</div>
|
|
|
|
|
|
<div><strong>Keyboard Shortcuts:</strong> ${eventStats.stats['keyboard-shortcut'] || 0}</div>
|
|
|
|
|
|
<div><strong>Context Menus:</strong> ${eventStats.stats['section-context-menu'] || 0}</div>
|
|
|
|
|
|
<div><strong>Drag Events:</strong> ${(eventStats.stats['section-drag-start'] || 0) + (eventStats.stats['section-drag-over'] || 0)}</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Recent Activity -->
|
|
|
|
|
|
${eventStats.recentEvents.length > 0 ? `
|
|
|
|
|
|
<div style="background: #f8f9fa; padding: 16px; border-radius: 8px; border-left: 4px solid #9b59b6;">
|
|
|
|
|
|
<h3 style="margin: 0 0 12px 0; color: #9b59b6; font-size: 16px;">🕒 Recent Activity (Last 10 Events)</h3>
|
|
|
|
|
|
<div style="max-height: 200px; overflow-y: auto;">
|
|
|
|
|
|
${eventStats.recentEvents.slice(-10).reverse().map(event => `
|
|
|
|
|
|
<div style="padding: 6px; margin: 4px 0; background: white; border-radius: 4px; font-size: 13px; border-left: 3px solid #ddd;">
|
|
|
|
|
|
<strong>${event.type}</strong> - ${event.timestamp}
|
|
|
|
|
|
${event.data.sectionId ? `<br/><span style="color: #666;">Section: ${event.data.sectionId.substring(0, 12)}...</span>` : ''}
|
|
|
|
|
|
${event.data.shortcut ? `<br/><span style="color: #666;">Shortcut: ${event.data.shortcut}</span>` : ''}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
`).join('')}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
` : ''}
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Section Details Table -->
|
|
|
|
|
|
<div style="margin-top: 20px;">
|
|
|
|
|
|
<h3 style="margin: 0 0 12px 0; color: #2c3e50; font-size: 16px;">📋 Section Details</h3>
|
|
|
|
|
|
<div style="max-height: 300px; overflow-y: auto; border: 1px solid #ddd; border-radius: 4px;">
|
|
|
|
|
|
<table style="width: 100%; border-collapse: collapse; font-size: 13px;">
|
|
|
|
|
|
<thead style="background: #34495e; color: white; position: sticky; top: 0;">
|
|
|
|
|
|
<tr>
|
|
|
|
|
|
<th style="padding: 8px; text-align: left; border-bottom: 1px solid #ddd;">Section</th>
|
|
|
|
|
|
<th style="padding: 8px; text-align: left; border-bottom: 1px solid #ddd;">Type</th>
|
|
|
|
|
|
<th style="padding: 8px; text-align: left; border-bottom: 1px solid #ddd;">State</th>
|
|
|
|
|
|
<th style="padding: 8px; text-align: left; border-bottom: 1px solid #ddd;">Length</th>
|
|
|
|
|
|
<th style="padding: 8px; text-align: left; border-bottom: 1px solid #ddd;">Changes</th>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
</thead>
|
|
|
|
|
|
<tbody>
|
|
|
|
|
|
${sections.map((section, index) => {
|
|
|
|
|
|
const hasChanges = section.editingMarkdown !== section.currentMarkdown;
|
|
|
|
|
|
const stateColor = section.state === 'editing' ? '#3498db' :
|
|
|
|
|
|
section.state === 'modified' ? '#f39c12' :
|
|
|
|
|
|
section.state === 'saved' ? '#27ae60' : '#95a5a6';
|
|
|
|
|
|
const preview = section.currentMarkdown.substring(0, 40).replace(/\n/g, ' ') +
|
|
|
|
|
|
(section.currentMarkdown.length > 40 ? '...' : '');
|
|
|
|
|
|
|
|
|
|
|
|
return `
|
|
|
|
|
|
<tr style="border-bottom: 1px solid #eee;">
|
|
|
|
|
|
<td style="padding: 8px; max-width: 200px; overflow: hidden; text-overflow: ellipsis;" title="${section.currentMarkdown.substring(0, 100)}">
|
|
|
|
|
|
${preview}
|
|
|
|
|
|
</td>
|
|
|
|
|
|
<td style="padding: 8px;">
|
|
|
|
|
|
<span style="background: #ecf0f1; padding: 2px 6px; border-radius: 3px; font-size: 11px;">
|
|
|
|
|
|
${section.type || 'paragraph'}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</td>
|
|
|
|
|
|
<td style="padding: 8px;">
|
|
|
|
|
|
<span style="color: ${stateColor}; font-weight: bold;">
|
|
|
|
|
|
${section.state || 'original'}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</td>
|
|
|
|
|
|
<td style="padding: 8px;">${section.currentMarkdown.length} chars</td>
|
|
|
|
|
|
<td style="padding: 8px; color: ${hasChanges ? '#e74c3c' : '#27ae60'};">
|
|
|
|
|
|
${hasChanges ? '●' : '○'}
|
|
|
|
|
|
</td>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
`;
|
|
|
|
|
|
}).join('')}
|
|
|
|
|
|
</tbody>
|
|
|
|
|
|
</table>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
|
|
this.showModal('📊 Comprehensive Document Status', statusHtml);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-03 19:32:39 +01:00
|
|
|
|
/**
|
|
|
|
|
|
* Toggle the debug panel on/off
|
|
|
|
|
|
*/
|
|
|
|
|
|
toggleDebugPanel() {
|
|
|
|
|
|
console.log('toggleDebugPanel called');
|
|
|
|
|
|
const debugContainer = document.getElementById('debug-messages-container');
|
|
|
|
|
|
const debugButton = document.getElementById('toggle-debug');
|
|
|
|
|
|
|
|
|
|
|
|
console.log('Elements found:', {
|
|
|
|
|
|
debugContainer: !!debugContainer,
|
|
|
|
|
|
debugButton: !!debugButton
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (!debugContainer || !debugButton) return;
|
|
|
|
|
|
|
|
|
|
|
|
if (debugPanelActive) {
|
|
|
|
|
|
// Hide debug panel
|
|
|
|
|
|
console.log('Hiding debug panel');
|
|
|
|
|
|
debugContainer.style.display = 'none';
|
|
|
|
|
|
debugButton.textContent = '🔍 Debug';
|
|
|
|
|
|
debugButton.style.background = '#6c757d';
|
|
|
|
|
|
debugPanelActive = false;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// Show debug panel
|
|
|
|
|
|
console.log('Showing debug panel');
|
|
|
|
|
|
debugContainer.style.display = 'block';
|
|
|
|
|
|
debugButton.textContent = '🔍 Debug (ON)';
|
|
|
|
|
|
debugButton.style.background = '#28a745';
|
|
|
|
|
|
debugPanelActive = true;
|
|
|
|
|
|
console.log('About to call this.updateDebugPanel()');
|
|
|
|
|
|
this.updateDebugPanel();
|
|
|
|
|
|
}
|
|
|
|
|
|
console.log('debugPanelActive is now:', debugPanelActive);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Update the debug panel with current messages
|
|
|
|
|
|
*/
|
|
|
|
|
|
updateDebugPanel() {
|
|
|
|
|
|
const debugContainer = document.getElementById('debug-messages-container');
|
|
|
|
|
|
if (!debugContainer || !debugPanelActive) {
|
|
|
|
|
|
console.log('updateDebugPanel skipped:', {
|
|
|
|
|
|
hasContainer: !!debugContainer,
|
|
|
|
|
|
debugPanelActive
|
|
|
|
|
|
});
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
console.log('updateDebugPanel running, messages count:', debugMessages.length);
|
|
|
|
|
|
|
|
|
|
|
|
if (debugMessages.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 = debugMessages.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 (${debugMessages.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', () => {
|
|
|
|
|
|
console.log('Clear button clicked'); // Debug log
|
|
|
|
|
|
window.clearDebugMessages();
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Auto-scroll to bottom to show newest messages
|
|
|
|
|
|
const scrollContainer = debugContainer.querySelector('div[style*="overflow-y"]');
|
|
|
|
|
|
if (scrollContainer) {
|
|
|
|
|
|
scrollContainer.scrollTop = scrollContainer.scrollHeight;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Initialize the debug system for this page load
|
|
|
|
|
|
*/
|
|
|
|
|
|
initializeDebugSystem() {
|
|
|
|
|
|
// Clear any debug messages from rendering time
|
|
|
|
|
|
debugMessages = [];
|
|
|
|
|
|
debugPanelActive = false;
|
|
|
|
|
|
|
|
|
|
|
|
console.log('Debug system initialized - messages cleared for new session');
|
|
|
|
|
|
|
|
|
|
|
|
// Add a welcome message to show the system is working
|
|
|
|
|
|
debug('initializeDebugSystem: Debug system ready for new session', 'INFO');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-02 10:01:11 +01:00
|
|
|
|
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;
|
|
|
|
|
|
}
|