📜
overlay_editor_copy3.js
Back
📝 Javascript ⚡ Executable Ctrl+S: Save • Ctrl+R: Run • Ctrl+F: Find
/** * 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 - store 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 if (!selectionRange) { if (deps.showToast) { deps.showToast('⚠️ No target. Using cursor position.', 'info'); } // Fallback to cursor const pos = globalEditorInstance.getCursorPosition(); const Range = ace.require('ace/range').Range; selectionRange = new Range(pos.row, pos.column, pos.row, pos.column); } // Check if it's an empty range (insertion) or actual selection (replacement) const Range = ace.require('ace/range').Range; const isEmpty = selectionRange.isEmpty(); if (isEmpty) { // Insert at position const contentToInsert = '\n' + allContent + '\n'; globalEditorInstance.session.insert(selectionRange.start, contentToInsert); if (deps.showToast) { deps.showToast('✅ Content inserted', 'success'); } } else { // Replace the 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*([\w\-\[\]_]+)\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*([\w\-\[\]_]+)\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; } // Get index - handle both hierarchical and flat structures const indexResult = deps.generateDocumentIndex(); let flatIndex = []; // Convert hierarchical structure to flat array if needed if (indexResult && typeof indexResult === 'object' && 'components' in indexResult) { // Hierarchical structure from new index const { components, unmarked } = indexResult; // Flatten components for (const [componentName, languages] of Object.entries(components)) { for (const [language, sections] of Object.entries(languages)) { sections.forEach(section => { flatIndex.push({ ...section, isMarker: true }); }); } } // Add unmarked items flatIndex = flatIndex.concat(unmarked); } else if (Array.isArray(indexResult)) { // Flat array from old index flatIndex = indexResult; } if (flatIndex.length === 0) { // No matches - find smart insertion point findSmartInsertionPoint(content); 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 flatIndex) { let itemStartRow, itemEndRow; if (item.isMarker || item.type === 'marker') { itemStartRow = item.row; itemEndRow = item.endRow || 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 (70%) const headerScore = calculateHeaderScore(item, firstContentLine, itemLines[0] || ''); // Calculate content match score (30%) const contentScore = calculateContentScore(contentLines, itemLines); // Final score: 70% header + 30% content const finalScore = (headerScore * 0.7) + (contentScore.score * 0.3); 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) { findSmartInsertionPoint(content); 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 match quality thresholds if (bestMatch.score >= 0.9) { // Excellent match (90%+) - just use it selectionRange = new Range( bestMatch.startRow, 0, bestMatch.endRow, session.getLine(bestMatch.endRow).length ); updateSectionDisplay(bestMatch); } else if (bestMatch.score >= 0.6) { // Good match (60-90%) - show options including the match const cursorPos = globalEditorInstance.getCursorPosition(); const options = []; // Option 1: Best match options.push({ type: 'match', row: bestMatch.startRow, endRow: bestMatch.endRow, label: `Replace ${bestMatch.label}`, detail: `${(bestMatch.score * 100).toFixed(0)}% match`, icon: bestMatch.icon, score: bestMatch.score, isReplacement: true, matchData: bestMatch }); // Option 2: After best match options.push({ type: 'after', row: bestMatch.endRow + 1, label: `After ${bestMatch.label}`, detail: 'Insert new section', icon: '📍', score: bestMatch.score * 0.8, isReplacement: false }); // Option 3: At cursor options.push({ type: 'cursor', row: cursorPos.row, label: `At cursor (line ${cursorPos.row + 1})`, detail: 'Current position', icon: '📍', score: 0, isReplacement: false }); // Set default to best option if (options[0].isReplacement) { selectionRange = new Range( options[0].row, 0, options[0].endRow, session.getLine(options[0].endRow).length ); } else { selectionRange = new Range(options[0].row, 0, options[0].row, 0); } updateSectionDisplay(null, null, { type: 'options', options: options, selectedIndex: 0 }); } else { // Poor match (<60%) - use smart insertion findSmartInsertionPoint(content, bestMatch); } } // ========================================================================= // SMART INSERTION POINT // ========================================================================= function findSmartInsertionPoint(content, poorMatch = null) { const globalEditorInstance = deps.getGlobalEditor(); const session = globalEditorInstance.getSession(); const Range = ace.require('ace/range').Range; // Analyze content to determine type const contentLines = content.split('\n'); let contentType = detectContentType(contentLines); // Get cursor position for fallback const cursorPos = globalEditorInstance.getCursorPosition(); const cursorRow = cursorPos.row; // Get index to find nearby sections const indexResult = deps.generateDocumentIndex(); let flatIndex = []; // Convert hierarchical structure to flat array if needed if (indexResult && typeof indexResult === 'object' && 'components' in indexResult) { const { components, unmarked } = indexResult; for (const [componentName, languages] of Object.entries(components)) { for (const [language, sections] of Object.entries(languages)) { sections.forEach(section => { flatIndex.push({ ...section, isMarker: true }); }); } } flatIndex = flatIndex.concat(unmarked); } else if (Array.isArray(indexResult)) { flatIndex = indexResult; } // Strategy 1: Find sections of same content type const matchingTypeSections = flatIndex.filter(item => { if (contentType === 'css' && item.icon === '🎨') return true; if (contentType === 'function' && (item.icon === '⚙️' || item.icon === '🔧')) return true; if (contentType === 'html' && item.icon === '📦') return true; return false; }); // Find the last matching section (or closest to cursor) let smartSection = null; let smartRow = null; if (matchingTypeSections.length > 0) { // Use last section of matching type smartSection = matchingTypeSections[matchingTypeSections.length - 1]; smartRow = smartSection.endRow ? smartSection.endRow + 1 : smartSection.row + 1; } // Strategy 2: Find closest section to cursor let nearestSection = null; let nearestDistance = Infinity; flatIndex.forEach(item => { const itemRow = item.row; const distance = Math.abs(itemRow - cursorRow); if (distance < nearestDistance) { nearestDistance = distance; nearestSection = item; } }); // Prepare insertion options const insertionOptions = []; // Option 1: Smart match based on content type if (smartSection) { insertionOptions.push({ type: 'smart', row: smartRow, label: `After ${smartSection.label}`, icon: smartSection.icon, section: smartSection }); } // Option 2: After nearest section if (nearestSection && (!smartSection || nearestSection.label !== smartSection.label)) { const afterRow = nearestSection.endRow ? nearestSection.endRow + 1 : nearestSection.row + 1; insertionOptions.push({ type: 'nearest', row: afterRow, label: `After ${nearestSection.label}`, icon: nearestSection.icon, section: nearestSection }); } // Option 3: At cursor insertionOptions.push({ type: 'cursor', row: cursorRow, label: `At cursor (line ${cursorRow + 1})`, icon: '📍', section: null }); // Default to first option (smart match or cursor) const defaultOption = insertionOptions[0]; selectionRange = new Range(defaultOption.row, 0, defaultOption.row, 0); updateSectionDisplay(null, poorMatch, { type: 'insertion', contentType: contentType, options: insertionOptions, selectedIndex: 0 }); } function detectContentType(lines) { const content = lines.join('\n').trim(); // Check for CSS if (content.match(/^\.([a-zA-Z0-9_-]+)\s*\{/) || content.match(/^(body|html|h[1-6])\s*\{/i)) { return 'css'; } // Check for function if (content.match(/function\s+(\w+)/) || content.match(/const\s+(\w+)\s*=\s*(?:function|\()/)) { return 'function'; } // Check for HTML if (content.match(/^<\w+/)) { return 'html'; } // Check for PHP if (content.includes('<?php') || content.match(/function\s+\w+\s*\(/)) { return 'php'; } return 'unknown'; } function findClosingBrace(startRow) { const globalEditorInstance = deps.getGlobalEditor(); const session = globalEditorInstance.getSession(); const lineCount = session.getLength(); let braceCount = 0; let foundOpening = false; for (let row = startRow; row < lineCount; row++) { const line = session.getLine(row); for (let i = 0; i < line.length; i++) { if (line[i] === '{') { braceCount++; foundOpening = true; } else if (line[i] === '}') { braceCount--; if (foundOpening && braceCount === 0) { return row; } } } } return startRow; } // ========================================================================= // SCORING HELPERS // ========================================================================= function calculateHeaderScore(item, firstContentLine, firstItemLine) { let score = 0; // Extract normalized names const itemLabel = (item.label || '').replace(/[\[\]()]/g, '').trim().toLowerCase(); const contentHeader = (firstContentLine || '').replace(/[\[\]()<>]/g, '').trim().toLowerCase(); // Small helper for edit distance (Levenshtein) function stringDistance(a, b) { const dp = Array.from({ length: a.length + 1 }, () => Array(b.length + 1).fill(0)); for (let i = 0; i <= a.length; i++) dp[i][0] = i; for (let j = 0; j <= b.length; j++) dp[0][j] = j; for (let i = 1; i <= a.length; i++) { for (let j = 1; j <= b.length; j++) { const cost = a[i - 1] === b[j - 1] ? 0 : 1; dp[i][j] = Math.min( dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + cost ); } } return dp[a.length][b.length]; } // Detect content language from first line const contentLanguage = detectContentLanguage(firstContentLine); // Get item language (from marker name if hierarchical) let itemLanguage = null; if (item.parsed && item.parsed.language) { itemLanguage = item.parsed.language.toLowerCase(); } else if (item.icon === '🎨') { itemLanguage = 'css'; } else if (item.icon === '⚙️' || item.icon === '🔧') { itemLanguage = 'js'; // or php, but we'll check syntax } else if (item.icon === '📦') { itemLanguage = 'html'; } // Language mismatch penalty - if we're confident about both languages and they don't match if (itemLanguage && contentLanguage && itemLanguage !== contentLanguage) { // Strong mismatch: CSS content vs JS function, etc. if ((contentLanguage === 'css' && itemLanguage !== 'css') || (contentLanguage === 'js' && itemLanguage === 'css') || (contentLanguage === 'php' && itemLanguage === 'css')) { return 0; // No match - wrong language } } // --- Marker match --- if (item.isMarker || item.type === 'marker') { const distance = stringDistance(itemLabel, contentHeader); const maxLen = Math.max(itemLabel.length, contentHeader.length); const similarity = 1 - distance / maxLen; // Hard cutoff: must be > 0.9 to be "same header" if (similarity > 0.9) { score = 1.0; } else if (similarity > 0.75) { score = 0.6; } else { score = 0; } } // --- Function header --- else if (item.icon === '⚙️' || item.icon === '🔧') { const funcName = item.label.replace('()', '').trim().toLowerCase(); const match = firstContentLine.match(/function\s+(\w+)/i); const arrowMatch = firstContentLine.match(/(?:const|let|var)\s+(\w+)\s*=/); if (match && match[1].toLowerCase() === funcName) { score = 1.0; } else if (arrowMatch && arrowMatch[1].toLowerCase() === funcName) { score = 1.0; } else if (contentHeader.includes(funcName)) { score = 0.5; } } // --- CSS selectors --- else if (item.icon === '🎨') { const sel = item.label.trim(); const selNormalized = sel.toLowerCase(); if (firstContentLine.startsWith(sel + ' {') || firstContentLine.startsWith(sel + '{')) { score = 1.0; } else if (firstContentLine.match(new RegExp(`^${sel.replace('.', '\\.')}\\s*\\{`, 'i'))) { score = 1.0; } else if (contentHeader.includes(selNormalized)) { score = 0.8; } } // --- HTML tags --- else if (item.icon === '📦') { const tagMatch = firstItemLine.match(/<([a-zA-Z0-9-]+)/); const tagContent = firstContentLine.match(/<([a-zA-Z0-9-]+)/); if (tagMatch && tagContent && tagMatch[1].toLowerCase() === tagContent[1].toLowerCase()) { score = 1.0; } } return score; } function detectContentLanguage(line) { const normalized = line.trim().toLowerCase(); // CSS detection - very specific patterns if (normalized.match(/^\.[\w-]+\s*\{/) || normalized.match(/^(body|html|header|footer|main|section|nav|article|aside|h[1-6]|p|div|span|a|button|input|form)\s*\{/i) || normalized.match(/^[\w-]+\s*:\s*[\w-]+;/)) { return 'css'; } // PHP detection if (normalized.includes('<?php') || normalized.match(/function\s+\w+\s*\([^)]*\)\s*\{/) && normalized.includes('$')) { return 'php'; } // JavaScript detection if (normalized.match(/function\s+\w+\s*\(/) || normalized.match(/(?:const|let|var)\s+\w+\s*=/) || normalized.includes('=>')) { return 'js'; } // HTML detection if (normalized.match(/^<[a-z]+/i)) { return 'html'; } return null; // Unknown } 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, insertionInfo = null) { const el = deps.getEl(); const sectionDisplay = el.querySelector('#sectionInfoDisplay'); if (!sectionDisplay) return; if (!bestMatch && insertionInfo && insertionInfo.type === 'options') { // Show multiple options for 60-90% matches const options = insertionInfo.options; const selectedIndex = insertionInfo.selectedIndex || 0; const selectedOption = options[selectedIndex]; let optionsHtml = options.map((opt, idx) => { const scoreInfo = opt.score > 0 ? ` • ${(opt.score * 100).toFixed(0)}%` : ''; return ` <label style=" display: flex; align-items: center; gap: 6px; padding: 6px 10px; background: ${idx === selectedIndex ? '#4a5568' : '#2d2d2d'}; border-radius: 4px; cursor: pointer; transition: background 0.15s; font-family: 'Segoe UI', sans-serif; font-size: 12px; " class="insertion-option" data-index="${idx}"> <input type="radio" name="insertionChoice" value="${idx}" ${idx === selectedIndex ? 'checked' : ''} style=" accent-color: #3b82f6; "> <span style="font-size: 14px;">${opt.icon}</span> <span style="color: #e0e0e0; flex: 1;"> ${deps.escapeHtml(opt.label)} <span style="color: #888; font-size: 11px;">${opt.detail}${scoreInfo}</span> </span> </label> `; }).join(''); sectionDisplay.innerHTML = ` <div style="flex: 1; display: flex; flex-direction: column; gap: 4px;"> <div style=" color: #fbbf24; font-size: 13px; font-weight: 600; margin-bottom: 2px; ">⚠️ Multiple options found:</div> ${optionsHtml} </div> <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; align-self: flex-start; ">🔄 Detect</button> `; // Attach option selection handlers setTimeout(() => { const globalEditorInstance = deps.getGlobalEditor(); const session = globalEditorInstance.getSession(); const Range = ace.require('ace/range').Range; const optionElements = sectionDisplay.querySelectorAll('.insertion-option'); optionElements.forEach((optEl, idx) => { optEl.addEventListener('mouseenter', () => { if (idx !== selectedIndex) { optEl.style.background = '#3d3d3d'; } }); optEl.addEventListener('mouseleave', () => { if (idx !== selectedIndex) { optEl.style.background = '#2d2d2d'; } }); optEl.addEventListener('click', () => { const option = options[idx]; // Update selection range based on option type if (option.isReplacement) { selectionRange = new Range( option.row, 0, option.endRow, session.getLine(option.endRow).length ); } else { selectionRange = new Range(option.row, 0, option.row, 0); } // Re-render with new selection updateSectionDisplay(null, poorMatch, { ...insertionInfo, selectedIndex: idx }); if (deps.showToast) { deps.showToast(`📍 Selected: ${option.label}`, 'info'); } }); }); }, 10); if (deps.showToast && insertionInfo.selectedIndex === undefined) { deps.showToast(`⚠️ ${(options[0].score * 100).toFixed(0)}% match - choose action`, 'warning'); } } else if (!bestMatch && insertionInfo && insertionInfo.options) { // Smart insertion mode with options (from findSmartInsertionPoint) const options = insertionInfo.options; const selectedIndex = insertionInfo.selectedIndex || 0; const selectedOption = options[selectedIndex]; let optionsHtml = options.map((opt, idx) => ` <label style=" display: flex; align-items: center; gap: 6px; padding: 6px 10px; background: ${idx === selectedIndex ? '#4a5568' : '#2d2d2d'}; border-radius: 4px; cursor: pointer; transition: background 0.15s; font-family: 'Segoe UI', sans-serif; font-size: 12px; " class="insertion-option" data-index="${idx}"> <input type="radio" name="insertionChoice" value="${idx}" ${idx === selectedIndex ? 'checked' : ''} style=" accent-color: #3b82f6; "> <span style="font-size: 14px;">${opt.icon}</span> <span style="color: #e0e0e0;">${deps.escapeHtml(opt.label)}</span> </label> `).join(''); sectionDisplay.innerHTML = ` <div style="flex: 1; display: flex; flex-direction: column; gap: 4px;"> <div style=" color: #3b82f6; font-size: 13px; font-weight: 600; margin-bottom: 2px; ">📍 Choose insertion point:</div> ${optionsHtml} </div> <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; align-self: flex-start; ">🔄 Detect</button> `; // Attach option selection handlers setTimeout(() => { const optionElements = sectionDisplay.querySelectorAll('.insertion-option'); optionElements.forEach((optEl, idx) => { optEl.addEventListener('mouseenter', () => { if (idx !== selectedIndex) { optEl.style.background = '#3d3d3d'; } }); optEl.addEventListener('mouseleave', () => { if (idx !== selectedIndex) { optEl.style.background = '#2d2d2d'; } }); optEl.addEventListener('click', () => { const globalEditorInstance = deps.getGlobalEditor(); const Range = ace.require('ace/range').Range; // Update selection range const option = options[idx]; selectionRange = new Range(option.row, 0, option.row, 0); // Re-render with new selection updateSectionDisplay(null, poorMatch, { ...insertionInfo, selectedIndex: idx }); if (deps.showToast) { deps.showToast(`📍 Will insert ${option.label}`, 'info'); } }); }); }, 10); if (deps.showToast && insertionInfo.selectedIndex === undefined) { deps.showToast(`📍 Choose where to insert content`, 'info'); } } else if (!bestMatch && insertionInfo) { // Legacy fallback (shouldn't hit this anymore) sectionDisplay.innerHTML = ` <span style="font-size: 16px;">📍</span> <span style=" flex: 1; color: #3b82f6; font-size: 14px; font-family: 'Segoe UI', sans-serif; font-weight: 500; ">Insert after line ${insertionInfo.row + 1} <span style="color: #888; font-size: 11px;">(${insertionInfo.contentType})</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(`📍 Will insert after line ${insertionInfo.row + 1}`, 'info'); } } else 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 (90%+) 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 for detect button 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"); })();