(function () {
try {
console.log("[overlay_editor.js] Loading overlay editor module...");
// Create overlay editor namespace immediately
window.OverlayEditor = window.OverlayEditor || {
_ready: false,
_readyCallbacks: []
};
let overlayEditorInstance = null;
let overlayContainer = null;
let onSaveCallback = null;
let currentInstanceIndex = 0;
let instances = [];
let sourceFileName = "";
// LocalStorage key for instances
const INSTANCES_KEY_PREFIX = "overlay_editor_instances_";
// Load instances from localStorage for a specific file
function loadInstances(fileName) {
try {
const key = INSTANCES_KEY_PREFIX + fileName;
const saved = localStorage.getItem(key);
return saved ? JSON.parse(saved) : [];
} catch (err) {
console.error("[overlay_editor.js] Failed to load instances:", err);
return [];
}
}
// Save instances to localStorage for a specific file
function saveInstances(fileName, instancesArray) {
try {
const key = INSTANCES_KEY_PREFIX + fileName;
localStorage.setItem(key, JSON.stringify(instancesArray));
console.log(`[overlay_editor.js] Saved ${instancesArray.length} instances for ${fileName}`);
} catch (err) {
console.error("[overlay_editor.js] Failed to save instances:", err);
}
}
// Get the current active file name
function getActiveFileName() {
try {
const files = JSON.parse(localStorage.getItem("sftp_active_files") || "[]");
const active = files.find((f) => f.active);
return active ? active.name : "default";
} catch {
return "default";
}
}
// Update the counter display
function updateCounter() {
const counter = document.getElementById("overlayInstanceCounter");
if (counter) {
if (instances.length === 0) {
counter.textContent = "No instances";
} else {
counter.textContent = `${currentInstanceIndex + 1} / ${instances.length}`;
}
}
}
// Update navigation buttons state
function updateNavButtons() {
const prevBtn = document.getElementById("overlayInstancePrev");
const nextBtn = document.getElementById("overlayInstanceNext");
const deleteBtn = document.getElementById("overlayInstanceDelete");
if (prevBtn) prevBtn.disabled = instances.length <= 1;
if (nextBtn) nextBtn.disabled = instances.length <= 1;
if (deleteBtn) deleteBtn.disabled = instances.length === 0;
}
// Load instance into editor
function loadInstance(index) {
if (index < 0 || index >= instances.length || !overlayEditorInstance) return;
currentInstanceIndex = index;
overlayEditorInstance.setValue(instances[index], -1);
updateCounter();
updateNavButtons();
}
// Navigate to previous instance
function prevInstance() {
if (instances.length <= 1) return;
currentInstanceIndex = (currentInstanceIndex - 1 + instances.length) % instances.length;
loadInstance(currentInstanceIndex);
}
// Navigate to next instance
function nextInstance() {
if (instances.length <= 1) return;
currentInstanceIndex = (currentInstanceIndex + 1) % instances.length;
loadInstance(currentInstanceIndex);
}
// Create new instance
function createNewInstance() {
const newContent = "<!-- New instance -->\n\n";
instances.push(newContent);
currentInstanceIndex = instances.length - 1;
loadInstance(currentInstanceIndex);
saveInstances(sourceFileName, instances);
if (typeof showToast === "function") {
showToast("â
New instance created", "success");
}
}
// Delete current instance
function deleteCurrentInstance() {
if (instances.length === 0) return;
if (instances.length === 1) {
// If it's the last one, just clear it
instances[0] = "<!-- No text selected -->\n\n";
loadInstance(0);
saveInstances(sourceFileName, instances);
if (typeof showToast === "function") {
showToast("đī¸ Instance cleared", "info");
}
return;
}
instances.splice(currentInstanceIndex, 1);
if (currentInstanceIndex >= instances.length) {
currentInstanceIndex = instances.length - 1;
}
loadInstance(currentInstanceIndex);
saveInstances(sourceFileName, instances);
if (typeof showToast === "function") {
showToast("đī¸ Instance deleted", "info");
}
}
// Clear all instances
function clearAllInstances() {
if (!confirm("Are you sure you want to delete all instances?")) return;
instances = ["<!-- No text selected -->\n\n"];
currentInstanceIndex = 0;
loadInstance(0);
saveInstances(sourceFileName, instances);
if (typeof showToast === "function") {
showToast("đī¸ All instances cleared", "info");
}
}
// =========================================================================
// TARGET DETECTION (Smart section matching)
// =========================================================================
function calculateHeaderScore(item, overlayFirstLine, itemFirstLine) {
const itemLabel = item.label?.toLowerCase() || '';
const overlayLower = overlayFirstLine.toLowerCase();
const itemLower = itemFirstLine.toLowerCase();
// Exact header match
if (itemLower === overlayLower) return 1.0;
// Label appears in overlay
if (itemLabel && overlayLower.includes(itemLabel)) return 0.9;
// Similar function/class names
const overlayWords = overlayLower.match(/\w+/g) || [];
const itemWords = itemLower.match(/\w+/g) || [];
const commonWords = overlayWords.filter(w => itemWords.includes(w) && w.length > 3);
if (commonWords.length > 0) {
return 0.7 + (commonWords.length * 0.1);
}
return 0.3;
}
function calculateContentScore(overlayLines, itemLines) {
let exactMatches = 0;
let partialMatches = 0;
for (const overlayLine of overlayLines) {
const overlayLower = overlayLine.toLowerCase();
for (const itemLine of itemLines) {
const itemLower = itemLine.toLowerCase();
if (itemLower === overlayLower) {
exactMatches++;
break;
} else if (itemLower.includes(overlayLower) || overlayLower.includes(itemLower)) {
partialMatches++;
break;
}
}
}
const score = (exactMatches * 1.0 + partialMatches * 0.5) / overlayLines.length;
return {
score: Math.min(score, 1.0),
exactMatches,
partialMatches
};
}
function detectTargetSection() {
console.log("[overlay_editor.js] Detecting target section...");
if (!overlayEditorInstance) {
console.error("[overlay_editor.js] No overlay editor instance");
if (typeof showToast === "function") {
showToast("â ī¸ Editor not initialized", "error");
}
return;
}
const content = overlayEditorInstance.getValue();
if (!content || !content.trim()) {
if (typeof showToast === "function") {
showToast("â ī¸ No content to analyze", "error");
}
return;
}
// Get the global editor instance
if (typeof window.AppItems === "undefined" || !window.AppItems.length) {
if (typeof showToast === "function") {
showToast("â ī¸ Main editor not available", "error");
}
return;
}
// Find the editor AppItem
const editorItem = window.AppItems.find(item => item.title === "HTML Editor");
if (!editorItem || !editorItem.getGlobalEditor) {
if (typeof showToast === "function") {
showToast("â ī¸ Could not access main editor", "error");
}
return;
}
const globalEditorInstance = editorItem.getGlobalEditor();
if (!globalEditorInstance) {
if (typeof showToast === "function") {
showToast("â ī¸ Main editor not initialized", "error");
}
return;
}
// Get index from EditorIndex module
if (typeof window.EditorIndex === "undefined" || !window.EditorIndex.generateDocumentIndex) {
if (typeof showToast === "function") {
showToast("â ī¸ Index module not loaded", "error");
}
return;
}
// Initialize EditorIndex if needed
if (window.EditorIndex.init) {
window.EditorIndex.init({
getGlobalEditor: () => globalEditorInstance,
showToast: typeof showToast === "function" ? showToast : null
});
}
const indexResult = window.EditorIndex.generateDocumentIndex();
let flatIndex = [];
// Flatten the hierarchical structure
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);
if (flatIndex.length === 0) {
if (typeof showToast === "function") {
showToast("âšī¸ No sections found in document", "info");
}
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] || '';
console.log(`[overlay_editor.js] Analyzing ${flatIndex.length} sections against overlay content`);
// Score each section in the document
for (const item of flatIndex) {
let itemStartRow, itemEndRow;
if (item.type === 'marker') {
itemStartRow = item.row;
itemEndRow = item.endRow || window.EditorIndex.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 scores
const headerScore = calculateHeaderScore(item, firstContentLine, itemLines[0] || '');
const contentScore = calculateContentScore(contentLines, itemLines);
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
});
}
// Sort by score (descending)
candidates.sort((a, b) => {
if (Math.abs(a.score - b.score) < 0.05) {
return a.size - b.size; // Prefer smaller sections for ties
}
return b.score - a.score;
});
const bestMatch = candidates[0];
console.log(`[overlay_editor.js] Best match: ${bestMatch.label} (score: ${(bestMatch.score * 100).toFixed(1)}%)`);
// Navigate to best match in main editor
const range = new Range(
bestMatch.startRow,
0,
bestMatch.endRow,
session.getLine(bestMatch.endRow).length
);
globalEditorInstance.selection.setRange(range, false);
globalEditorInstance.scrollToLine(bestMatch.startRow, true, true, () => {});
globalEditorInstance.focus();
if (typeof showToast === "function") {
const quality = bestMatch.score >= 0.9 ? "Excellent" : bestMatch.score >= 0.7 ? "Good" : "Fair";
showToast(`đ¯ ${bestMatch.label} (${quality} - ${(bestMatch.score * 100).toFixed(0)}%)`, "success");
}
console.log("[overlay_editor.js] Target detection complete");
}
// Create the overlay HTML
function createOverlayHTML() {
return `
<div id="overlayEditorContainer" style="
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.8);
z-index: 2147483647;
display: flex;
align-items: center;
justify-content: center;
backdrop-filter: blur(4px);
">
<div style="
background: #1e1e1e;
border: 1px solid #3a3a3a;
border-radius: 8px;
width: 90%;
max-width: 1200px;
height: 85%;
display: flex;
flex-direction: column;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.5);
position: relative;
">
<!-- Header -->
<div style="
padding: 12px 16px;
background: #2d2d2d;
border-bottom: 1px solid #3a3a3a;
border-radius: 8px 8px 0 0;
display: flex;
flex-direction: column;
gap: 8px;
">
<!-- First Row: Title and Primary Actions -->
<div style="
display: flex;
justify-content: space-between;
align-items: center;
gap: 8px;
flex-wrap: wrap;
">
<h3 style="
margin: 0;
color: #e0e0e0;
font-size: 16px;
font-weight: 500;
flex-shrink: 0;
">Edit Selection</h3>
<div style="display: flex; gap: 8px; flex-wrap: wrap;">
<button id="overlayEditorSave" style="
background: #007acc;
color: white;
border: none;
padding: 6px 16px;
border-radius: 4px;
cursor: pointer;
font-size: 13px;
font-weight: 500;
">Save</button>
<button id="overlayEditorCancel" style="
background: #3a3a3a;
color: #e0e0e0;
border: none;
padding: 6px 16px;
border-radius: 4px;
cursor: pointer;
font-size: 13px;
">Cancel</button>
</div>
</div>
<!-- Second Row: Navigation and Management -->
<div style="
display: flex;
justify-content: space-between;
align-items: center;
gap: 8px;
flex-wrap: wrap;
">
<!-- Navigation Controls -->
<div style="display: flex; align-items: center; gap: 8px;">
<button id="overlayInstancePrev" title="Previous instance (Alt+Left)" style="
background: #3a3a3a;
color: #e0e0e0;
border: none;
padding: 6px 12px;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
">â</button>
<span id="overlayInstanceCounter" style="
color: #e0e0e0;
font-size: 13px;
min-width: 60px;
text-align: center;
">1 / 1</span>
<button id="overlayInstanceNext" title="Next instance (Alt+Right)" style="
background: #3a3a3a;
color: #e0e0e0;
border: none;
padding: 6px 12px;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
">â</button>
</div>
<!-- Management Buttons -->
<div style="display: flex; gap: 6px; flex-wrap: wrap;">
<button id="overlayInstanceNew" title="Create new instance (Ctrl+N)" style="
background: #2e7d32;
color: white;
border: none;
padding: 6px 10px;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
">+ New</button>
<button id="overlayInstanceDelete" title="Delete current instance (Ctrl+D)" style="
background: #c62828;
color: white;
border: none;
padding: 6px 10px;
border-radius: 4px;
cursor: pointer;
font-size: 13px;
">đī¸</button>
<button id="overlayInstanceClearAll" title="Clear all instances" style="
background: #d32f2f;
color: white;
border: none;
padding: 6px 10px;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
">Clear All</button>
<button id="overlayDetectTarget" title="Find best matching section in document" style="
background: #3a7ca5;
color: white;
border: none;
padding: 6px 12px;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
margin-left: 8px;
">đ¯ Find Target</button>
</div>
</div>
</div>
<!-- Editor Container -->
<div id="overlayEditorDiv" style="
flex: 1;
position: relative;
overflow: hidden;
"></div>
</div>
</div>
`;
}
// Open the overlay editor with initial text
window.OverlayEditor.open = function (initialText, callback) {
console.log("[overlay_editor.js] open() called");
console.log("[overlay_editor.js] initialText length:", initialText?.length || 0);
if (overlayContainer) {
console.warn("[overlay_editor.js] Overlay already open");
return;
}
onSaveCallback = callback;
sourceFileName = getActiveFileName();
// Load existing instances for this file
instances = loadInstances(sourceFileName);
// If no instances exist, create one with the initial text
if (instances.length === 0) {
instances = [initialText || "<!-- No text selected -->\n\n"];
} else {
// Add the new selection as a new instance if it's not empty
if (initialText && initialText.trim() !== "" && initialText !== "<!-- No text selected -->\n\n") {
instances.push(initialText);
currentInstanceIndex = instances.length - 1;
} else {
currentInstanceIndex = instances.length - 1; // Load the last instance
}
}
// Save the updated instances
saveInstances(sourceFileName, instances);
// Create and append overlay
overlayContainer = document.createElement("div");
overlayContainer.innerHTML = createOverlayHTML();
// CRITICAL: Append to body (not inside any other container) and ensure it's the last element
if (document.body.lastElementChild !== overlayContainer) {
document.body.appendChild(overlayContainer);
}
console.log("[overlay_editor.js] Overlay container appended to body");
// Wait for DOM insertion
setTimeout(() => {
console.log("[overlay_editor.js] Initializing Ace editor...");
const editorDiv = document.getElementById("overlayEditorDiv");
if (!editorDiv) {
console.error("[overlay_editor.js] Editor div not found");
return;
}
// Check if Ace is available
if (typeof ace === 'undefined') {
console.error("[overlay_editor.js] Ace editor not loaded!");
if (typeof showToast === "function") {
showToast("â ī¸ Ace editor not loaded yet", "error");
}
window.OverlayEditor.close();
return;
}
// Initialize Ace editor
overlayEditorInstance = ace.edit(editorDiv);
overlayEditorInstance.setTheme("ace/theme/monokai");
overlayEditorInstance.session.setMode("ace/mode/html");
overlayEditorInstance.setOptions({
fontSize: "14px",
wrap: true,
showPrintMargin: false,
useWorker: false,
enableAutoIndent: true,
highlightActiveLine: true,
highlightGutterLine: true
});
// Load the current instance
loadInstance(currentInstanceIndex);
console.log("[overlay_editor.js] Ace editor initialized");
// Focus the editor
overlayEditorInstance.focus();
// Wire up buttons
const saveBtn = document.getElementById("overlayEditorSave");
const cancelBtn = document.getElementById("overlayEditorCancel");
const newBtn = document.getElementById("overlayInstanceNew");
const deleteBtn = document.getElementById("overlayInstanceDelete");
const clearAllBtn = document.getElementById("overlayInstanceClearAll");
const prevBtn = document.getElementById("overlayInstancePrev");
const nextBtn = document.getElementById("overlayInstanceNext");
if (saveBtn) {
saveBtn.addEventListener("click", () => {
console.log("[overlay_editor.js] Save button clicked");
// Update current instance with edited content
instances[currentInstanceIndex] = overlayEditorInstance.getValue();
saveInstances(sourceFileName, instances);
const editedText = overlayEditorInstance.getValue();
window.OverlayEditor.close();
if (onSaveCallback) {
onSaveCallback(editedText);
}
});
}
if (cancelBtn) {
cancelBtn.addEventListener("click", () => {
console.log("[overlay_editor.js] Cancel button clicked");
window.OverlayEditor.close();
});
}
if (newBtn) {
newBtn.addEventListener("click", createNewInstance);
}
if (deleteBtn) {
deleteBtn.addEventListener("click", deleteCurrentInstance);
}
if (clearAllBtn) {
clearAllBtn.addEventListener("click", clearAllInstances);
}
if (prevBtn) {
prevBtn.addEventListener("click", prevInstance);
}
if (nextBtn) {
nextBtn.addEventListener("click", nextInstance);
}
// Find Target button
const targetBtn = document.getElementById("overlayDetectTarget");
if (targetBtn) {
targetBtn.addEventListener("click", () => {
console.log("[overlay_editor.js] Find Target button clicked");
detectTargetSection();
});
}
// Auto-save current instance when switching
overlayEditorInstance.getSession().on("change", () => {
// Update the current instance in memory
instances[currentInstanceIndex] = overlayEditorInstance.getValue();
});
// Keyboard shortcuts
overlayEditorInstance.commands.addCommand({
name: "closeOverlay",
bindKey: { win: "Esc", mac: "Esc" },
exec: () => {
console.log("[overlay_editor.js] ESC pressed");
window.OverlayEditor.close();
}
});
overlayEditorInstance.commands.addCommand({
name: "saveOverlay",
bindKey: { win: "Ctrl-S", mac: "Command-S" },
exec: () => {
console.log("[overlay_editor.js] Save shortcut pressed");
instances[currentInstanceIndex] = overlayEditorInstance.getValue();
saveInstances(sourceFileName, instances);
const editedText = overlayEditorInstance.getValue();
window.OverlayEditor.close();
if (onSaveCallback) {
onSaveCallback(editedText);
}
}
});
overlayEditorInstance.commands.addCommand({
name: "newInstance",
bindKey: { win: "Ctrl-N", mac: "Command-N" },
exec: createNewInstance
});
overlayEditorInstance.commands.addCommand({
name: "deleteInstance",
bindKey: { win: "Ctrl-D", mac: "Command-D" },
exec: deleteCurrentInstance
});
overlayEditorInstance.commands.addCommand({
name: "prevInstance",
bindKey: { win: "Alt-Left", mac: "Alt-Left" },
exec: prevInstance
});
overlayEditorInstance.commands.addCommand({
name: "nextInstance",
bindKey: { win: "Alt-Right", mac: "Alt-Right" },
exec: nextInstance
});
}, 50);
};
// Close the overlay editor
window.OverlayEditor.close = function () {
// Save all instances before closing
if (instances.length > 0 && sourceFileName) {
saveInstances(sourceFileName, instances);
}
if (overlayEditorInstance) {
overlayEditorInstance.destroy();
overlayEditorInstance = null;
}
if (overlayContainer) {
overlayContainer.remove();
overlayContainer = null;
}
onSaveCallback = null;
instances = [];
currentInstanceIndex = 0;
sourceFileName = "";
};
// Check if ready method exists, if not create it
window.OverlayEditor.ready = function(callback) {
if (window.OverlayEditor._ready) {
callback();
} else {
window.OverlayEditor._readyCallbacks.push(callback);
}
};
// Get current editor value (for target detection)
window.OverlayEditor.getValue = function() {
if (overlayEditorInstance) {
return overlayEditorInstance.getValue();
}
return "";
};
// Mark as ready
window.OverlayEditor._ready = true;
// Execute any pending callbacks
if (window.OverlayEditor._readyCallbacks.length > 0) {
console.log("[overlay_editor.js] Executing", window.OverlayEditor._readyCallbacks.length, "pending callbacks");
window.OverlayEditor._readyCallbacks.forEach(cb => cb());
window.OverlayEditor._readyCallbacks = [];
}
console.log("[overlay_editor.js] â Module loaded successfully");
} catch (err) {
console.error("[overlay_editor.js] Fatal error:", err);
}
})();