📜
scopes_copy.js
Back
📝 Javascript ⚡ Executable Ctrl+S: Save • Ctrl+R: Run • Ctrl+F: Find
// Storage Editor - Scopes Module v2 (scopes.js) // Block-based visual editor with containers, scopes, and unmarked blocks (function() { console.log('🚀 Loading Scopes Block Editor v2...'); // Language styles const LANG_STYLES = { javascript: { color: '#f7df1e', bg: 'rgba(247, 223, 30, 0.1)', icon: '🟨', label: 'JS' }, css: { color: '#264de4', bg: 'rgba(38, 77, 228, 0.1)', icon: '🟦', label: 'CSS' }, php: { color: '#8892bf', bg: 'rgba(136, 146, 191, 0.1)', icon: '🟪', label: 'PHP' }, html: { color: '#e34c26', bg: 'rgba(227, 76, 38, 0.1)', icon: '🟧', label: 'HTML' }, python: { color: '#3776ab', bg: 'rgba(55, 118, 171, 0.1)', icon: '🐍', label: 'PY' }, text: { color: '#64748b', bg: 'rgba(100, 116, 139, 0.1)', icon: '📄', label: 'TXT' } }; const UNMARKED_STYLE = { color: '#6b7280', bg: 'rgba(55, 65, 81, 0.05)', border: '2px dashed #374151', icon: '📝', label: 'UNMARKED' }; const CONTAINER_STYLE = { color: '#8b5cf6', bg: 'rgba(139, 92, 246, 0.05)', border: '3px solid #8b5cf6', icon: '📦', headerBg: '#7c3aed' }; function getLanguageStyle(lang) { return LANG_STYLES[lang] || LANG_STYLES.text; } function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } // Detect language from context function detectLanguage(lines, startLine, endLine) { let inScript = false, inStyle = false, inPhp = false; for (let i = 0; i <= startLine; i++) { const line = lines[i]; if (/<script[^>]*>/i.test(line)) inScript = true; if (/<\/script>/i.test(line)) inScript = false; if (/<style[^>]*>/i.test(line)) inStyle = true; if (/<\/style>/i.test(line)) inStyle = false; if (/<\?php/i.test(line)) inPhp = true; if (/\?>/i.test(line)) inPhp = false; } if (inScript) return 'javascript'; if (inStyle) return 'css'; if (inPhp) return 'php'; const content = lines.slice(startLine, endLine + 1).join('\n'); if (/<\?php/i.test(content)) return 'php'; if (/<script/i.test(content)) return 'javascript'; if (/<style/i.test(content)) return 'css'; if (/<[a-z]+/i.test(content)) return 'html'; return 'text'; } // Parse scopes and containers from file content function parseScopes(content) { if (!content) return { scopes: [], containers: [] }; const lines = content.split('\n'); const scopes = []; const containers = []; const stack = []; const containerStack = []; // Patterns for containers const containerOpenPatterns = [ /\/\/\s*([a-z0-9-]+):\s*container<\s*$/, /\/\*\s*([a-z0-9-]+):\s*container<\s*\*\//, /<!--\s*([a-z0-9-]+):\s*container<\s*-->/, /#\s*([a-z0-9-]+):\s*container<\s*$/ ]; // Patterns for regular scopes const openPatterns = [ /\/\/\s*([a-z0-9-]+)<\s*$/, /\/\*\s*([a-z0-9-]+)<\s*\*\//, /<!--\s*([a-z0-9-]+)<\s*-->/, /#\s*([a-z0-9-]+)<\s*$/ ]; lines.forEach((line, idx) => { // Check for container opening for (const pattern of containerOpenPatterns) { const match = line.match(pattern); if (match) { containerStack.push({ name: match[1], startLine: idx }); break; } } // Check for container closing if (containerStack.length > 0) { const current = containerStack[containerStack.length - 1]; const closePatterns = [ new RegExp(`\\/\\/\\s*${current.name}:\\s*container>\\s*$`), new RegExp(`\\/\\*\\s*${current.name}:\\s*container>\\s*\\*\\/`), new RegExp(`<!--\\s*${current.name}:\\s*container>\\s*-->`), new RegExp(`#\\s*${current.name}:\\s*container>\\s*$`) ]; for (const pattern of closePatterns) { if (pattern.test(line)) { current.endLine = idx; containers.push(current); containerStack.pop(); break; } } } // Check for scope opening for (const pattern of openPatterns) { const match = line.match(pattern); if (match) { const scopeData = { name: match[1], startLine: idx, container: containerStack.length > 0 ? containerStack[containerStack.length - 1].name : null }; stack.push(scopeData); break; } } // Check for scope closing if (stack.length > 0) { const current = stack[stack.length - 1]; const closePatterns = [ new RegExp(`\\/\\/\\s*${current.name}>\\s*$`), new RegExp(`\\/\\*\\s*${current.name}>\\s*\\*\\/`), new RegExp(`<!--\\s*${current.name}>\\s*-->`), new RegExp(`#\\s*${current.name}>\\s*$`) ]; for (const pattern of closePatterns) { if (pattern.test(line)) { current.endLine = idx; current.lineCount = current.endLine - current.startLine + 1; current.language = detectLanguage(lines, current.startLine, current.endLine); scopes.push(current); stack.pop(); break; } } } }); return { scopes, containers }; } // Build hierarchical block structure with unmarked blocks function buildBlockStructure(content) { const lines = content.split('\n'); const parsed = parseScopes(content); const { scopes, containers } = parsed; // Create a map of all marked line ranges const markedRanges = []; // Add container ranges containers.forEach(c => { markedRanges.push({ type: 'container', start: c.startLine, end: c.endLine, data: c }); }); // Add scope ranges scopes.forEach(s => { markedRanges.push({ type: 'scope', start: s.startLine, end: s.endLine, data: s }); }); // Sort by start line markedRanges.sort((a, b) => a.start - b.start); // Build block structure const blocks = []; let currentLine = 0; markedRanges.forEach(range => { // Add unmarked block before this range if there's a gap if (currentLine < range.start) { const content = lines.slice(currentLine, range.start).join('\n'); const trimmedContent = content.trim(); // Only add unmarked block if it has actual content (not just whitespace) if (trimmedContent.length > 0) { blocks.push({ type: 'unmarked', startLine: currentLine, endLine: range.start - 1, content: content, container: null }); } } // Add the marked range if (range.type === 'container') { const container = range.data; const containerScopes = scopes.filter(s => s.container === container.name); const containerBlocks = []; let containerCurrentLine = container.startLine + 1; // Skip opening marker containerScopes.forEach(scope => { // Add unmarked block inside container before scope if (containerCurrentLine < scope.startLine) { const content = lines.slice(containerCurrentLine, scope.startLine).join('\n'); const trimmedContent = content.trim(); // Only add if it has actual content if (trimmedContent.length > 0) { containerBlocks.push({ type: 'unmarked', startLine: containerCurrentLine, endLine: scope.startLine - 1, content: content, container: container.name }); } } // Add scope block containerBlocks.push({ type: 'scope', startLine: scope.startLine, endLine: scope.endLine, content: lines.slice(scope.startLine + 1, scope.endLine).join('\n'), // Exclude markers data: scope, container: container.name }); containerCurrentLine = scope.endLine + 1; }); // Add trailing unmarked block inside container if (containerCurrentLine < container.endLine) { const content = lines.slice(containerCurrentLine, container.endLine).join('\n'); const trimmedContent = content.trim(); // Only add if it has actual content if (trimmedContent.length > 0) { containerBlocks.push({ type: 'unmarked', startLine: containerCurrentLine, endLine: container.endLine - 1, content: content, container: container.name }); } } blocks.push({ type: 'container', startLine: container.startLine, endLine: container.endLine, data: container, children: containerBlocks }); currentLine = container.endLine + 1; } else if (range.type === 'scope' && !range.data.container) { // Top-level scope (not in a container) blocks.push({ type: 'scope', startLine: range.start, endLine: range.end, content: lines.slice(range.start + 1, range.end).join('\n'), // Exclude markers data: range.data, container: null }); currentLine = range.end + 1; } }); // Add trailing unmarked block if (currentLine < lines.length) { const content = lines.slice(currentLine).join('\n'); const trimmedContent = content.trim(); // Only add if it has actual content if (trimmedContent.length > 0) { blocks.push({ type: 'unmarked', startLine: currentLine, endLine: lines.length - 1, content: content, container: null }); } } return blocks; } // Render a scope block function renderScopeBlock(block, blockId) { const style = getLanguageStyle(block.data.language); const lineRange = `Lines ${block.startLine + 1}-${block.endLine + 1}`; const lineCount = block.content.split('\n').length; const textareaHeight = Math.max(80, lineCount * 22 + 32); // ~22px per line + padding return ` <div class="block-scope" data-block-id="${blockId}" style=" display: flex; margin-bottom: 12px; border-radius: 8px; overflow: hidden; background: ${style.bg}; "> <!-- Vertical Header --> <div style=" background: ${style.color}; width: 40px; display: flex; flex-direction: column; align-items: center; padding: 12px 0; gap: 8px; flex-shrink: 0; "> <span style="font-size: 20px;">${style.icon}</span> <div style=" writing-mode: vertical-rl; transform: rotate(180deg); font-weight: 700; color: #000; font-family: monospace; font-size: 13px; letter-spacing: 1px; ">${escapeHtml(block.data.name)}</div> <div style=" background: rgba(0,0,0,0.2); padding: 4px 6px; border-radius: 4px; font-size: 10px; font-weight: 700; color: #000; ">${style.label}</div> <div style=" writing-mode: vertical-rl; transform: rotate(180deg); font-size: 9px; color: rgba(0,0,0,0.6); font-weight: 600; margin-top: auto; ">${lineRange}</div> </div> <!-- Content Area --> <textarea class="block-content" data-block-id="${blockId}" style=" flex: 1; height: ${textareaHeight}px; background: #1e1e1e; color: #e6edf3; border: none; border-left: 2px solid ${style.color}; padding: 16px; font-family: 'Consolas', 'Monaco', monospace; font-size: 13px; line-height: 1.6; resize: none; outline: none; overflow: hidden; ">${escapeHtml(block.content)}</textarea> </div> `; } // Render an unmarked block function renderUnmarkedBlock(block, blockId) { const lineRange = `Lines ${block.startLine + 1}-${block.endLine + 1}`; const lineCount = block.endLine - block.startLine + 1; const textareaHeight = Math.max(60, lineCount * 20 + 24); // ~20px per line + padding return ` <div class="block-unmarked" data-block-id="${blockId}" style=" display: flex; margin-bottom: 12px; border-radius: 8px; overflow: hidden; background: ${UNMARKED_STYLE.bg}; opacity: 0.7; "> <!-- Vertical Header --> <div style=" background: #374151; width: 40px; display: flex; flex-direction: column; align-items: center; padding: 12px 0; gap: 8px; flex-shrink: 0; "> <span style="font-size: 18px;">${UNMARKED_STYLE.icon}</span> <div style=" writing-mode: vertical-rl; transform: rotate(180deg); font-weight: 600; color: #9ca3af; font-size: 11px; letter-spacing: 0.5px; ">UNMARKED</div> ${block.container ? ` <div style=" writing-mode: vertical-rl; transform: rotate(180deg); font-size: 9px; color: #8b5cf6; font-weight: 600; ">${escapeHtml(block.container)}</div> ` : ''} <div style=" writing-mode: vertical-rl; transform: rotate(180deg); font-size: 9px; color: #6b7280; margin-top: auto; ">${lineCount}L</div> </div> <!-- Content Area --> <textarea class="block-content" data-block-id="${blockId}" style=" flex: 1; height: ${textareaHeight}px; background: #1a1a1a; color: #9ca3af; border: none; border-left: 2px dashed #374151; padding: 12px; font-family: 'Consolas', 'Monaco', monospace; font-size: 12px; line-height: 1.5; resize: none; outline: none; overflow: hidden; ">${escapeHtml(block.content)}</textarea> </div> `; } // Render a container block function renderContainerBlock(block, blockId) { const lineRange = `Lines ${block.startLine + 1}-${block.endLine + 1}`; let childrenHtml = ''; block.children.forEach((child, idx) => { const childId = `${blockId}-child-${idx}`; if (child.type === 'scope') { childrenHtml += renderScopeBlock(child, childId); } else if (child.type === 'unmarked') { childrenHtml += renderUnmarkedBlock(child, childId); } }); return ` <div class="block-container" data-block-id="${blockId}" style=" background: ${CONTAINER_STYLE.bg}; border: ${CONTAINER_STYLE.border}; border-radius: 12px; margin-bottom: 20px; overflow: hidden; "> <!-- Horizontal Container Header --> <div class="container-header" data-block-id="${blockId}" style=" background: ${CONTAINER_STYLE.headerBg}; padding: 14px 20px; display: flex; justify-content: space-between; align-items: center; cursor: pointer; user-select: none; "> <div style="font-weight: 700; color: #fff; display: flex; align-items: center; gap: 12px; font-size: 16px;"> <span class="container-toggle" style="font-size: 14px;">▼</span> <span style="font-size: 20px;">${CONTAINER_STYLE.icon}</span> <span style="font-family: monospace; text-transform: uppercase; letter-spacing: 0.5px;"> ${escapeHtml(block.data.name)} </span> <span style=" background: rgba(255,255,255,0.2); padding: 3px 10px; border-radius: 4px; font-size: 11px; font-weight: 600; ">${block.children.length} blocks</span> </div> <div style="font-size: 11px; color: rgba(255,255,255,0.7); font-weight: 600;"> ${lineRange} </div> </div> <!-- Container Body --> <div class="container-body" data-block-id="${blockId}" style=" padding: 16px; "> ${childrenHtml} </div> </div> `; } // Create main block editor HTML function createBlockEditorHTML() { // Check for dependencies if (!window.StorageEditor) { return ` <div style="padding: 40px; text-align: center; color: #ef4444;"> <h2>⚠️ Storage Editor Not Loaded</h2> <p>The core Storage Editor module must be loaded first.</p> </div> `; } const file = window.StorageEditor.getActiveFile(); if (!file) { return '<div style="padding: 40px; text-align: center; color: #64748b;">📄 No file open</div>'; } const blocks = buildBlockStructure(file.content); let html = ` <div style=" height: 100%; display: flex; flex-direction: column; background: #0a0a0a; "> <!-- Toolbar --> <div style=" background: #1a1a1a; padding: 12px 20px; border-bottom: 2px solid #2a2a2a; display: flex; justify-content: space-between; align-items: center; flex-shrink: 0; "> <div style="display: flex; align-items: center; gap: 12px;"> <h2 style="margin: 0; color: #e6edf3; font-size: 18px; font-weight: 700;"> 📦 Block Editor </h2> <span style="color: #64748b; font-size: 14px;"> ${blocks.length} top-level blocks </span> </div> <button id="saveAllBlocks" style=" background: #10b981; color: #fff; border: none; padding: 10px 20px; border-radius: 6px; cursor: pointer; font-weight: 700; font-size: 14px; text-transform: uppercase; letter-spacing: 0.5px; ">💾 Save All</button> </div> <!-- Blocks Container --> <div id="blocksContainer" style=" flex: 1; overflow-y: auto; padding: 20px; "> `; blocks.forEach((block, idx) => { const blockId = `block-${idx}`; if (block.type === 'container') { html += renderContainerBlock(block, blockId); } else if (block.type === 'scope') { html += renderScopeBlock(block, blockId); } else if (block.type === 'unmarked') { html += renderUnmarkedBlock(block, blockId); } }); html += ` </div> </div> `; return html; } // Setup interactions function setupBlockEditorInteractions(container) { // Check for dependencies if (!window.StorageEditor) { console.error('StorageEditor not available'); return; } const file = window.StorageEditor.getActiveFile(); if (!file) return; const blocks = buildBlockStructure(file.content); // Container collapse/expand container.querySelectorAll('.container-header').forEach(header => { header.addEventListener('click', () => { const blockId = header.dataset.blockId; const body = container.querySelector(`.container-body[data-block-id="${blockId}"]`); const toggle = header.querySelector('.container-toggle'); if (body.style.display === 'none') { body.style.display = 'block'; toggle.textContent = '▼'; } else { body.style.display = 'none'; toggle.textContent = '▶'; } }); }); // Save all button const saveBtn = container.querySelector('#saveAllBlocks'); if (saveBtn) { saveBtn.addEventListener('click', () => { if (!confirm('Save all changes to file?')) return; // Collect all textarea values with their block IDs const textareas = container.querySelectorAll('.block-content'); const updates = new Map(); textareas.forEach(ta => { const blockId = ta.dataset.blockId; updates.set(blockId, ta.value); }); // Reconstruct file content const lines = []; function processBlock(block, blockId) { const updatedContent = updates.get(blockId); if (block.type === 'container') { lines.push(file.content.split('\n')[block.startLine]); // Opening marker block.children.forEach((child, idx) => { const childId = `${blockId}-child-${idx}`; processBlock(child, childId); }); lines.push(file.content.split('\n')[block.endLine]); // Closing marker } else if (block.type === 'scope') { lines.push(file.content.split('\n')[block.startLine]); // Opening marker lines.push(updatedContent); lines.push(file.content.split('\n')[block.endLine]); // Closing marker } else if (block.type === 'unmarked') { lines.push(updatedContent); } } blocks.forEach((block, idx) => { processBlock(block, `block-${idx}`); }); // Save to storage const files = window.StorageEditor.loadActiveFiles(); const activeIdx = files.findIndex(f => f.active); if (activeIdx !== -1) { files[activeIdx].content = lines.join('\n'); files[activeIdx].lastModified = new Date().toISOString(); window.StorageEditor.saveActiveFiles(files); window.dispatchEvent(new Event('activeFilesUpdated')); saveBtn.textContent = '✅ SAVED'; saveBtn.style.background = '#10b981'; setTimeout(() => { saveBtn.textContent = '💾 SAVE ALL'; saveBtn.style.background = '#10b981'; }, 2000); } }); } } // Export window.StorageEditorScopes = { open: () => { if (window.AppOverlay) { AppOverlay.open([{ title: '📦 Block Editor', html: createBlockEditorHTML(), onRender: setupBlockEditorInteractions }]); } }, parseScopes, buildBlockStructure, getLanguageStyle }; console.log('✅ Scopes Block Editor v2 loaded'); })();