// Storage Editor - Core Editor Component (editor.js)
(function() {
const ACTIVE_FILES_KEY = 'sftp_active_files';
// === LANGUAGE HANDLING ===
const LANG_DEFS = {
html: {
mode: "ace/mode/html",
exts: ["html", "htm"],
comment: { open: "<!--", close: "-->" }
},
css: {
mode: "ace/mode/css",
exts: ["css"],
comment: { open: "/*", close: "*/" }
},
javascript: {
mode: "ace/mode/javascript",
exts: ["js", "mjs", "cjs"],
comment: { open: "//", close: "" }
},
php: {
mode: "ace/mode/php",
exts: ["php", "phtml"],
comment: { open: "//", close: "" }
},
json: {
mode: "ace/mode/json",
exts: ["json"],
comment: { open: "//", close: "" }
},
markdown: {
mode: "ace/mode/markdown",
exts: ["md", "markdown"],
comment: { open: "<!--", close: "-->" }
},
python: {
mode: "ace/mode/python",
exts: ["py"],
comment: { open: "#", close: "" }
},
text: {
mode: "ace/mode/text",
exts: ["txt"],
comment: { open: "//", close: "" }
}
};
// Choose Ace mode based on file extension
function modeFromFileName(fileName = "") {
const ext = (fileName.split(".").pop() || "").toLowerCase();
for (const [lang, def] of Object.entries(LANG_DEFS)) {
if (def.exts.includes(ext)) return def.mode;
}
return LANG_DEFS.html.mode;
}
// Get language name from mode
function getLangNameFromMode(mode) {
for (const [lang, def] of Object.entries(LANG_DEFS)) {
if (def.mode === mode || mode.includes(lang)) return lang;
}
return 'text';
}
// Detect which sublanguage is active under the cursor
function detectSubLanguage(editor) {
const pos = editor.getCursorPosition();
const row = pos.row;
const col = pos.column;
// Get tokens on current line
const tokens = editor.session.getTokens(row);
// Find which token we're in
let currentCol = 0;
let activeToken = null;
for (const token of tokens) {
const tokenEnd = currentCol + token.value.length;
if (col >= currentCol && col <= tokenEnd) {
activeToken = token;
break;
}
currentCol = tokenEnd;
}
if (!activeToken) {
activeToken = tokens[tokens.length - 1] || { type: 'text' };
}
const type = (activeToken.type || '').toLowerCase();
const currentMode = editor.session.getMode().$id || "";
// Check for JavaScript
if (type.includes('javascript') ||
type.includes('js') ||
type.includes('script') ||
type === 'storage.type.js' ||
type === 'keyword.operator.js' ||
type.startsWith('source.js')) {
return "javascript";
}
// Check for CSS
if (type.includes('css') ||
type.includes('style') ||
type === 'source.css' ||
type.startsWith('entity.name.tag.css') ||
type.startsWith('support.type.property-name')) {
return "css";
}
// Check for PHP
if (type.includes('php') ||
type.includes('meta.tag.php') ||
type === 'source.php' ||
type.startsWith('keyword.control.php') ||
type.startsWith('support.function.php')) {
return "php";
}
// For HTML/PHP files, check context
if (currentMode.includes("html") || currentMode.includes("php")) {
let scriptDepth = 0;
let styleDepth = 0;
let phpDepth = 0;
for (let i = 0; i <= row; i++) {
const line = editor.session.getLine(i);
scriptDepth += (line.match(/<script[^>]*>/gi) || []).length;
scriptDepth -= (line.match(/<\/script>/gi) || []).length;
styleDepth += (line.match(/<style[^>]*>/gi) || []).length;
styleDepth -= (line.match(/<\/style>/gi) || []).length;
phpDepth += (line.match(/<\?php/gi) || []).length;
phpDepth -= (line.match(/\?>/gi) || []).length;
}
if (scriptDepth > 0) return "javascript";
if (styleDepth > 0) return "css";
if (phpDepth > 0) return "php";
if (type.includes('tag') || type.includes('attr') || type.includes('entity.name.tag')) {
return "html";
}
}
// Fallback to mode
if (currentMode.includes("javascript")) return "javascript";
if (currentMode.includes("css")) return "css";
if (currentMode.includes("python")) return "python";
if (currentMode.includes("markdown")) return "markdown";
if (currentMode.includes("json")) return "json";
if (currentMode.includes("php")) return "php";
if (currentMode.includes("html")) return "html";
return "text";
}
// Return correct comment delimiters for the detected language
function getCommentStyleFor(langKey) {
return LANG_DEFS[langKey]?.comment || { open: "//", close: "" };
}
// Wait for ACE to be available
function waitForAce(callback, attempts = 0) {
if (typeof ace !== 'undefined') {
console.log('✅ ACE Editor is available');
callback();
} else if (attempts < 50) {
setTimeout(() => waitForAce(callback, attempts + 1), 100);
} else {
console.error('❌ ACE Editor failed to load after 5 seconds');
callback();
}
}
// Load active files from localStorage
function loadActiveFiles() {
try {
const saved = localStorage.getItem(ACTIVE_FILES_KEY);
return saved ? JSON.parse(saved) : [];
} catch (e) {
console.error('Failed to load active files:', e);
return [];
}
}
// Save active files to localStorage
function saveActiveFiles(files) {
try {
localStorage.setItem(ACTIVE_FILES_KEY, JSON.stringify(files));
return true;
} catch (e) {
console.error('Failed to save active files:', e);
return false;
}
}
// Get the currently active file
function getActiveFile() {
const files = loadActiveFiles();
return files.find(f => f.active) || null;
}
// Update the active file content
function updateActiveFileContent(content) {
const files = loadActiveFiles();
const activeIndex = files.findIndex(f => f.active);
if (activeIndex !== -1) {
files[activeIndex].content = content;
files[activeIndex].lastModified = new Date().toISOString();
saveActiveFiles(files);
return true;
}
return false;
}
// Create the editor interface
function createEditorHTML() {
return `
<div style="
display: flex;
flex-direction: column;
height: 100%;
background: #1e1e1e;
">
<!-- File Info Bar -->
<div id="editorFileInfo" style="
background: #252525;
padding: 8px 16px;
border-bottom: 1px solid #3a3a3a;
display: flex;
justify-content: space-between;
align-items: center;
color: #9aa4b2;
font-size: 14px;
flex-shrink: 0;
">
<div style="display: flex; align-items: center; gap: 12px;">
<div>
<span id="editorFileName" style="color: #e6edf3; font-weight: 600;">No file selected</span>
<span id="editorFilePath" style="margin-left: 12px; color: #64748b; font-size: 12px;"></span>
</div>
<button id="openScopesBtn" class="editor-toolbar-btn" style="
background: #1a1a1a;
border: 1px solid #2a2a2a;
color: #fff;
padding: 6px 16px;
border-radius: 0;
cursor: pointer;
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 1px;
">🎯 Scopes</button>
<button id="addMarkerBtn" class="editor-toolbar-btn" style="
background: #1a1a1a;
border: 1px solid #2a2a2a;
color: #fff;
padding: 6px 16px;
border-radius: 0;
cursor: pointer;
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 1px;
">➕ Mark</button>
</div>
<div id="editorStatus" style="color: #10b981;">
● Auto-save enabled
</div>
</div>
<!-- ACE Editor Container -->
<div id="aceEditorContainer" style="
flex:1;
position:relative;
width:100%;
min-height:600px;
"></div>
<!-- Status Bar -->
<div style="
background: #252525;
padding: 6px 16px;
border-top: 1px solid #3a3a3a;
display: flex;
justify-content: space-between;
align-items: center;
font-size: 12px;
color: #64748b;
flex-shrink: 0;
">
<div id="editorStats">
Lines: <span id="lineCount">0</span> |
Characters: <span id="charCount">0</span> |
Mode: <span id="editorMode">text</span> |
Cursor: <span id="cursorLang" style="color: #3b82f6; font-weight: 600;">-</span>
</div>
<div id="lastSaved">Last saved: <span id="lastSavedTime">Never</span></div>
</div>
</div>
`;
}
// Initialize ACE editor functionality
function initializeEditor(containerEl) {
console.log('🔧 Initializing ACE editor...');
if (typeof ace === 'undefined') {
console.error('❌ ACE Editor is not available');
containerEl.innerHTML = `
<div style="padding: 40px; text-align: center; color: #ef4444;">
<h2>⚠️ ACE Editor Not Loaded</h2>
<p>ACE Editor library is not available.</p>
<button onclick="location.reload()" style="
margin-top: 20px;
padding: 10px 20px;
background: #3b82f6;
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
">🔄 Reload Page</button>
</div>
`;
return;
}
const editorContainer = containerEl.querySelector('#aceEditorContainer');
const fileNameEl = containerEl.querySelector('#editorFileName');
const filePathEl = containerEl.querySelector('#editorFilePath');
const statusEl = containerEl.querySelector('#editorStatus');
const lineCountEl = containerEl.querySelector('#lineCount');
const charCountEl = containerEl.querySelector('#charCount');
const modeEl = containerEl.querySelector('#editorMode');
const cursorLangEl = containerEl.querySelector('#cursorLang');
const lastSavedTimeEl = containerEl.querySelector('#lastSavedTime');
const openScopesBtn = containerEl.querySelector('#openScopesBtn');
const addMarkerBtn = containerEl.querySelector('#addMarkerBtn');
console.log('🎨 Creating ACE editor instance...');
const editor = ace.edit(editorContainer);
console.log('✅ ACE editor instance created');
editor.setTheme('ace/theme/monokai');
editor.session.setMode('ace/mode/text');
editor.setOptions({
fontSize: '14px',
enableBasicAutocompletion: true,
enableLiveAutocompletion: true,
enableSnippets: true,
showPrintMargin: false,
highlightActiveLine: true,
tabSize: 2,
useSoftTabs: true,
wrap: true
});
let saveTimeout = null;
let isInitialLoad = true;
// Update cursor language indicator
function updateCursorLang() {
const lang = detectSubLanguage(editor);
if (cursorLangEl) {
cursorLangEl.textContent = lang;
}
}
// Listen for cursor position changes
editor.selection.on('changeCursor', updateCursorLang);
// Toolbar button hover effects
const toolbarBtns = containerEl.querySelectorAll('.editor-toolbar-btn');
toolbarBtns.forEach(btn => {
btn.addEventListener('mouseenter', () => {
btn.style.background = '#2a2a2a';
btn.style.borderColor = '#3a3a3a';
});
btn.addEventListener('mouseleave', () => {
btn.style.background = '#1a1a1a';
btn.style.borderColor = '#2a2a2a';
});
});
// Scopes button handler - calls external module
if (openScopesBtn) {
openScopesBtn.addEventListener('click', () => {
if (window.StorageEditorScopes && typeof StorageEditorScopes.open === 'function') {
StorageEditorScopes.open();
} else {
alert('⚠️ Scopes module not loaded');
}
});
}
// Mark button handler - calls external module
if (addMarkerBtn) {
addMarkerBtn.addEventListener('click', () => {
if (window.StorageEditorMark && typeof StorageEditorMark.addMarker === 'function') {
StorageEditorMark.addMarker(editor);
} else {
alert('⚠️ Mark module not loaded');
}
});
}
// Load active file
function loadFile() {
const activeFile = getActiveFile();
if (activeFile) {
editor.setValue(activeFile.content || '', -1);
fileNameEl.textContent = activeFile.name;
filePathEl.textContent = activeFile.path;
const mode = modeFromFileName(activeFile.name);
editor.session.setMode(mode);
modeEl.textContent = getLangNameFromMode(mode);
if (activeFile.lastModified) {
const lastMod = new Date(activeFile.lastModified);
lastSavedTimeEl.textContent = lastMod.toLocaleTimeString();
}
} else {
editor.setValue('', -1);
fileNameEl.textContent = 'No file selected';
filePathEl.textContent = '';
lastSavedTimeEl.textContent = 'Never';
editor.session.setMode('ace/mode/text');
modeEl.textContent = 'text';
}
updateStats();
updateCursorLang();
setTimeout(() => {
isInitialLoad = false;
}, 500);
}
// Update statistics
function updateStats() {
const content = editor.getValue();
const lines = editor.session.getLength();
const chars = content.length;
lineCountEl.textContent = lines;
charCountEl.textContent = chars;
}
// Auto-save function
function autoSave() {
if (isInitialLoad) return;
const content = editor.getValue();
const success = updateActiveFileContent(content);
if (success) {
statusEl.style.color = '#10b981';
statusEl.textContent = '● Saved';
lastSavedTimeEl.textContent = new Date().toLocaleTimeString();
window.dispatchEvent(new CustomEvent('activeFileUpdated', {
detail: { content }
}));
setTimeout(() => {
statusEl.textContent = '● Auto-save enabled';
}, 2000);
} else {
statusEl.style.color = '#ef4444';
statusEl.textContent = '● Save failed';
}
}
// Listen for changes
editor.session.on('change', () => {
if (isInitialLoad) return;
updateStats();
statusEl.style.color = '#f59e0b';
statusEl.textContent = '● Unsaved changes';
clearTimeout(saveTimeout);
saveTimeout = setTimeout(autoSave, 500);
});
// Listen for external file changes
window.addEventListener('activeFilesUpdated', () => {
loadFile();
});
// Initial load
loadFile();
// Focus editor and force resize
setTimeout(() => {
editor.resize(true);
editor.focus();
}, 100);
// Resize on window resize
window.addEventListener('resize', () => {
editor.resize();
});
// Store editor instance for external access
containerEl._aceEditor = editor;
window._globalEditorInstance = editor;
console.log('✅ ACE editor fully initialized');
}
// Wait for ACE before initializing
waitForAce(() => {
console.log('🚀 Storage Editor Core initializing...');
if (window.AppItems) {
window.AppItems.push({
title: 'Storage Editor',
html: createEditorHTML(),
onRender: initializeEditor
});
}
// Export API
window.StorageEditor = {
open: () => {
if (window.AppOverlay && typeof AppOverlay.open === 'function') {
AppOverlay.open([{
title: '📝 Storage Editor',
html: createEditorHTML(),
onRender: initializeEditor
}]);
}
},
getActiveFile,
loadActiveFiles,
saveActiveFiles,
detectSubLanguage,
getCommentStyleFor,
LANG_DEFS
};
console.log('✅ Storage Editor Core initialized');
});
})();