/**
* Editor Index Module v3
* Hierarchical, language-aware document structure navigation
* Uses localStorage for data access - no direct Ace dependency
*/
(function() {
'use strict';
console.log("[editor_index.js v3] Loading editor index module...");
// =========================================================================
// LOCALSTORAGE KEYS
// =========================================================================
const ACTIVE_FILES_KEY = "sftp_active_files";
// =========================================================================
// DEPENDENCIES (injected from main editor)
// =========================================================================
let deps = {
getGlobalEditor: null, // Optional - for navigation if available
showToast: null
};
// =========================================================================
// LANGUAGE DETECTION
// =========================================================================
const LANGUAGE_INFO = {
php: { icon: '📄', color: '#8892BF' },
js: { icon: '⚙️', color: '#F7DF1E' },
javascript: { icon: '⚙️', color: '#F7DF1E' },
css: { icon: '🎨', color: '#264DE4' },
html: { icon: '📦', color: '#E34F26' },
htm: { icon: '📦', color: '#E34F26' }
};
function getLanguageInfo(lang) {
const normalized = lang?.toLowerCase() || 'unknown';
return LANGUAGE_INFO[normalized] || { icon: '📝', color: '#888' };
}
// =========================================================================
// LOCALSTORAGE ACCESS
// =========================================================================
/**
* Get the active file content from localStorage
*/
function getActiveFileContent() {
try {
const files = JSON.parse(localStorage.getItem(ACTIVE_FILES_KEY) || "[]");
const active = files.find(f => f.active);
return {
content: active?.content || "",
name: active?.name || "Untitled",
path: active?.path || ""
};
} catch (err) {
console.error("[editor_index.js v3] Failed to load file from localStorage:", err);
return { content: "", name: "Untitled", path: "" };
}
}
/**
* Get all lines from the active file
*/
function getLines() {
const { content } = getActiveFileContent();
return content.split('\n');
}
/**
* Get a specific line by index
*/
function getLine(index) {
const lines = getLines();
return lines[index] || "";
}
/**
* Get total line count
*/
function getLineCount() {
return getLines().length;
}
// =========================================================================
// MARKER PARSING
// =========================================================================
/**
* Parse marker name into components
* Examples:
* "buttons_css_1" -> { component: "buttons", language: "css", number: 1 }
* "buttons_css" -> { component: "buttons", language: "css", number: null }
* "buttons" -> { component: "buttons", language: null, number: null }
*/
function parseMarkerName(markerName) {
// Remove brackets if present
const cleaned = markerName.replace(/[\[\]]/g, '').trim();
const parts = cleaned.split('_');
if (parts.length === 1) {
// Just component name: "buttons"
return {
component: parts[0],
language: null,
number: null,
fullName: cleaned
};
}
if (parts.length === 2) {
// Component + language: "buttons_css"
return {
component: parts[0],
language: parts[1],
number: null,
fullName: cleaned
};
}
if (parts.length >= 3) {
// Component + language + number: "buttons_css_1"
const number = parseInt(parts[2]);
return {
component: parts[0],
language: parts[1],
number: isNaN(number) ? null : number,
fullName: cleaned
};
}
return {
component: cleaned,
language: null,
number: null,
fullName: cleaned
};
}
/**
* Find the closing marker for a given opening marker
*/
function findMarkerEnd(startRow, markerName) {
const lineCount = getLineCount();
for (let row = startRow + 1; row < lineCount; row++) {
const line = getLine(row);
const trimmed = line.trim();
// Look for closing marker with >
if (trimmed.includes('>')) {
const closeMatch = trimmed.match(/(?:<!--|\/\*|\/\/\/|\/\/)\s*([\w\-\[\]_]+)\s*>/);
if (closeMatch && closeMatch[1].trim() === markerName) {
return row;
}
}
}
return startRow; // No closing found, return start
}
// =========================================================================
// INDEX GENERATION (HIERARCHICAL MODE)
// =========================================================================
function generateHierarchicalIndex() {
const lineCount = getLineCount();
const components = {}; // Hierarchical structure
const unmarked = []; // Items without proper markers
const markerRanges = []; // Track which rows are inside markers
// =====================================================================
// FIRST PASS: Find all markers and their ranges
// =====================================================================
for (let row = 0; row < lineCount; row++) {
const line = getLine(row);
const trimmed = line.trim();
if (!trimmed) continue;
// Improved marker detection - opening markers with <
if (trimmed.includes('<')) {
let markerMatch = trimmed.match(/(?:<!--|\/\*|\/\/\/|\/\/)\s*([\w\-\[\]_]+)\s*</);
if (markerMatch) {
const markerName = markerMatch[1].trim();
const parsed = parseMarkerName(markerName);
// Find closing marker
const endRow = findMarkerEnd(row, markerName);
const markerItem = {
type: 'marker',
row: row,
endRow: endRow,
label: markerName,
parsed: parsed,
preview: trimmed.substring(0, 60) + (trimmed.length > 60 ? '...' : ''),
children: [] // Will hold items found inside this marker
};
// Track this range
markerRanges.push({
startRow: row,
endRow: endRow,
markerItem: markerItem
});
if (parsed.language) {
// Has language specification - add to components
if (!components[parsed.component]) {
components[parsed.component] = {};
}
if (!components[parsed.component][parsed.language]) {
components[parsed.component][parsed.language] = [];
}
components[parsed.component][parsed.language].push(markerItem);
} else {
// No language - add to unmarked
unmarked.push(markerItem);
}
}
}
}
// =====================================================================
// SECOND PASS: Find all items and assign them to markers or unmarked
// =====================================================================
for (let row = 0; row < lineCount; row++) {
const line = getLine(row);
const trimmed = line.trim();
if (!trimmed) continue;
// Skip marker opening/closing lines
if (trimmed.includes('<') || trimmed.includes('>')) {
if (trimmed.match(/(?:<!--|\/\*|\/\/\/|\/\/)\s*[\w\-\[\]_]+\s*[<>]/)) {
continue;
}
}
let match;
let item = null;
// HTML tags
if ((match = trimmed.match(/^<(div|section|nav|header|footer|main|article|aside|button)[^>]*(?:id=["']([^"']+)["']|class=["']([^"']+)["'])?[^>]*>/i))) {
const tag = match[1];
const id = match[2];
const className = match[3];
const label = id ? `#${id}` : (className ? `.${className.split(' ')[0]}` : `<${tag}>`);
item = {
type: 'html',
row: row,
label: label,
icon: '📦',
preview: trimmed.substring(0, 60) + (trimmed.length > 60 ? '...' : '')
};
}
// JavaScript functions
else if ((match = trimmed.match(/(?:function\s+(\w+)|(?:const|let|var)\s+(\w+)\s*=\s*(?:function|\([^)]*\)\s*=>))/))) {
const funcName = match[1] || match[2];
item = {
type: 'function',
row: row,
label: `${funcName}()`,
icon: '⚙️',
preview: trimmed.substring(0, 60) + (trimmed.length > 60 ? '...' : '')
};
}
// PHP functions
else if ((match = trimmed.match(/(?:public|private|protected|static)?\s*function\s+(\w+)\s*\(/))) {
item = {
type: 'function',
row: row,
label: `${match[1]}()`,
icon: '🔧',
preview: trimmed.substring(0, 60) + (trimmed.length > 60 ? '...' : '')
};
}
// CSS classes
else if ((match = trimmed.match(/^\.([a-zA-Z0-9_-]+)\s*\{/))) {
item = {
type: 'css',
row: row,
label: `.${match[1]}`,
icon: '🎨',
preview: trimmed.substring(0, 60) + (trimmed.length > 60 ? '...' : '')
};
}
// CSS IDs
else if ((match = trimmed.match(/^#([a-zA-Z0-9_-]+)\s*\{/))) {
item = {
type: 'css',
row: row,
label: `#${match[1]}`,
icon: '🎨',
preview: trimmed.substring(0, 60) + (trimmed.length > 60 ? '...' : '')
};
}
if (!item) continue;
// Find which marker this item belongs to
let belongsToMarker = null;
for (const range of markerRanges) {
if (row > range.startRow && row < range.endRow) {
belongsToMarker = range.markerItem;
break;
}
}
if (belongsToMarker) {
belongsToMarker.children.push(item);
} else {
unmarked.push(item);
}
}
return { components, unmarked };
}
// =========================================================================
// HTML GENERATION
// =========================================================================
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function generateHierarchicalHTML(data) {
const { components, unmarked } = data;
let html = `
<div style="padding: 20px; color: #e0e0e0;">
<h2 style="margin: 0 0 20px 0; color: #fff;">📑 Document Index</h2>
`;
// Render components
const componentNames = Object.keys(components).sort();
if (componentNames.length > 0) {
componentNames.forEach(componentName => {
const languages = components[componentName];
const languageNames = Object.keys(languages).sort();
html += `
<div style="margin-bottom: 16px; background: #2d2d2d; border-radius: 6px; overflow: hidden;">
<div class="component-header" data-component="${escapeHtml(componentName)}" style="
padding: 12px 16px;
background: #3a3a3a;
cursor: pointer;
display: flex;
align-items: center;
gap: 8px;
font-weight: 600;
font-size: 15px;
user-select: none;
">
<span class="collapse-icon">▼</span>
<span>📦 ${escapeHtml(componentName)}</span>
<span style="color: #888; font-size: 12px; font-weight: normal;">(${languageNames.length} language${languageNames.length !== 1 ? 's' : ''})</span>
</div>
<div class="component-content" data-component="${escapeHtml(componentName)}" style="padding: 8px;">
`;
// Render languages within component
languageNames.forEach(langName => {
const markers = languages[langName];
const langInfo = getLanguageInfo(langName);
html += `
<div style="margin-bottom: 8px;">
<div class="language-header" style="
padding: 8px 12px;
background: #3a3a3a;
cursor: pointer;
display: flex;
align-items: center;
gap: 8px;
border-radius: 4px;
font-size: 13px;
user-select: none;
">
<span>▼</span>
<span>${langInfo.icon}</span>
<span style="color: ${langInfo.color}; font-weight: 500;">${escapeHtml(langName)}</span>
<span style="color: #888; font-size: 11px;">(${markers.length} marker${markers.length !== 1 ? 's' : ''})</span>
</div>
<div class="language-content" style="padding-left: 16px;">
`;
// Render markers
markers.forEach(marker => {
html += `
<div class="index-item" data-row="${marker.row}" data-is-marker="true" data-label="${escapeHtml(marker.label)}" style="
padding: 8px 12px;
margin: 4px 0;
background: #2d2d2d;
border-radius: 4px;
cursor: pointer;
transition: background 0.2s;
">
<div style="display: flex; align-items: center; gap: 8px;">
<span style="font-size: 16px;">🏷️</span>
<div style="flex: 1; min-width: 0;">
<div style="color: #e0e0e0; font-weight: 500; font-size: 13px;">${escapeHtml(marker.label)}</div>
<div style="color: #888; font-size: 11px;">Lines ${marker.row + 1}–${marker.endRow + 1}</div>
</div>
</div>
`;
// Render children (items inside this marker)
if (marker.children && marker.children.length > 0) {
html += `<div style="margin-top: 8px; padding-left: 20px; border-left: 2px solid #444;">`;
marker.children.forEach(child => {
html += `
<div class="index-item-child" data-row="${child.row}" style="
padding: 6px 10px;
margin: 2px 0;
background: #252525;
border-radius: 3px;
cursor: pointer;
font-size: 12px;
">
<div style="display: flex; align-items: center; gap: 6px;">
<span>${child.icon}</span>
<span style="color: #d0d0d0;">${escapeHtml(child.label)}</span>
<span style="color: #666; font-size: 10px;">:${child.row + 1}</span>
</div>
</div>
`;
});
html += `</div>`;
}
html += `</div>`;
});
html += `
</div>
</div>
`;
});
html += `
</div>
</div>
`;
});
}
// Render unmarked items
if (unmarked.length > 0) {
html += `
<div style="margin-top: 20px;">
<h3 style="color: #aaa; font-size: 14px; margin-bottom: 10px;">📌 Unmarked Items</h3>
`;
unmarked.forEach(item => {
const icon = item.type === 'marker' ? '🏷️' : item.icon;
const isMarker = item.type === 'marker';
html += `
<div class="index-item" data-row="${item.row}" data-is-marker="${isMarker}" data-label="${isMarker ? escapeHtml(item.label) : ''}" style="
padding: 8px 12px;
margin: 4px 0;
background: #3d3d3d;
border-radius: 4px;
cursor: pointer;
transition: background 0.2s;
">
<div style="display: flex; align-items: center; gap: 8px;">
<span style="font-size: 16px;">${icon}</span>
<div style="flex: 1; min-width: 0;">
<div style="color: #e0e0e0; font-weight: 500; font-size: 14px;">${escapeHtml(item.label)}</div>
<div style="color: #888; font-size: 12px;">Line ${item.row + 1}</div>
</div>
</div>
</div>
`;
});
html += `</div>`;
}
if (componentNames.length === 0 && unmarked.length === 0) {
html += `
<div style="padding: 40px; text-align: center; color: #888;">
<div style="font-size: 48px; margin-bottom: 16px;">📄</div>
<div style="font-size: 16px;">No structure found</div>
<div style="font-size: 13px; margin-top: 8px;">Add markers or structural elements to see the index</div>
</div>
`;
}
html += `</div>`;
return html;
}
// =========================================================================
// STANDALONE INDEX OVERLAY (Doesn't interfere with editor or OverlayEditor)
// =========================================================================
let indexOverlayElement = null;
function createIndexOverlay() {
// Create overlay container
const overlay = document.createElement('div');
overlay.id = 'indexOverlay';
overlay.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.95);
z-index: 2147483641;
display: flex;
align-items: center;
justify-content: center;
backdrop-filter: blur(6px);
`;
// Create content container
const content = document.createElement('div');
content.style.cssText = `
background: #1e1e1e;
border: 1px solid #444;
border-radius: 8px;
width: 90%;
max-width: 800px;
max-height: 90vh;
overflow: hidden;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.8);
position: relative;
z-index: 2147483642;
display: flex;
flex-direction: column;
`;
// Create header with close button
const header = document.createElement('div');
header.style.cssText = `
padding: 16px 20px;
background: #2d2d2d;
border-bottom: 1px solid #444;
display: flex;
align-items: center;
justify-content: space-between;
`;
const title = document.createElement('h2');
title.textContent = '📑 Document Index';
title.style.cssText = `
margin: 0;
color: #fff;
font-size: 18px;
font-family: 'Segoe UI', sans-serif;
`;
const closeBtn = document.createElement('button');
closeBtn.textContent = '✕';
closeBtn.style.cssText = `
background: #3a3a3a;
border: 1px solid #555;
color: #fff;
font-size: 20px;
width: 32px;
height: 32px;
border-radius: 4px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: background 0.2s;
`;
closeBtn.addEventListener('mouseenter', () => {
closeBtn.style.background = '#4a4a4a';
});
closeBtn.addEventListener('mouseleave', () => {
closeBtn.style.background = '#3a3a3a';
});
closeBtn.addEventListener('click', closeIndexOverlay);
header.appendChild(title);
header.appendChild(closeBtn);
// Create scrollable body
const body = document.createElement('div');
body.style.cssText = `
flex: 1;
overflow-y: auto;
padding: 0;
`;
content.appendChild(header);
content.appendChild(body);
overlay.appendChild(content);
// Close on background click
overlay.addEventListener('click', (e) => {
if (e.target === overlay) {
closeIndexOverlay();
}
});
// Close on ESC key
const escHandler = (e) => {
if (e.key === 'Escape') {
closeIndexOverlay();
document.removeEventListener('keydown', escHandler);
}
};
document.addEventListener('keydown', escHandler);
return { overlay, content: body };
}
function closeIndexOverlay() {
if (indexOverlayElement) {
console.log("[editor_index.js v3] Closing index overlay");
indexOverlayElement.remove();
indexOverlayElement = null;
}
}
// =========================================================================
// SHOW INDEX (PUBLIC)
// =========================================================================
function showIndexOverlay() {
console.log("[editor_index.js v3] Generating hierarchical index...");
// Close any existing index overlay first
if (indexOverlayElement) {
console.log("[editor_index.js v3] Closing existing index overlay");
closeIndexOverlay();
}
const data = generateHierarchicalIndex();
const html = generateHierarchicalHTML(data);
// Create standalone overlay
console.log("[editor_index.js v3] Creating index overlay...");
const { overlay, content } = createIndexOverlay();
content.innerHTML = html;
// --- Ensure overlay is absolutely top-level ---
overlay.style.zIndex = '2147483647'; // Max possible value
overlay.style.position = 'fixed'; // Just in case
overlay.style.inset = '0'; // Cover whole viewport
// --- Add to the very top of the document ---
document.body.appendChild(overlay); // Add once
document.body.append(overlay); // Re-append to ensure it's last in DOM
indexOverlayElement = overlay;
console.log("[editor_index.js v3] Index overlay appended to body, z-index:", overlay.style.zIndex);
console.log("[editor_index.js v3] Total elements in body:", document.body.children.length);
console.log("[editor_index.js v3] Index overlay element:", overlay);
// Attach event listeners
attachEventListeners(content);
console.log("[editor_index.js v3] Index overlay opened successfully");
}
// =========================================================================
// EVENT LISTENERS
// =========================================================================
function attachEventListeners(el) {
setTimeout(() => {
// Component collapse/expand
el.querySelectorAll('.component-header').forEach(header => {
header.addEventListener('click', (e) => {
const component = header.dataset.component;
const content = el.querySelector(`.component-content[data-component="${component}"]`);
const icon = header.querySelector('.collapse-icon');
if (content.style.display === 'none') {
content.style.display = 'block';
icon.textContent = '▼';
} else {
content.style.display = 'none';
icon.textContent = '▶';
}
});
});
// Language collapse/expand
el.querySelectorAll('.language-header').forEach(header => {
header.addEventListener('click', (e) => {
e.stopPropagation();
const content = header.nextElementSibling;
const icon = header.querySelector('span:first-child');
if (content.style.display === 'none') {
content.style.display = 'block';
icon.textContent = '▼';
} else {
content.style.display = 'none';
icon.textContent = '▶';
}
});
});
// Item click - navigate directly
el.querySelectorAll('.index-item').forEach(item => {
item.addEventListener('mouseenter', () => {
item.style.background = '#4a5568';
});
item.addEventListener('mouseleave', () => {
item.style.background = item.closest('.language-content') ? '#2d2d2d' : '#3d3d3d';
});
item.addEventListener('click', () => {
const row = parseInt(item.dataset.row);
const isMarker = item.dataset.isMarker === 'true';
const label = item.dataset.label;
if (isMarker) {
navigateToMarker(row, label);
} else {
navigateToRow(row);
}
// Navigation function handles closing the overlay
});
});
// Child item click - navigate directly
el.querySelectorAll('.index-item-child').forEach(item => {
item.addEventListener('mouseenter', () => {
item.style.background = '#3a3a3a';
});
item.addEventListener('mouseleave', () => {
item.style.background = '#252525';
});
item.addEventListener('click', (e) => {
e.stopPropagation(); // Don't trigger parent marker click
const row = parseInt(item.dataset.row);
navigateToRow(row);
// Navigation function handles closing the overlay
});
});
}, 50);
}
// =========================================================================
// NAVIGATION (FAST - Direct editor communication)
// =========================================================================
/**
* Navigate directly to marker and close index
*/
function navigateToMarker(startRow, label) {
const globalEditorInstance = deps.getGlobalEditor ? deps.getGlobalEditor() : null;
if (!globalEditorInstance) {
console.error("[editor_index.js v3] No editor instance available");
if (deps.showToast) {
deps.showToast("⚠️ Editor not available", "error");
}
return;
}
const endRow = findMarkerEnd(startRow, label);
console.log(`[editor_index.js v3] Direct navigation to ${label} at rows ${startRow}-${endRow}`);
// Navigate immediately if Ace is available
if (typeof ace !== 'undefined') {
const session = globalEditorInstance.getSession();
const Range = ace.require('ace/range').Range;
const range = new Range(
startRow,
0,
endRow,
session.getLine(endRow).length
);
globalEditorInstance.selection.setRange(range, false);
globalEditorInstance.scrollToLine(startRow, true, true, () => {});
globalEditorInstance.focus();
// Show toast
if (deps.showToast) {
deps.showToast(`📍 ${label}`, "success");
}
console.log(`[editor_index.js v3] ✓ Navigated to ${label}`);
}
// Close the standalone index overlay
closeIndexOverlay();
}
/**
* Navigate directly to row and close index
*/
function navigateToRow(row) {
const globalEditorInstance = deps.getGlobalEditor ? deps.getGlobalEditor() : null;
if (!globalEditorInstance) {
console.error("[editor_index.js v3] No editor instance available");
if (deps.showToast) {
deps.showToast("⚠️ Editor not available", "error");
}
return;
}
const line = getLine(row);
// Determine type and label from line content
let type = 'line';
let label = `Line ${row + 1}`;
if (line.includes('function')) {
type = 'function';
const match = line.match(/function\s+(\w+)|(\w+)\s*=\s*function/);
if (match) label = (match[1] || match[2]) + '()';
} else if (line.includes('class=') || line.includes('id=')) {
type = 'html';
const idMatch = line.match(/id=["']([^"']+)["']/);
const classMatch = line.match(/class=["']([^"']+)["']/);
if (idMatch) label = `#${idMatch[1]}`;
else if (classMatch) label = `.${classMatch[1].split(' ')[0]}`;
} else if (line.match(/^[\.\#][\w-]+\s*\{/)) {
type = 'css';
const match = line.match(/^([\.\#][\w-]+)/);
if (match) label = match[1];
}
console.log(`[editor_index.js v3] Direct navigation to ${label} (${type}) at row ${row}`);
// Navigate immediately if Ace is available
if (typeof ace !== 'undefined') {
const session = globalEditorInstance.getSession();
const Range = ace.require('ace/range').Range;
const foldRange = session.getFoldWidgetRange(row);
if (foldRange) {
const extended = new Range(
foldRange.start.row,
0,
foldRange.end.row,
session.getLine(foldRange.end.row).length
);
globalEditorInstance.selection.setRange(extended, false);
} else {
const range = new Range(row, 0, row, line.length);
globalEditorInstance.selection.setRange(range, false);
}
globalEditorInstance.scrollToLine(row, true, true, () => {});
globalEditorInstance.focus();
// Show toast
if (deps.showToast) {
deps.showToast(`📍 ${label}`, "success");
}
console.log(`[editor_index.js v3] ✓ Navigated to ${label}`);
}
// Close the standalone index overlay
closeIndexOverlay();
}
// =========================================================================
// INITIALIZATION
// =========================================================================
function init(dependencies) {
deps = { ...deps, ...dependencies };
console.log("[editor_index.js v3] Initialized with dependencies");
}
// =========================================================================
// PUBLIC API
// =========================================================================
window.EditorIndex = {
init: init,
generateDocumentIndex: generateHierarchicalIndex,
findMarkerEnd: findMarkerEnd,
showIndexOverlay: showIndexOverlay,
navigateToMarker: navigateToMarker,
navigateToRow: navigateToRow,
parseMarkerName: parseMarkerName,
// Data access methods for external use
getActiveFileContent: getActiveFileContent,
getLines: getLines,
getLine: getLine,
getLineCount: getLineCount
};
console.log("[editor_index.js v3] Module loaded successfully");
})();