📜
editor_copy.js
Back
📝 Javascript ⚡ Executable Ctrl+S: Save • Ctrl+R: Run • Ctrl+F: Find
(function () { try { console.log("[editor.js] Loading HTML editor module..."); window.AppItems = window.AppItems || []; // Store editor instance globally so close hook can access it let globalEditorInstance = null; let saveTimeout = null; // Search state let searchState = { matches: [], idx: -1, markers: [] }; // Fold selection state let lastCursorPos = null; let lastFoldIndex = -1; let cachedFolds = []; // Improved save function with proper error handling function saveToLocalStorage(editorInstance) { if (!editorInstance) return; try { const files = JSON.parse(localStorage.getItem('sftp_active_files') || '[]'); const active = files.find(f => f.active); if (active) { active.content = editorInstance.getValue(); localStorage.setItem('sftp_active_files', JSON.stringify(files)); console.log(`[editor.js] ✓ Saved ${active.name}`); return true; } } catch (err) { console.error("[editor.js] Failed to save:", err); return false; } } // Debounced save - saves 500ms after last keystroke function debouncedSave(editorInstance) { clearTimeout(saveTimeout); saveTimeout = setTimeout(() => { saveToLocalStorage(editorInstance); }, 500); } // === SMART MARKER FUNCTIONS === function detectSubLanguage(editor) { const pos = editor.getCursorPosition(); const token = editor.session.getTokenAt(pos.row, pos.column); if (!token) return "php"; const t = token.type || ""; if (t.includes("php")) return "php"; if (t.includes("js")) return "javascript"; if (t.includes("css")) return "css"; if (t.includes("tag") || t.includes("attr")) return "html"; return "php"; } function getCommentStyleFor(lang) { switch (lang) { case "html": return { open: "<!--", close: "-->" }; case "css": return { open: "/*", close: "*/" }; case "javascript": return { open: "//", close: "" }; case "php": return { open: "/*", close: "*/" }; default: return { open: "//", close: "" }; } } function wrapSelectionWithSmartMarker(markerName) { if (!globalEditorInstance) return; const selected = globalEditorInstance.getSelectedText(); if (!selected) { if (typeof showToast === 'function') { showToast('⚠️ Select some text first!', 'error'); } return; } const range = globalEditorInstance.getSelectionRange(); const subLang = detectSubLanguage(globalEditorInstance); const { open, close } = getCommentStyleFor(subLang); // Build wrapped text with proper comment syntax let wrapped; if (close) { // Block comment style (HTML, CSS, PHP) - each marker closes on same line wrapped = `${open}${markerName}<${close}\n${selected}\n${open}${markerName}>${close}`; } else { // Line comment style (JavaScript, PHP with //) wrapped = `${open}${markerName}<\n${selected}\n${open}${markerName}>`; } // Temporarily enable editing to insert marker const wasReadOnly = globalEditorInstance.getReadOnly(); globalEditorInstance.setReadOnly(false); globalEditorInstance.session.replace(range, wrapped); // Restore read-only state globalEditorInstance.setReadOnly(wasReadOnly); if (typeof showToast === 'function') { showToast(`✅ Wrapped with marker: ${markerName}`, 'success'); } console.log(`[editor.js] Wrapped selection with marker "${markerName}" using ${subLang} syntax`); } const section = { title: "HTML Editor", html: ` <div class="editor-section"> <div class="editor-toolbar" style=" display: flex; gap: 8px; padding: 8px 12px; background: #1e1e1e; border-bottom: 1px solid #333; align-items: center; "> <button id="indexBtn" title="Show document index" 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; " >📑 Index</button> <button id="editSelectionBtn" title="Edit selection in overlay" 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; " >✏️ Edit</button> <input type="text" id="editorSearchInput" placeholder="Find in file... (Ctrl+F)" style=" flex: 1; padding: 6px 10px; background: #2d2d2d; border: 1px solid #444; border-radius: 4px; color: #e0e0e0; font-size: 13px; font-family: 'Segoe UI', sans-serif; " /> <button id="searchPrevBtn" title="Previous match (Shift+Enter)" style=" padding: 6px 12px; background: #3d3d3d; border: 1px solid #555; border-radius: 4px; color: #e0e0e0; cursor: pointer; font-size: 16px; " >↑</button> <button id="searchNextBtn" title="Next match (Enter)" style=" padding: 6px 12px; background: #3d3d3d; border: 1px solid #555; border-radius: 4px; color: #e0e0e0; cursor: pointer; font-size: 16px; " >↓</button> <span id="matchCounter" style=" color: #888; font-size: 13px; font-family: 'Segoe UI', sans-serif; min-width: 60px; "></span> </div> <div class="ace-editor" id="ace-editor-placeholder"></div> </div> <!-- Multi-purpose Overlay --> <div id="multiOverlay" style=" display: none; position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.7); z-index: 999998; align-items: center; justify-content: center; "> <div style=" background: #2d2d2d; border: 1px solid #555; border-radius: 8px; width: 90%; max-width: 600px; max-height: 80vh; display: flex; flex-direction: column; box-shadow: 0 8px 32px rgba(0,0,0,0.5); "> <div style=" padding: 16px 20px; border-bottom: 1px solid #555; display: flex; justify-content: space-between; align-items: center; "> <h3 id="overlayTitle" style="margin: 0; color: #e0e0e0; font-size: 18px; font-family: 'Segoe UI', sans-serif;">Overlay</h3> <button id="closeOverlayBtn" style=" background: none; border: none; color: #888; font-size: 24px; cursor: pointer; padding: 0; line-height: 1; ">&times;</button> </div> <div id="overlayContent" style=" padding: 12px; overflow-y: auto; flex: 1; "></div> <div id="overlayFooter" style=" padding: 12px 20px; border-top: 1px solid #555; display: none; gap: 8px; "></div> </div> </div> `, onRender(el) { console.log("[editor.js] onRender fired"); const container = el.querySelector('.ace-editor'); if (!container) return console.warn("[editor.js] No .ace-editor found"); container.style.minHeight = "calc(70vh - 50px)"; container.style.display = "block"; function loadAce(cb) { if (window.ace) return cb(); const s = document.createElement('script'); s.src = "https://cdnjs.cloudflare.com/ajax/libs/ace/1.32.3/ace.js"; s.onload = cb; document.head.appendChild(s); } function fitToOverlayBody() { const body = container.closest('.app-dialog')?.querySelector('.app-dialog__body'); if (!body) return; const bodyRect = body.getBoundingClientRect(); const topInBody = container.getBoundingClientRect().top - bodyRect.top; const targetH = Math.max(200, Math.floor(bodyRect.height - topInBody - 6)); container.style.height = targetH + "px"; } // === OVERLAY MANAGEMENT === function showOverlay(title, content, footer = null) { const overlay = el.querySelector('#multiOverlay'); const titleEl = el.querySelector('#overlayTitle'); const contentEl = el.querySelector('#overlayContent'); const footerEl = el.querySelector('#overlayFooter'); if (!overlay || !titleEl || !contentEl || !footerEl) return; titleEl.textContent = title; contentEl.innerHTML = content; if (footer) { footerEl.innerHTML = footer; footerEl.style.display = 'flex'; } else { footerEl.style.display = 'none'; } overlay.style.display = 'flex'; } function hideOverlay() { const overlay = el.querySelector('#multiOverlay'); if (overlay) overlay.style.display = 'none'; // Call cleanup from overlay editor module if (window.OverlayEditor) { window.OverlayEditor.cleanup(); } } // === UTILITY FUNCTIONS === function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } // === SEARCH FUNCTIONS === function clearMarkers() { if (!globalEditorInstance) return; const session = globalEditorInstance.getSession(); searchState.markers.forEach(id => { try { session.removeMarker(id); } catch (e) {} }); searchState.markers = []; } function markMatches() { if (!globalEditorInstance) return; clearMarkers(); const session = globalEditorInstance.getSession(); const Range = ace.require('ace/range').Range; searchState.matches.forEach((m, i) => { const r = new Range(m.r, m.s, m.r, m.e); const cls = i === searchState.idx ? 'ace_selected-word' : 'ace_selection'; const markerId = session.addMarker(r, cls, 'text'); searchState.markers.push(markerId); }); } function searchInEditor(query) { if (!globalEditorInstance || !query) { clearMarkers(); searchState = { matches: [], idx: -1, markers: [] }; updateMatchCounter(); return; } const session = globalEditorInstance.getSession(); const lines = session.getDocument().getAllLines(); const regex = new RegExp(query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'gi'); searchState.matches = []; searchState.idx = -1; clearMarkers(); lines.forEach((line, r) => { let m; regex.lastIndex = 0; while ((m = regex.exec(line))) { searchState.matches.push({ r, s: m.index, e: m.index + m[0].length }); } }); if (searchState.matches.length) { searchState.idx = 0; gotoMatch(); } updateMatchCounter(); } function gotoMatch() { if (searchState.idx < 0 || !searchState.matches.length || !globalEditorInstance) return; const m = searchState.matches[searchState.idx]; const Range = ace.require('ace/range').Range; const r = new Range(m.r, m.s, m.r, m.e); globalEditorInstance.selection.setRange(r, false); globalEditorInstance.scrollToLine(m.r, true, true, () => {}); markMatches(); updateMatchCounter(); } function nextMatch() { if (!searchState.matches.length) return; searchState.idx = (searchState.idx + 1) % searchState.matches.length; gotoMatch(); } function prevMatch() { if (!searchState.matches.length) return; searchState.idx = (searchState.idx - 1 + searchState.matches.length) % searchState.matches.length; gotoMatch(); } function updateMatchCounter() { const counter = el.querySelector('#matchCounter'); if (counter) { if (searchState.matches.length > 0) { counter.textContent = `${searchState.idx + 1} / ${searchState.matches.length}`; } else { counter.textContent = ''; } } } // === FOLD/SCOPE SELECTION FUNCTIONS === function getAllFoldsForRowTokenAware(targetRow) { if (!globalEditorInstance) return []; const session = globalEditorInstance.getSession(); const lineCount = session.getLength(); const stack = []; const allPairs = []; const isCodeToken = (type) => !/comment|string|regex/i.test(type); for (let row = 0; row < lineCount; row++) { const tokens = session.getTokens(row); let col = 0; for (const tok of tokens) { const { type, value } = tok; if (isCodeToken(type)) { for (let i = 0; i < value.length; i++) { const ch = value[i]; if (ch === '{') stack.push({ row, col: col + i }); else if (ch === '}') { const open = stack.pop(); if (open) { allPairs.push({ startRow: open.row, startCol: open.col, endRow: row, endCol: col + i, }); } } } } col += value.length; } } const cursor = globalEditorInstance.getCursorPosition(); const containsCursor = (p) => { if (cursor.row < p.startRow || cursor.row > p.endRow) return false; if (cursor.row === p.startRow && cursor.column <= p.startCol) return false; if (cursor.row === p.endRow && cursor.column >= p.endCol) return false; return true; }; const filtered = allPairs.filter(containsCursor); filtered.sort((a, b) => (a.endRow - a.startRow) - (b.endRow - b.startRow)); return filtered.map((p) => ({ start: p.startRow, end: p.endRow })); } function selectFold() { if (!globalEditorInstance) return; const pos = globalEditorInstance.getCursorPosition(); const session = globalEditorInstance.getSession(); const R = ace.require('ace/range').Range; if (!lastCursorPos || lastCursorPos.row !== pos.row) { cachedFolds = getAllFoldsForRowTokenAware(pos.row); lastFoldIndex = -1; lastCursorPos = { row: pos.row, column: pos.column }; } if (lastFoldIndex === -1) { const line = session.getLine(pos.row); const range = new R(pos.row, 0, pos.row, line.length); globalEditorInstance.selection.setRange(range, false); globalEditorInstance.focus(); lastFoldIndex = 0; return; } if (lastFoldIndex < cachedFolds.length) { const fold = cachedFolds[lastFoldIndex]; const range = new R(fold.start, 0, fold.end, session.getLine(fold.end).length); globalEditorInstance.selection.setRange(range, false); globalEditorInstance.scrollToLine(fold.start, true, true, () => {}); globalEditorInstance.focus(); lastFoldIndex++; if (lastFoldIndex >= cachedFolds.length) lastFoldIndex = -1; } } loadAce(() => { console.log("[editor.js] Ace script loaded"); requestAnimationFrame(() => { globalEditorInstance = ace.edit(container); globalEditorInstance.setTheme("ace/theme/monokai"); globalEditorInstance.session.setMode("ace/mode/html"); let fileContent = ''; let fileName = 'Untitled'; let detectedMode = 'html'; try { const files = JSON.parse(localStorage.getItem('sftp_active_files') || '[]'); const active = files.find(f => f.active); if (active && typeof active.content === 'string') { fileContent = active.content; fileName = active.name; console.log(`[editor.js] Loaded content for ${active.name}`); } } catch (err) { console.warn("[editor.js] Failed to load saved file content:", err); } if (fileName !== 'Untitled') { const ext = fileName.split('.').pop().toLowerCase(); const modeMap = { php: 'php', html: 'html', htm: 'html', js: 'javascript', css: 'css', json: 'json', py: 'python', md: 'markdown', txt: 'text' }; detectedMode = modeMap[ext] || 'html'; globalEditorInstance.session.setMode(`ace/mode/${detectedMode}`); } globalEditorInstance.setValue( fileContent.trim() !== '' ? fileContent : `<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <title>Document</title> </head> <body> <h1>Hello!</h1> </body> </html>`, -1 ); globalEditorInstance.setOptions({ fontSize: "14px", wrap: true, showPrintMargin: false, useWorker: false, showFoldWidgets: true, foldStyle: 'markbegin', enableAutoIndent: true, readOnly: true, highlightActiveLine: false, highlightGutterLine: false }); globalEditorInstance.commands.removeCommand('toggleFoldWidget'); globalEditorInstance.commands.removeCommand('toggleParentFoldWidget'); globalEditorInstance.getSession().addFold = () => false; globalEditorInstance.on('guttermousedown', function (e) { const target = e.domEvent.target; if (target.classList.contains('ace_fold-widget')) { const row = e.getDocumentPosition().row; const range = globalEditorInstance.getSession().getFoldWidgetRange(row); e.stop(); e.stopPropagation(); e.domEvent.stopPropagation(); e.domEvent.preventDefault(); if (range) { const Range = ace.require('ace/range').Range; const extended = new Range(range.start.row, 0, range.end.row, globalEditorInstance.getSession().getLine(range.end.row).length); globalEditorInstance.selection.setRange(extended, false); globalEditorInstance.scrollToLine(range.start.row, true, true, () => {}); globalEditorInstance.focus(); } return true; } }); globalEditorInstance.getSession().on('change', () => { cachedFolds = []; lastCursorPos = null; lastFoldIndex = -1; debouncedSave(globalEditorInstance); }); globalEditorInstance.on("blur", () => { clearTimeout(saveTimeout); saveToLocalStorage(globalEditorInstance); }); // === INITIALIZE INDEX MODULE === if (window.EditorIndex) { window.EditorIndex.init({ getGlobalEditor: () => globalEditorInstance, getEl: () => el, showOverlay: showOverlay, hideOverlay: hideOverlay, escapeHtml: escapeHtml, showToast: typeof showToast === 'function' ? showToast : null }); console.log("[editor.js] Editor index initialized"); } else { console.warn("[editor.js] EditorIndex module not loaded"); } // === INITIALIZE OVERLAY EDITOR MODULE === if (window.OverlayEditor) { window.OverlayEditor.init({ getGlobalEditor: () => globalEditorInstance, getEl: () => el, showOverlay: showOverlay, hideOverlay: hideOverlay, generateDocumentIndex: window.EditorIndex ? window.EditorIndex.generateDocumentIndex : () => [], findMarkerEnd: window.EditorIndex ? window.EditorIndex.findMarkerEnd : () => 0, escapeHtml: escapeHtml, showToast: typeof showToast === 'function' ? showToast : null }); console.log("[editor.js] Overlay editor initialized"); } else { console.warn("[editor.js] OverlayEditor module not loaded"); } // === BUTTON EVENT LISTENERS === const searchInput = el.querySelector('#editorSearchInput'); const prevBtn = el.querySelector('#searchPrevBtn'); const nextBtn = el.querySelector('#searchNextBtn'); const indexBtn = el.querySelector('#indexBtn'); const editSelectionBtn = el.querySelector('#editSelectionBtn'); const closeOverlayBtn = el.querySelector('#closeOverlayBtn'); const multiOverlay = el.querySelector('#multiOverlay'); if (indexBtn) { indexBtn.addEventListener('click', () => { if (window.EditorIndex) { window.EditorIndex.showIndexOverlay(); } else { console.error("[editor.js] EditorIndex module not available"); if (typeof showToast === 'function') { showToast('⚠️ Index not available', 'error'); } } }); } if (editSelectionBtn) { editSelectionBtn.addEventListener('click', () => { if (window.OverlayEditor) { window.OverlayEditor.showEditSelectionOverlay(); } else { console.error("[editor.js] OverlayEditor module not available"); if (typeof showToast === 'function') { showToast('⚠️ Edit overlay not available', 'error'); } } }); } if (closeOverlayBtn) { closeOverlayBtn.addEventListener('click', hideOverlay); } if (multiOverlay) { multiOverlay.addEventListener('click', (e) => { if (e.target === multiOverlay) { hideOverlay(); } }); } // === SEARCH EVENT LISTENERS === if (searchInput) { searchInput.addEventListener('input', (e) => searchInEditor(e.target.value)); searchInput.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); e.shiftKey ? prevMatch() : nextMatch(); } else if (e.key === 'Escape') { searchInput.value = ''; searchInEditor(''); globalEditorInstance.focus(); } }); } if (prevBtn) prevBtn.addEventListener('click', () => { prevMatch(); globalEditorInstance.focus(); }); if (nextBtn) nextBtn.addEventListener('click', () => { nextMatch(); globalEditorInstance.focus(); }); // === KEYBOARD SHORTCUTS === globalEditorInstance.commands.addCommand({ name: 'focusSearch', bindKey: {win: 'Ctrl-F', mac: 'Command-F'}, exec: () => { searchInput?.focus(); searchInput?.select(); } }); globalEditorInstance.commands.addCommand({ name: 'showIndex', bindKey: {win: 'Ctrl-I', mac: 'Command-I'}, exec: () => { if (window.EditorIndex) { window.EditorIndex.showIndexOverlay(); } } }); globalEditorInstance.commands.addCommand({ name: 'selectScopeUp', bindKey: {win: 'Alt-Up', mac: 'Alt-Up'}, exec: selectFold }); globalEditorInstance.commands.addCommand({ name: 'selectScopeDown', bindKey: {win: 'Alt-Down', mac: 'Alt-Down'}, exec: selectFold }); fitToOverlayBody(); globalEditorInstance.resize(true); window.addEventListener("resize", () => { fitToOverlayBody(); globalEditorInstance.resize(true); }); }); }); } }; window.AppItems.push(section); if (!window.AppOverlayMenuItems) window.AppOverlayMenuItems = []; window.AppOverlayMenuItems.push({ label: "Toggle Edit Mode", action: () => { if (!globalEditorInstance) return; const isReadOnly = globalEditorInstance.getReadOnly(); globalEditorInstance.setReadOnly(!isReadOnly); globalEditorInstance.setOptions({ highlightActiveLine: !isReadOnly, highlightGutterLine: !isReadOnly }); if (typeof showToast === 'function') { showToast(isReadOnly ? '✏️ Editor now editable' : '🔒 Editor now read-only', 'success'); } } }); window.AppOverlayMenuItems.push({ label: "Add Marker", action: () => { if (!globalEditorInstance) return; const selected = globalEditorInstance.getSelectedText(); if (!selected) { if (typeof showToast === 'function') showToast('⚠️ Select some text first!', 'error'); return; } const markerName = prompt("Enter marker name:"); if (markerName && markerName.trim()) wrapSelectionWithSmartMarker(markerName.trim()); } }); window.AppOverlayMenuItems.push({ label: "Language", submenu: [ { label: "HTML", action: () => { if (globalEditorInstance) { globalEditorInstance.session.setMode('ace/mode/html'); if (typeof showToast === 'function') showToast('✅ Switched to HTML', 'success'); }}}, { label: "PHP", action: () => { if (globalEditorInstance) { globalEditorInstance.session.setMode('ace/mode/php'); if (typeof showToast === 'function') showToast('✅ Switched to PHP', 'success'); }}}, { label: "JavaScript", action: () => { if (globalEditorInstance) { globalEditorInstance.session.setMode('ace/mode/javascript'); if (typeof showToast === 'function') showToast('✅ Switched to JavaScript', 'success'); }}}, { label: "CSS", action: () => { if (globalEditorInstance) { globalEditorInstance.session.setMode('ace/mode/css'); if (typeof showToast === 'function') showToast('✅ Switched to CSS', 'success'); }}}, { label: "JSON", action: () => { if (globalEditorInstance) { globalEditorInstance.session.setMode('ace/mode/json'); if (typeof showToast === 'function') showToast('✅ Switched to JSON', 'success'); }}}, { label: "Markdown", action: () => { if (globalEditorInstance) { globalEditorInstance.session.setMode('ace/mode/markdown'); if (typeof showToast === 'function') showToast('✅ Switched to Markdown', 'success'); }}}, { label: "Python", action: () => { if (globalEditorInstance) { globalEditorInstance.session.setMode('ace/mode/python'); if (typeof showToast === 'function') showToast('✅ Switched to Python', 'success'); }}}, { label: "Plain Text", action: () => { if (globalEditorInstance) { globalEditorInstance.session.setMode('ace/mode/text'); if (typeof showToast === 'function') showToast('✅ Switched to Plain Text', 'success'); }}} ] }); if (window.AppOverlay && typeof window.AppOverlay.close === "function") { const originalClose = window.AppOverlay.close; window.AppOverlay.close = function(...args) { clearTimeout(saveTimeout); if (globalEditorInstance) { const saved = saveToLocalStorage(globalEditorInstance); if (saved && typeof showToast === "function") { const files = JSON.parse(localStorage.getItem('sftp_active_files') || '[]'); const active = files.find(f => f.active); if (active) showToast(`💾 Saved ${active.name}`, "success"); } } return originalClose.apply(this, args); }; } } catch (err) { console.error("[editor.js] Fatal error:", err); } })();