📜
files.js
Back
📝 Javascript ⚡ Executable Ctrl+S: Save • Ctrl+R: Run • Ctrl+F: Find
// files.js - File Management and Display (function() { console.log("[files] Loading File Management module..."); // --- Utility Functions --- function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } // --- Local Storage Functions --- function getFilesFromLocalStorage() { try { return JSON.parse(localStorage.getItem('sftp_active_files') || '[]'); } catch { return []; } } function saveFilesToLocalStorage(files) { try { localStorage.setItem('sftp_active_files', JSON.stringify(files)); } catch (err) { console.error('[files] Failed to save files:', err); } } // --- File State Management --- function setFileState(fileName, state) { const files = getFilesFromLocalStorage(); const file = files.find(f => f.name === fileName); if (!file) return; if (state === 'active') { files.forEach(f => { if (f.name === fileName) { f.active = true; f.read = false; // Enable AI mode when a file becomes active if (window.FilesManager && typeof window.FilesManager.setAIMode === "function") { window.FilesManager.setAIMode(true); } } else { f.active = false; } }); } else if (state === 'read') { file.read = true; file.active = false; } else if (state === 'inactive') { file.read = false; file.active = false; } saveFilesToLocalStorage(files); // Trigger update event for blocks.js to update parsed JSON window.dispatchEvent(new Event('activeFilesUpdated')); } // --- Version Management --- function restoreVersion(fileName, versionIndex) { const files = getFilesFromLocalStorage(); const file = files.find(f => f.name === fileName); if (!file || !file.versions || !file.versions[versionIndex]) return; // Restore the selected version to current content file.content = file.versions[versionIndex].content; saveFilesToLocalStorage(files); // Trigger update event window.dispatchEvent(new Event('activeFilesUpdated')); } function deleteVersion(fileName, versionIndex) { const files = getFilesFromLocalStorage(); const file = files.find(f => f.name === fileName); if (!file || !file.versions || !file.versions[versionIndex]) return; // Remove the version file.versions.splice(versionIndex, 1); // Relabel remaining versions file.versions.forEach((version, idx) => { version.label = `v${idx + 1}`; }); saveFilesToLocalStorage(files); // Trigger update event window.dispatchEvent(new Event('activeFilesUpdated')); } // --- New File Dialog --- async function showNewFileDialog(container, onFileChange) { // Fetch templates from server via templates.php let templates = []; try { const response = await fetch('templates.php?action=list'); const data = await response.json(); if (data.success && Array.isArray(data.templates)) { templates = data.templates.map(name => ({ name: name, path: name })); console.log('[files] Found templates:', templates); } else { console.error('[files] Failed to load templates:', data.error || 'Unknown error'); } } catch (error) { console.error('[files] Failed to load templates:', error); } // Create dialog overlay const overlay = document.createElement('div'); overlay.style.cssText = ` position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0, 0, 0, 0.8); display: flex; align-items: center; justify-content: center; z-index: 2147483647; `; const dialog = document.createElement('div'); dialog.style.cssText = ` background: #1a1a1a; border: 2px solid #3a3a3a; border-radius: 12px; padding: 24px; max-width: 500px; width: 90%; max-height: 80vh; overflow-y: auto; `; dialog.innerHTML = ` <h2 style="margin: 0 0 20px 0; color: #e6edf3; font-size: 20px; font-weight: 700;"> 📄 Create New File </h2> <div style="margin-bottom: 16px;"> <label style="display: block; color: #9ca3af; font-size: 13px; font-weight: 600; margin-bottom: 8px;"> File Name </label> <input id="newFileName" type="text" placeholder="my-file.php" style=" width: 100%; padding: 10px 12px; background: #0a0a0a; border: 1px solid #3a3a3a; border-radius: 6px; color: #e0e0e0; font-size: 14px; font-family: monospace; outline: none; " /> </div> <div style="margin-bottom: 20px;"> <label style="display: block; color: #9ca3af; font-size: 13px; font-weight: 600; margin-bottom: 8px;"> Start from Template </label> <div id="templateList" style=" max-height: 300px; overflow-y: auto; border: 1px solid #3a3a3a; border-radius: 6px; background: #0a0a0a; "> <div class="template-option" data-template="" style=" padding: 12px; border-bottom: 1px solid #2a2a2a; cursor: pointer; transition: background 0.2s; "> <div style="font-weight: 600; color: #e6edf3;">📝 Empty File</div> <div style="font-size: 12px; color: #666; margin-top: 4px;">Start with a blank file</div> </div> ${templates.length === 0 ? ` <div style="padding: 12px; color: #666; font-size: 13px; text-align: center;"> No templates found in /templates folder </div> ` : ''} </div> </div> <div style="display: flex; gap: 12px;"> <button id="createFileBtn" style=" flex: 1; padding: 12px; background: #16a34a; border: 1px solid #15803d; border-radius: 6px; color: #fff; cursor: pointer; font-size: 14px; font-weight: 700; transition: all 0.2s; ">Create File</button> <button id="cancelFileBtn" style=" flex: 1; padding: 12px; background: #374151; border: 1px solid #4b5563; border-radius: 6px; color: #e0e0e0; cursor: pointer; font-size: 14px; font-weight: 700; transition: all 0.2s; ">Cancel</button> </div> `; overlay.appendChild(dialog); document.body.appendChild(overlay); const fileNameInput = dialog.querySelector('#newFileName'); const templateList = dialog.querySelector('#templateList'); const createBtn = dialog.querySelector('#createFileBtn'); const cancelBtn = dialog.querySelector('#cancelFileBtn'); let selectedTemplate = ''; // Add template options templates.forEach(template => { const option = document.createElement('div'); option.className = 'template-option'; option.dataset.template = template.path; option.style.cssText = ` padding: 12px; border-bottom: 1px solid #2a2a2a; cursor: pointer; transition: background 0.2s; `; option.innerHTML = ` <div style="font-weight: 600; color: #e6edf3;">📄 ${escapeHtml(template.name)}</div> <div style="font-size: 11px; color: #666; margin-top: 4px; font-family: monospace;"> ${escapeHtml(template.path)} </div> `; templateList.appendChild(option); }); // Template selection templateList.addEventListener('click', (e) => { const option = e.target.closest('.template-option'); if (!option) return; templateList.querySelectorAll('.template-option').forEach(opt => { opt.style.background = '#0a0a0a'; opt.style.borderLeftWidth = '0'; }); option.style.background = '#1a1a1a'; option.style.borderLeft = '3px solid #16a34a'; selectedTemplate = option.dataset.template; }); // Hover effects for templates templateList.addEventListener('mouseover', (e) => { const option = e.target.closest('.template-option'); if (option && option.style.background !== 'rgb(26, 26, 26)') { option.style.background = '#151515'; } }); templateList.addEventListener('mouseout', (e) => { const option = e.target.closest('.template-option'); if (option && option.style.background !== 'rgb(26, 26, 26)') { option.style.background = '#0a0a0a'; } }); // Create file handler const createFile = async () => { const fileName = fileNameInput.value.trim(); if (!fileName) { alert('Please enter a file name'); return; } const files = getFilesFromLocalStorage(); if (files.find(f => f.name === fileName)) { alert('A file with this name already exists'); return; } let content = ''; // Load template content if selected if (selectedTemplate) { try { const response = await fetch('templates.php?action=read&file=' + encodeURIComponent(selectedTemplate)); const data = await response.json(); if (data.success) { content = data.content; console.log('[files] Loaded template content from:', selectedTemplate); } else { console.error('[files] Failed to load template:', data.error); alert('Failed to load template. Starting with empty file.'); } } catch (error) { console.error('[files] Failed to load template:', error); alert('Failed to load template. Starting with empty file.'); } } // Add new file files.push({ name: fileName, path: fileName, content: content, active: false, read: false, versions: [] }); saveFilesToLocalStorage(files); window.dispatchEvent(new Event('activeFilesUpdated')); // Close dialog document.body.removeChild(overlay); // Refresh file list renderFilesList(container, onFileChange); if (onFileChange) onFileChange(); }; createBtn.addEventListener('click', createFile); cancelBtn.addEventListener('click', () => { document.body.removeChild(overlay); }); // Enter to create fileNameInput.addEventListener('keydown', (e) => { if (e.key === 'Enter') { createFile(); } }); // Close on overlay click overlay.addEventListener('click', (e) => { if (e.target === overlay) { document.body.removeChild(overlay); } }); // Focus input fileNameInput.focus(); } // --- Files List Rendering --- function renderFilesList(container, onFileChange) { const files = getFilesFromLocalStorage(); container.innerHTML = ''; if (files.length === 0) { const emptyMsg = document.createElement('div'); emptyMsg.style.cssText = ` color: #666; text-align: center; padding: 40px; font-size: 14px; `; emptyMsg.textContent = '📁 No files open - open files from Storage Editor'; container.appendChild(emptyMsg); return; } // Header with New File button const header = document.createElement('div'); header.style.cssText = ` margin-bottom: 20px; padding-bottom: 12px; border-bottom: 2px solid #2a2a2a; display: flex; justify-content: space-between; align-items: center; `; const headerLeft = document.createElement('div'); headerLeft.innerHTML = ` <h2 style="margin: 0; color: #e6edf3; font-size: 18px; font-weight: 700;"> 📁 All Files (${files.length}) </h2> <p style="margin: 8px 0 0 0; color: #64748b; font-size: 13px;"> Click file name to set state | Click versions to view history </p> `; const newFileBtn = document.createElement('button'); newFileBtn.style.cssText = ` padding: 10px 20px; background: #16a34a; border: 1px solid #15803d; border-radius: 6px; color: #fff; cursor: pointer; font-size: 13px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.5px; transition: all 0.2s; `; newFileBtn.textContent = '+ New File'; newFileBtn.addEventListener('click', async () => { await showNewFileDialog(container, onFileChange); }); newFileBtn.addEventListener('mouseenter', () => { newFileBtn.style.background = '#15803d'; }); newFileBtn.addEventListener('mouseleave', () => { newFileBtn.style.background = '#16a34a'; }); header.appendChild(headerLeft); header.appendChild(newFileBtn); container.appendChild(header); // Files grid files.forEach(file => { const fileCard = document.createElement('div'); fileCard.style.cssText = ` background: #1a1a1a; border: 2px solid #2a2a2a; border-radius: 8px; padding: 16px; margin-bottom: 12px; transition: all 0.2s; `; // Style based on state if (file.active) { fileCard.style.borderColor = '#16a34a'; fileCard.style.background = 'rgba(22, 163, 74, 0.1)'; } else if (file.read) { fileCard.style.borderColor = '#3b82f6'; fileCard.style.background = 'rgba(59, 130, 246, 0.1)'; } const statusBadge = file.active ? '🟢 ACTIVE' : file.read ? '🔵 READ' : '⚪ INACTIVE'; const statusColor = file.active ? '#16a34a' : file.read ? '#3b82f6' : '#666'; const versionCount = file.versions ? file.versions.length : 0; const fileHeader = document.createElement('div'); fileHeader.style.cssText = ` display: flex; justify-content: space-between; align-items: start; margin-bottom: 8px; cursor: pointer; `; fileHeader.innerHTML = ` <div style=" font-weight: 700; color: #e6edf3; font-size: 15px; font-family: monospace; ">${escapeHtml(file.name)}</div> <div style=" color: ${statusColor}; font-size: 11px; font-weight: 700; padding: 4px 8px; background: rgba(0,0,0,0.3); border-radius: 4px; ">${statusBadge}</div> `; // Click file name to cycle states fileHeader.addEventListener('click', () => { if (!file.active && !file.read) { setFileState(file.name, 'read'); } else if (file.read) { setFileState(file.name, 'active'); } else if (file.active) { setFileState(file.name, 'inactive'); } renderFilesList(container, onFileChange); if (onFileChange) onFileChange(); }); fileCard.appendChild(fileHeader); // Path const pathDiv = document.createElement('div'); pathDiv.style.cssText = ` color: #64748b; font-size: 12px; margin-bottom: 8px; `; pathDiv.textContent = file.path || 'No path'; fileCard.appendChild(pathDiv); // Preview const previewDiv = document.createElement('div'); previewDiv.style.cssText = ` color: #888; font-size: 11px; font-family: monospace; background: #0a0a0a; padding: 8px; border-radius: 4px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin-bottom: 8px; `; previewDiv.textContent = file.content ? file.content.substring(0, 80) + '...' : '(empty)'; fileCard.appendChild(previewDiv); // Parse Scope button (requires blocks.js) if (window.BlocksManager) { const parseScopeBtn = document.createElement('button'); parseScopeBtn.style.cssText = ` width: 100%; padding: 10px; background: #1e3a8a; border: 1px solid #1e40af; border-radius: 6px; color: #fff; cursor: pointer; font-size: 12px; font-weight: 700; margin-bottom: 8px; transition: all 0.2s; `; parseScopeBtn.textContent = '📋 View Document Structure'; parseScopeBtn.addEventListener('click', () => { window.BlocksManager.showParsedJSON(file.name, file.content); }); parseScopeBtn.addEventListener('mouseenter', () => { parseScopeBtn.style.background = '#1e40af'; }); parseScopeBtn.addEventListener('mouseleave', () => { parseScopeBtn.style.background = '#1e3a8a'; }); fileCard.appendChild(parseScopeBtn); } // Versions section if (versionCount > 0) { const versionsHeader = document.createElement('div'); versionsHeader.style.cssText = ` display: flex; justify-content: space-between; align-items: center; padding: 8px 0; border-top: 1px solid #2a2a2a; margin-top: 8px; cursor: pointer; user-select: none; `; versionsHeader.innerHTML = ` <div style="color: #3b82f6; font-size: 12px; font-weight: 600;"> <span class="version-toggle">▶</span> 💾 ${versionCount} Version${versionCount > 1 ? 's' : ''} </div> `; const versionsBody = document.createElement('div'); versionsBody.style.cssText = ` max-height: 0; overflow: hidden; transition: max-height 0.3s ease; margin-top: 8px; `; // Render versions (newest first) const reversedVersions = [...file.versions].reverse(); reversedVersions.forEach((version, idx) => { const realIndex = file.versions.length - 1 - idx; const date = new Date(version.timestamp); const isCurrent = file.content === version.content; const versionItem = document.createElement('div'); versionItem.style.cssText = ` display: flex; align-items: stretch; margin-bottom: 6px; gap: 4px; `; const versionBtn = document.createElement('button'); versionBtn.style.cssText = ` flex: 1; text-align: left; padding: 8px 12px; background: ${isCurrent ? '#1e3a5f' : '#0a0a0a'}; border: 1px solid ${isCurrent ? '#3b82f6' : '#2a2a2a'}; border-radius: 4px; color: #e0e0e0; cursor: ${isCurrent ? 'default' : 'pointer'}; font-size: 12px; transition: all 0.2s; `; versionBtn.innerHTML = ` <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 4px;"> <span style="font-weight: 700; color: ${isCurrent ? '#3b82f6' : '#fff'};"> ${version.label} ${isCurrent ? '(Current)' : ''} </span> <span style="color: #666; font-size: 10px;">${date.toLocaleString()}</span> </div> <div style="color: #888; font-size: 11px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;"> ${version.content.substring(0, 60)}${version.content.length > 60 ? '...' : ''} </div> `; if (!isCurrent) { versionBtn.addEventListener('click', () => { if (confirm(`Restore ${version.label}? This will replace the current content.`)) { restoreVersion(file.name, realIndex); renderFilesList(container, onFileChange); if (onFileChange) onFileChange(); } }); versionBtn.addEventListener('mouseenter', () => { versionBtn.style.background = '#1a1a1a'; versionBtn.style.borderColor = '#3a3a3a'; }); versionBtn.addEventListener('mouseleave', () => { versionBtn.style.background = '#0a0a0a'; versionBtn.style.borderColor = '#2a2a2a'; }); } // Delete button const deleteBtn = document.createElement('button'); deleteBtn.style.cssText = ` width: 36px; padding: 8px; background: #1a1a1a; border: 1px solid #2a2a2a; border-radius: 4px; color: #ef4444; cursor: pointer; font-size: 16px; transition: all 0.2s; display: flex; align-items: center; justify-content: center; `; deleteBtn.innerHTML = '🗑️'; deleteBtn.title = `Delete ${version.label}`; deleteBtn.addEventListener('click', (e) => { e.stopPropagation(); if (confirm(`Delete ${version.label}? This cannot be undone.`)) { deleteVersion(file.name, realIndex); renderFilesList(container, onFileChange); if (onFileChange) onFileChange(); } }); deleteBtn.addEventListener('mouseenter', () => { deleteBtn.style.background = '#ef4444'; deleteBtn.style.borderColor = '#dc2626'; deleteBtn.style.color = '#fff'; }); deleteBtn.addEventListener('mouseleave', () => { deleteBtn.style.background = '#1a1a1a'; deleteBtn.style.borderColor = '#2a2a2a'; deleteBtn.style.color = '#ef4444'; }); versionItem.appendChild(versionBtn); versionItem.appendChild(deleteBtn); versionsBody.appendChild(versionItem); }); // Toggle versions let versionsExpanded = false; versionsHeader.addEventListener('click', () => { versionsExpanded = !versionsExpanded; const toggle = versionsHeader.querySelector('.version-toggle'); if (versionsExpanded) { versionsBody.style.maxHeight = versionsBody.scrollHeight + 'px'; toggle.textContent = '▼'; } else { versionsBody.style.maxHeight = '0'; toggle.textContent = '▶'; } }); fileCard.appendChild(versionsHeader); fileCard.appendChild(versionsBody); } container.appendChild(fileCard); }); } // --- Expose API --- window.FilesManager = { getFiles: getFilesFromLocalStorage, saveFiles: saveFilesToLocalStorage, setFileState: setFileState, render: renderFilesList, restoreVersion: restoreVersion, deleteVersion: deleteVersion, // NEW — get real active file getActiveFile: function () { const files = getFilesFromLocalStorage(); return files.find(f => f.active) || null; }, // NEW — global AI mode flag aiMode: false, // NEW — toggle AI mode setAIMode: function (state) { this.aiMode = state; window.dispatchEvent(new Event('aiModeChanged')); console.log('[files] AI mode:', state ? 'ON' : 'OFF'); } }; console.log('[files] File Management module loaded'); })();