/**
* Overlay Editor Module
* Handles the edit selection overlay functionality for the Ace editor
*/
(function() {
'use strict';
console.log("[overlay_editor.js] Loading overlay editor module...");
// =========================================================================
// MODULE STATE
// =========================================================================
let overlayEditorInstance = null;
let selectionRange = null;
let editHistory = [];
let currentEditIndex = 0;
// =========================================================================
// DEPENDENCIES (injected from main editor)
// =========================================================================
let deps = {
getGlobalEditor: null, // Function that returns globalEditorInstance
getEl: null, // Function that returns el (DOM element)
showOverlay: null, // Function to show overlay
hideOverlay: null, // Function to hide overlay
generateDocumentIndex: null,// Function to generate document index
findMarkerEnd: null, // Function to find marker end
escapeHtml: null, // Function to escape HTML
showToast: null, // Function to show toast notifications
onHideOverlay: null // Callback when overlay is hidden
};
// =========================================================================
// INITIALIZATION
// =========================================================================
function init(dependencies) {
deps = { ...deps, ...dependencies };
console.log("[overlay_editor.js] Initialized with dependencies");
}
// =========================================================================
// MAIN OVERLAY FUNCTION
// =========================================================================
function showEditSelectionOverlay() {
const globalEditorInstance = deps.getGlobalEditor();
if (!globalEditorInstance) {
console.error("[overlay_editor.js] Global editor instance not available");
return;
}
// Get current selection or cursor position
const selected = globalEditorInstance.getSelectedText();
const hasInitialSelection = selected && selected.trim().length > 0;
let initialContent = '';
if (hasInitialSelection) {
// Store the selection range
selectionRange = globalEditorInstance.getSelectionRange();
// Check if selection overlaps with any index items and expand if needed
const expandedContent = expandSelectionToIndexItems(selectionRange);
if (expandedContent) {
initialContent = expandedContent.content;
} else {
initialContent = selected;
}
} else {
// No selection - just get cursor position
const pos = globalEditorInstance.getCursorPosition();
const Range = ace.require('ace/range').Range;
selectionRange = new Range(pos.row, pos.column, pos.row, pos.column);
initialContent = '';
}
// Initialize edit history with current content
editHistory = [initialContent];
currentEditIndex = 0;
// Create editor container with section info area
const editorHtml = `
<div style="display: flex; gap: 12px; height: 400px;">
<div style="flex: 1; display: flex; flex-direction: column; gap: 8px;">
<div id="sectionInfoDisplay" style="
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
background: #1e293b;
border-radius: 4px;
">
<span style="font-size: 16px;">❓</span>
<span style="
flex: 1;
color: #888;
font-size: 14px;
font-family: 'Segoe UI', sans-serif;
font-weight: 500;
">Target: Not detected yet</span>
<button id="refreshSectionBtn" style="
padding: 4px 8px;
background: #3d3d3d;
border: 1px solid #555;
border-radius: 4px;
color: #e0e0e0;
cursor: pointer;
font-size: 12px;
font-family: 'Segoe UI', sans-serif;
">🔄 Detect</button>
</div>
<div style="display: flex; align-items: center; gap: 8px; padding: 8px; background: #1e293b; border-radius: 4px;">
<button id="prevEditBtn" style="
padding: 6px 12px;
background: #3d3d3d;
border: 1px solid #555;
border-radius: 4px;
color: #e0e0e0;
cursor: pointer;
font-size: 16px;
display: none;
">←</button>
<span id="editIndexDisplay" style="
flex: 1;
text-align: center;
color: #888;
font-size: 13px;
font-family: 'Segoe UI', sans-serif;
display: none;
">1 / 1</span>
<button id="nextEditBtn" style="
padding: 6px 12px;
background: #3d3d3d;
border: 1px solid #555;
border-radius: 4px;
color: #e0e0e0;
cursor: pointer;
font-size: 16px;
display: none;
">→</button>
<button id="addEditBtn" style="
padding: 6px 12px;
background: #3d3d3d;
border: 1px solid #555;
border-radius: 4px;
color: #e0e0e0;
cursor: pointer;
font-size: 13px;
font-family: 'Segoe UI', sans-serif;
">+ New</button>
</div>
<div id="overlayEditor" style="flex: 1; border: 1px solid #555; border-radius: 4px;"></div>
</div>
</div>
`;
const footerHtml = `
<button id="applyBtn" style="
padding: 8px 16px;
background: #16a34a;
border: 1px solid #15803d;
border-radius: 4px;
color: #fff;
cursor: pointer;
font-size: 14px;
font-family: 'Segoe UI', sans-serif;
font-weight: 600;
">✅ Apply Changes</button>
<button id="cancelEditBtn" style="
padding: 8px 16px;
background: #3d3d3d;
border: 1px solid #555;
border-radius: 4px;
color: #e0e0e0;
cursor: pointer;
font-size: 14px;
font-family: 'Segoe UI', sans-serif;
">Cancel</button>
`;
deps.showOverlay('Edit Content', editorHtml, footerHtml);
// Initialize Ace editor in overlay
setTimeout(() => {
initializeOverlayEditor();
setupEditNavigation();
}, 100);
}
// =========================================================================
// OVERLAY EDITOR INITIALIZATION
// =========================================================================
function initializeOverlayEditor() {
const el = deps.getEl();
const globalEditorInstance = deps.getGlobalEditor();
const overlayEditorContainer = el.querySelector('#overlayEditor');
if (!overlayEditorContainer) {
console.error("[overlay_editor.js] Could not find #overlayEditor container");
return;
}
overlayEditorInstance = ace.edit(overlayEditorContainer);
overlayEditorInstance.setTheme("ace/theme/monokai");
// Match the main editor's mode
const currentMode = globalEditorInstance.getSession().getMode().$id;
overlayEditorInstance.session.setMode(currentMode);
// Set initial content
overlayEditorInstance.setValue(editHistory[currentEditIndex], -1);
overlayEditorInstance.setOptions({
fontSize: "14px",
wrap: true,
showPrintMargin: false,
useWorker: false,
enableAutoIndent: true
});
overlayEditorInstance.focus();
}
// =========================================================================
// NAVIGATION SETUP
// =========================================================================
function setupEditNavigation() {
const el = deps.getEl();
const prevBtn = el.querySelector('#prevEditBtn');
const nextBtn = el.querySelector('#nextEditBtn');
const addBtn = el.querySelector('#addEditBtn');
const applyBtn = el.querySelector('#applyBtn');
const cancelBtn = el.querySelector('#cancelEditBtn');
const refreshBtn = el.querySelector('#refreshSectionBtn');
// Update display
updateEditNavigation();
if (refreshBtn) {
refreshBtn.addEventListener('click', () => {
detectTargetSection();
});
}
if (prevBtn) {
prevBtn.addEventListener('click', () => {
if (currentEditIndex > 0) {
saveCurrentEdit();
currentEditIndex--;
loadCurrentEdit();
updateEditNavigation();
}
});
}
if (nextBtn) {
nextBtn.addEventListener('click', () => {
if (currentEditIndex < editHistory.length - 1) {
saveCurrentEdit();
currentEditIndex++;
loadCurrentEdit();
updateEditNavigation();
}
});
}
if (addBtn) {
addBtn.addEventListener('click', () => {
saveCurrentEdit();
editHistory.push('');
currentEditIndex = editHistory.length - 1;
loadCurrentEdit();
updateEditNavigation();
});
}
if (applyBtn) {
applyBtn.addEventListener('click', handleApplyChanges);
}
if (cancelBtn) {
cancelBtn.addEventListener('click', () => {
cleanup();
deps.hideOverlay();
});
}
}
// =========================================================================
// EDIT MANAGEMENT
// =========================================================================
function saveCurrentEdit() {
if (overlayEditorInstance) {
editHistory[currentEditIndex] = overlayEditorInstance.getValue();
}
}
function loadCurrentEdit() {
if (overlayEditorInstance) {
overlayEditorInstance.setValue(editHistory[currentEditIndex], -1);
overlayEditorInstance.focus();
}
}
function updateEditNavigation() {
const el = deps.getEl();
const prevBtn = el.querySelector('#prevEditBtn');
const nextBtn = el.querySelector('#nextEditBtn');
const indexDisplay = el.querySelector('#editIndexDisplay');
const hasMultiple = editHistory.length > 1;
if (prevBtn) {
prevBtn.style.display = hasMultiple ? 'block' : 'none';
prevBtn.disabled = currentEditIndex === 0;
prevBtn.style.opacity = currentEditIndex === 0 ? '0.5' : '1';
}
if (nextBtn) {
nextBtn.style.display = hasMultiple ? 'block' : 'none';
nextBtn.disabled = currentEditIndex === editHistory.length - 1;
nextBtn.style.opacity = currentEditIndex === editHistory.length - 1 ? '0.5' : '1';
}
if (indexDisplay) {
indexDisplay.style.display = hasMultiple ? 'block' : 'none';
indexDisplay.textContent = `${currentEditIndex + 1} / ${editHistory.length}`;
}
}
// =========================================================================
// APPLY CHANGES
// =========================================================================
function handleApplyChanges() {
const globalEditorInstance = deps.getGlobalEditor();
if (!globalEditorInstance || !overlayEditorInstance) return;
// Save current edit before applying
saveCurrentEdit();
// Combine all edits with line breaks
const allContent = editHistory.filter(e => e.trim().length > 0).join('\n\n');
if (!allContent.trim()) {
if (deps.showToast) {
deps.showToast('⚠️ No content to apply', 'error');
}
return;
}
// Check if we have a valid target from detectTargetSection
if (!selectionRange) {
if (deps.showToast) {
deps.showToast('⚠️ No target detected. Click "Detect" first.', 'error');
}
return;
}
// Check if it's an empty range (cursor position) or actual selection
const Range = ace.require('ace/range').Range;
const isEmpty = selectionRange.isEmpty();
if (isEmpty) {
// Insert at cursor
const contentToInsert = '\n' + allContent;
globalEditorInstance.session.insert(selectionRange.start, contentToInsert);
if (deps.showToast) {
deps.showToast('✅ Content inserted', 'success');
}
} else {
// Replace the detected range
globalEditorInstance.session.replace(selectionRange, allContent);
if (deps.showToast) {
deps.showToast('✅ Content replaced', 'success');
}
}
cleanup();
deps.hideOverlay();
globalEditorInstance.focus();
}
// =========================================================================
// SELECTION EXPANSION
// =========================================================================
function expandSelectionToIndexItems(range) {
const globalEditorInstance = deps.getGlobalEditor();
if (!globalEditorInstance) return null;
const session = globalEditorInstance.getSession();
const Range = ace.require('ace/range').Range;
const startRow = range.start.row;
const endRow = range.end.row;
// Build index of ALL sections (markers + foldable)
const allSections = [];
// 1. Find all markers
const lineCount = session.getLength();
for (let row = 0; row < lineCount; row++) {
const line = session.getLine(row);
if (line.includes('<') && (line.includes('<!--') || line.includes('/*') || line.includes('//'))) {
const openMatch = line.match(/(?:<!--|\/\*|\/\/\/|\/\/)\s*(.+?)</);
if (openMatch) {
const markerName = openMatch[1].trim();
for (let closeRow = row + 1; closeRow < lineCount; closeRow++) {
const closeLine = session.getLine(closeRow);
if (closeLine.includes('>')) {
const closeMatch = closeLine.match(/(?:<!--|\/\*|\/\/\/|\/\/)\s*(.+?)>/);
if (closeMatch && closeMatch[1].trim() === markerName) {
allSections.push({
startRow: row,
endRow: closeRow,
length: closeRow - row,
type: 'marker',
icon: '🏷️',
label: `[${markerName}]`,
name: markerName
});
break;
}
}
}
}
}
}
// 2. Find all foldable sections
for (let row = 0; row < lineCount; row++) {
const foldWidget = session.getFoldWidget(row);
if (!foldWidget || foldWidget === '') continue;
const foldRange = session.getFoldWidgetRange(row);
if (!foldRange) continue;
const line = session.getLine(foldRange.start.row).trim();
let icon = '📦';
let label = line.substring(0, 40);
// Detect type for icon
if (line.match(/^\.([a-zA-Z0-9_-]+)/)) {
icon = '🎨';
label = line.match(/^(\.([a-zA-Z0-9_-]+))/)[1];
} else if (line.match(/^(body|html|h[1-6])/)) {
icon = '🎨';
label = line.match(/^([a-zA-Z0-9]+)/)[1];
} else if (line.match(/function\s+(\w+)/)) {
icon = '⚙️';
const match = line.match(/function\s+(\w+)/);
label = match[1] + '()';
}
allSections.push({
startRow: foldRange.start.row,
endRow: foldRange.end.row,
length: foldRange.end.row - foldRange.start.row,
type: 'fold',
icon: icon,
label: label,
name: line.substring(0, 40)
});
}
// 3. Check which sections have BOTH start AND end inside them
const fullyContainingSections = allSections.filter(section =>
section.startRow <= startRow && section.endRow >= endRow
);
if (fullyContainingSections.length === 0) {
return null; // No sections contain the selection
}
// 4. Sort by length (shortest first) and pick the smallest
fullyContainingSections.sort((a, b) => a.length - b.length);
const smallest = fullyContainingSections[0];
// 5. Expand selection to this section
selectionRange = new Range(
smallest.startRow,
0,
smallest.endRow,
session.getLine(smallest.endRow).length
);
const lines = [];
for (let row = smallest.startRow; row <= smallest.endRow; row++) {
lines.push(session.getLine(row));
}
if (deps.showToast) {
deps.showToast(`📦 Expanded to ${smallest.label}`, 'info', 3000);
}
return {
content: lines.join('\n'),
sectionInfo: smallest
};
}
// =========================================================================
// TARGET DETECTION
// =========================================================================
function detectTargetSection() {
const globalEditorInstance = deps.getGlobalEditor();
if (!overlayEditorInstance) {
if (deps.showToast) {
deps.showToast('⚠️ Overlay editor not initialized', 'error');
}
return;
}
const content = overlayEditorInstance.getValue();
if (!content || !content.trim()) {
if (deps.showToast) {
deps.showToast('⚠️ No content to analyze', 'error');
}
return;
}
const index = deps.generateDocumentIndex();
if (!index || index.length === 0) {
if (deps.showToast) {
deps.showToast('⚠️ No sections found in document', 'error');
}
return;
}
const session = globalEditorInstance.getSession();
const Range = ace.require('ace/range').Range;
let candidates = [];
// Get lines from overlay content
const contentLines = content.split('\n').map(l => l.trim()).filter(l => l.length > 0);
const firstContentLine = contentLines[0] || '';
// Score each section in the document
for (const item of index) {
let itemStartRow, itemEndRow;
if (item.isMarker) {
itemStartRow = item.row;
itemEndRow = deps.findMarkerEnd(item.row, item.label);
} else {
const foldRange = session.getFoldWidgetRange(item.row);
if (foldRange) {
itemStartRow = foldRange.start.row;
itemEndRow = foldRange.end.row;
} else {
itemStartRow = itemEndRow = item.row;
}
}
// Get all lines from this section
const itemLines = [];
for (let row = itemStartRow; row <= itemEndRow; row++) {
const line = session.getLine(row).trim();
if (line.length > 0) {
itemLines.push(line);
}
}
// Calculate header match score (50%)
const headerScore = calculateHeaderScore(item, firstContentLine, itemLines[0] || '');
// Calculate content match score (50%)
const contentScore = calculateContentScore(contentLines, itemLines);
// Final score: 50% header + 50% content
const finalScore = (headerScore * 0.5) + (contentScore.score * 0.5);
candidates.push({
...item,
startRow: itemStartRow,
endRow: itemEndRow,
score: finalScore,
headerScore: headerScore,
contentScore: contentScore.score,
exactMatches: contentScore.exactMatches,
partialMatches: contentScore.partialMatches,
totalLines: contentLines.length,
size: itemEndRow - itemStartRow
});
}
if (candidates.length === 0) {
updateSectionDisplay(null);
return;
}
// Sort by score (descending), then by size (ascending for ties)
candidates.sort((a, b) => {
if (Math.abs(a.score - b.score) < 0.05) {
return a.size - b.size;
}
return b.score - a.score;
});
const bestMatch = candidates[0];
// Check if match is good enough (threshold: 40%)
if (bestMatch.score < 0.4) {
updateSectionDisplay(null, bestMatch);
return;
}
// Good match - update selectionRange
selectionRange = new Range(
bestMatch.startRow,
0,
bestMatch.endRow,
session.getLine(bestMatch.endRow).length
);
updateSectionDisplay(bestMatch);
}
// =========================================================================
// SCORING HELPERS
// =========================================================================
function calculateHeaderScore(item, firstContentLine, firstItemLine) {
let score = 0;
if (item.isMarker) {
const markerName = item.label.replace(/[\[\]]/g, '').trim();
if (firstContentLine.includes(markerName + '<') ||
(firstContentLine.includes(markerName) && firstContentLine.includes('<'))) {
score = 1.0;
}
} else if (item.icon === '⚙️') {
const funcName = item.label.replace('()', '').trim();
if (firstContentLine.includes('function') && firstContentLine.includes(funcName)) {
score = 1.0;
} else if (firstContentLine.includes(funcName) && firstContentLine.includes('{')) {
score = 0.8;
}
} else if (item.icon === '🎨') {
const selector = item.label.trim();
if (firstContentLine === selector || firstContentLine.startsWith(selector + ' {')) {
score = 1.0;
} else if (firstContentLine.includes(selector)) {
score = 0.7;
}
} else if (item.icon === '📦') {
const tagMatch = firstItemLine.match(/<([a-zA-Z]+)/);
if (tagMatch) {
const tagName = tagMatch[1];
if (firstContentLine.includes('<' + tagName)) {
score = 1.0;
}
}
}
return score;
}
function calculateContentScore(contentLines, itemLines) {
let exactMatches = 0;
let partialMatches = 0;
for (const contentLine of contentLines) {
let foundExact = false;
let foundPartial = false;
for (const itemLine of itemLines) {
if (contentLine === itemLine) {
foundExact = true;
break;
}
if (contentLine.length > 10 && itemLine.length > 10) {
if (itemLine.includes(contentLine) || contentLine.includes(itemLine)) {
foundPartial = true;
}
}
}
if (foundExact) exactMatches++;
else if (foundPartial) partialMatches++;
}
const totalMatches = exactMatches + (partialMatches * 0.5);
const score = totalMatches / contentLines.length;
return { score, exactMatches, partialMatches };
}
// =========================================================================
// UI UPDATES
// =========================================================================
function updateSectionDisplay(bestMatch, poorMatch = null) {
const el = deps.getEl();
const sectionDisplay = el.querySelector('#sectionInfoDisplay');
if (!sectionDisplay) return;
if (!bestMatch) {
// No good match
const poorInfo = poorMatch
? `<span style="color: #888; font-size: 11px;">(best: ${deps.escapeHtml(poorMatch.label)} ${(poorMatch.score * 100).toFixed(0)}%)</span>`
: '';
sectionDisplay.innerHTML = `
<span style="font-size: 16px;">❌</span>
<span style="
flex: 1;
color: #ef4444;
font-size: 14px;
font-family: 'Segoe UI', sans-serif;
font-weight: 500;
">No match found ${poorInfo}</span>
<button id="refreshSectionBtn" style="
padding: 4px 8px;
background: #3d3d3d;
border: 1px solid #555;
border-radius: 4px;
color: #e0e0e0;
cursor: pointer;
font-size: 12px;
font-family: 'Segoe UI', sans-serif;
">🔄 Detect</button>
`;
if (deps.showToast) {
deps.showToast('❌ No good match found', 'error');
}
} else {
// Good match
const matchInfo = `H:${(bestMatch.headerScore * 100).toFixed(0)}% C:${(bestMatch.contentScore * 100).toFixed(0)}%`;
sectionDisplay.innerHTML = `
<span style="font-size: 16px;">${bestMatch.icon}</span>
<span style="
flex: 1;
color: #22c55e;
font-size: 14px;
font-family: 'Segoe UI', sans-serif;
font-weight: 500;
">Target: ${deps.escapeHtml(bestMatch.label)} <span style="color: #888; font-size: 11px;">(${(bestMatch.score * 100).toFixed(0)}% • ${matchInfo})</span></span>
<button id="refreshSectionBtn" style="
padding: 4px 8px;
background: #3d3d3d;
border: 1px solid #555;
border-radius: 4px;
color: #e0e0e0;
cursor: pointer;
font-size: 12px;
font-family: 'Segoe UI', sans-serif;
">🔄 Detect</button>
`;
if (deps.showToast) {
deps.showToast(`🎯 Target: ${bestMatch.label} (${(bestMatch.score * 100).toFixed(0)}%)`, 'success');
}
}
// Re-attach event listener
const refreshBtn = sectionDisplay.querySelector('#refreshSectionBtn');
if (refreshBtn) {
refreshBtn.addEventListener('click', detectTargetSection);
}
}
// =========================================================================
// CLEANUP
// =========================================================================
function cleanup() {
if (overlayEditorInstance) {
overlayEditorInstance.destroy();
overlayEditorInstance = null;
}
editHistory = [];
currentEditIndex = 0;
selectionRange = null;
}
// =========================================================================
// PUBLIC API
// =========================================================================
window.OverlayEditor = {
init: init,
showEditSelectionOverlay: showEditSelectionOverlay,
cleanup: cleanup
};
console.log("[overlay_editor.js] Module loaded successfully");
})();