class QLEditor { constructor(username, project, selectedFile) { this.username = username; this.project = project; this.selectedFile = selectedFile; this.editor = null; this.autoSaveTimer = null; this.isDirty = false; this.consoleHistory = []; this.consoleHistoryIndex = -1; this.init(); } init() { this.bindEvents(); this.setupAutoSave(); this.updateStatus(); this.initConsole(); console.log(`QL Editor initialized for ${this.username}/${this.project}/${this.selectedFile}`); } bindEvents() { // Zmiana pliku document.querySelectorAll('.file-item').forEach(item => { item.addEventListener('click', (e) => { const filename = e.currentTarget.dataset.filename; if (filename !== this.selectedFile) { this.switchFile(filename); } }); }); // Zapisz przycisk document.getElementById('saveBtn')?.addEventListener('click', () => this.saveFile()); // Nowy plik document.getElementById('newFileBtn')?.addEventListener('click', () => this.showNewFileModal()); // Uruchom kod document.getElementById('runBtn')?.addEventListener('click', () => this.runCode()); // Formatuj kod document.getElementById('formatBtn')?.addEventListener('click', () => this.formatCode()); // Śledź zmiany w edytorze const textarea = document.getElementById('codeEditor'); if (textarea) { textarea.addEventListener('input', () => { this.isDirty = true; this.updateStatus(); }); // Skróty klawiaturowe w edytorze textarea.addEventListener('keydown', (e) => { // Ctrl+S - zapisz if (e.ctrlKey && e.key === 's') { e.preventDefault(); this.saveFile(); } // Ctrl+Enter - uruchom if (e.ctrlKey && e.key === 'Enter') { e.preventDefault(); this.runCode(); } // Ctrl+F - formatuj if (e.ctrlKey && e.key === 'f') { e.preventDefault(); this.formatCode(); } }); } // Globalne skróty klawiaturowe document.addEventListener('keydown', (e) => { if (e.ctrlKey && e.key === 's') { e.preventDefault(); this.saveFile(); } }); // Ostrzeżenie przed opuszczeniem strony window.addEventListener('beforeunload', (e) => { if (this.isDirty) { e.preventDefault(); e.returnValue = 'Masz niezapisane zmiany. Czy na pewno chcesz opuścić stronę?'; return e.returnValue; } }); } initConsole() { const consoleInput = document.getElementById('consoleInput'); const consoleSend = document.getElementById('consoleSend'); const clearConsoleBtn = document.getElementById('clearConsoleBtn'); if (!consoleInput) return; // Enter w konsoli consoleInput.addEventListener('keypress', (e) => { if (e.key === 'Enter') { this.executeConsoleCommand(); } // Strzałki dla historii if (e.key === 'ArrowUp') { e.preventDefault(); this.navigateConsoleHistory(-1); } if (e.key === 'ArrowDown') { e.preventDefault(); this.navigateConsoleHistory(1); } }); // Przycisk wysyłania if (consoleSend) { consoleSend.addEventListener('click', () => this.executeConsoleCommand()); } // Przycisk czyszczenia konsoli if (clearConsoleBtn) { clearConsoleBtn.addEventListener('click', () => this.clearConsole()); } // Auto-focus na konsoli setTimeout(() => { consoleInput?.focus(); }, 100); } switchFile(filename) { if (this.isDirty) { if (!confirm('Masz niezapisane zmiany. Czy na pewno chcesz zmienić plik?')) { return; } } window.location.href = `/editor/${this.username}/${this.project}?file=${filename}`; } async saveFile() { const content = document.getElementById('codeEditor').value; const filename = this.selectedFile; if (!content.trim()) { this.showNotification('Plik jest pusty!', 'warning'); return; } try { this.showNotification('Zapisywanie...', 'info'); const response = await fetch(`/editor/${this.username}/${this.project}/save`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ filename: filename, content: content }) }); const data = await response.json(); if (response.ok) { this.showNotification('✓ Plik zapisany pomyślnie', 'success'); this.isDirty = false; this.updateStatus(); this.addConsoleMessage('✓ Plik zapisany', 'success'); // Zaktualizuj czas modyfikacji w liście plików const fileItem = document.querySelector(`[data-filename="${filename}"] .file-modified`); if (fileItem) { const now = new Date(); fileItem.textContent = now.toLocaleTimeString(); } } else { throw new Error(data.error || 'Błąd zapisu'); } } catch (error) { this.showNotification(`✗ Błąd: ${error.message}`, 'error'); this.addConsoleMessage(`✗ Błąd zapisu: ${error.message}`, 'error'); } } async createFile(filename) { if (!filename) { this.showNotification('Podaj nazwę pliku', 'warning'); return; } try { const response = await fetch(`/editor/${this.username}/${this.project}/create`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ filename: filename }) }); const data = await response.json(); if (response.ok) { this.showNotification('✓ Plik utworzony pomyślnie', 'success'); this.addConsoleMessage(`✓ Utworzono plik: ${filename}`, 'success'); // Przełącz na nowy plik setTimeout(() => { window.location.href = `/editor/${this.username}/${this.project}?file=${filename}`; }, 500); } else { throw new Error(data.error || 'Błąd tworzenia pliku'); } } catch (error) { this.showNotification(`✗ Błąd: ${error.message}`, 'error'); } } async deleteFile() { if (!this.selectedFile || this.selectedFile === 'README.md') { this.showNotification('Nie można usunąć tego pliku', 'warning'); return; } if (!confirm(`Czy na pewno chcesz usunąć plik "${this.selectedFile}"?`)) { return; } try { const response = await fetch(`/editor/${this.username}/${this.project}/delete`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ filename: this.selectedFile }) }); const data = await response.json(); if (response.ok) { this.showNotification('✓ Plik usunięty pomyślnie', 'success'); this.addConsoleMessage(`✓ Usunięto plik: ${this.selectedFile}`, 'success'); // Wróć do głównego pliku setTimeout(() => { window.location.href = `/editor/${this.username}/${this.project}`; }, 500); } else { throw new Error(data.error || 'Błąd usuwania pliku'); } } catch (error) { this.showNotification(`✗ Błąd: ${error.message}`, 'error'); } } showNewFileModal() { const filename = prompt('Podaj nazwę nowego pliku (np. program.ql):', 'nowy.ql'); if (filename) { this.createFile(filename); } } setupAutoSave() { // Auto-zapis co 30 sekund jeśli są zmiany this.autoSaveTimer = setInterval(() => { if (this.isDirty) { this.autoSave(); } }, 30000); } async autoSave() { const content = document.getElementById('codeEditor').value; const filename = this.selectedFile; try { const response = await fetch(`/editor/${this.username}/${this.project}/save`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ filename: filename, content: content }) }); if (response.ok) { this.addConsoleMessage('✓ Auto-zapisano plik', 'success'); } } catch (error) { console.error('Auto-save error:', error); } } updateStatus() { const status = document.getElementById('editorStatus'); if (status) { const editor = document.getElementById('codeEditor'); if (!editor) return; const lines = editor.value.split('\n').length; const chars = editor.value.length; const dirtyIndicator = this.isDirty ? ' ●' : ''; status.textContent = `${this.selectedFile} | Linie: ${lines} | Znaki: ${chars}${dirtyIndicator}`; } } async runCode() { const code = document.getElementById('codeEditor').value; if (!code.trim()) { return; } // WYCZYŚĆ KONSOLĘ PRZED NOWYM WYKONANIEM this.clearConsoleForRun(); try { const response = await fetch(`/editor/${this.username}/${this.project}/execute`, { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ code: code }) }); const data = await response.json(); if (data.status === 'success') { // WYŚWIETL TYLKO OUTPUT Z PRINT() if (data.output && data.output.length > 0) { data.output.forEach(line => { if (line && line.trim()) { this.addConsoleMessage(line, 'response'); } }); } } else { // Tylko błędy if (data.output && data.output.length > 0) { this.addConsoleMessage(data.output[0], 'error'); } } } catch (error) { this.addConsoleMessage(`Błąd: ${error.message}`, 'error'); } } // Dodaj tę nową funkcję do klasy: clearConsoleForRun() { const consoleOutput = document.getElementById('consoleOutput'); if (!consoleOutput) return; // WYCZYŚĆ KONSOLĘ, ale NIE dodawaj komunikatów "QL Console" consoleOutput.innerHTML = ''; // Możesz dodać tylko nazwę polecenia jak w terminalu: // this.addConsoleMessage(`$ ql ${this.selectedFile}`, 'command'); } formatCode() { const editor = document.getElementById('codeEditor'); if (!editor) return; let code = editor.value; // Proste formatowanie kodu QL // 1. Zamień taby na 4 spacje code = code.replace(/\t/g, ' '); // 2. Usuń nadmiarowe puste linie code = code.replace(/\n\s*\n\s*\n/g, '\n\n'); // 3. Usuń białe znaki na końcu linii code = code.replace(/\s+$/gm, ''); // 4. Dodaj wcięcia dla bloków let indent = 0; const lines = code.split('\n'); const formattedLines = []; for (let line of lines) { line = line.trim(); // Zmniejsz wcięcie przed zamknięciem bloku if (line.includes('}')) { indent = Math.max(0, indent - 1); } // Dodaj wcięcie const indentedLine = ' '.repeat(indent) + line; formattedLines.push(indentedLine); // Zwiększ wcięcie po otwarciu bloku if (line.includes('{')) { indent++; } } editor.value = formattedLines.join('\n'); this.isDirty = true; this.updateStatus(); this.addConsoleMessage('✓ Kod sformatowany', 'success'); this.showNotification('Kod sformatowany!', 'info'); } executeConsoleCommand() { const consoleInput = document.getElementById('consoleInput'); if (!consoleInput) return; const command = consoleInput.value.trim(); if (!command) return; // Dodaj do historii this.consoleHistory.push(command); this.consoleHistoryIndex = -1; // Wyświetl komendę this.addConsoleLine(`> ${command}`, 'command'); // Wykonaj komendę const result = this.processConsoleCommand(command); if (result) { this.addConsoleLine(result); } // Wyczyść input consoleInput.value = ''; consoleInput.focus(); } processConsoleCommand(command) { const parts = command.trim().split(' '); const cmd = parts[0].toLowerCase(); const args = parts.slice(1); const commands = { 'help': () => { return `Dostępne komendy: help - wyświetla tę pomoc ls - lista plików w projekcie clear - czyści konsolę run - uruchamia aktualny plik save - zapisuje aktualny plik format - formatuje kod status - pokazuje status projektu time - pokazuje aktualny czas echo [text] - wyświetla tekst`; }, 'ls': () => { const fileList = document.getElementById('fileList'); if (!fileList) return 'Brak listy plików'; const files = Array.from(fileList.querySelectorAll('.list-group-item')) .map(item => item.dataset.filename) .filter(Boolean); if (files.length === 0) return 'Brak plików w projekcie'; return files.map(f => `📄 ${f}`).join('\n'); }, 'clear': () => { this.clearConsole(); return 'Konsola wyczyszczona'; }, 'run': () => { this.runCode(); return 'Uruchamianie kodu...'; }, 'save': () => { this.saveFile(); return 'Zapisywanie pliku...'; }, 'format': () => { this.formatCode(); return 'Formatowanie kodu...'; }, 'status': () => { const editor = document.getElementById('codeEditor'); if (!editor) return 'Brak edytora'; const lines = editor.value.split('\n').length; const chars = editor.value.length; return `Projekt: ${this.project} Plik: ${this.selectedFile} Linie: ${lines} Znaki: ${chars} Zmiany: ${this.isDirty ? 'TAK' : 'NIE'}`; }, 'time': () => new Date().toLocaleString(), 'echo': () => args.join(' ') }; if (commands[cmd]) { try { const result = commands[cmd](); return typeof result === 'string' ? result : ''; } catch (error) { return `Błąd: ${error.message}`; } } return `Nieznana komenda: ${cmd}\nWpisz 'help' aby zobaczyć dostępne komendy`; } navigateConsoleHistory(direction) { const consoleInput = document.getElementById('consoleInput'); if (!consoleInput || this.consoleHistory.length === 0) return; if (direction === -1) { // Strzałka w górę if (this.consoleHistoryIndex < this.consoleHistory.length - 1) { if (this.consoleHistoryIndex === -1) { this.currentCommand = consoleInput.value; } this.consoleHistoryIndex++; } } else { // Strzałka w dół if (this.consoleHistoryIndex > 0) { this.consoleHistoryIndex--; } else if (this.consoleHistoryIndex === 0) { this.consoleHistoryIndex = -1; consoleInput.value = this.currentCommand || ''; return; } } if (this.consoleHistoryIndex >= 0) { consoleInput.value = this.consoleHistory[this.consoleHistory.length - 1 - this.consoleHistoryIndex]; } // Przesuń kursor na koniec consoleInput.selectionStart = consoleInput.selectionEnd = consoleInput.value.length; } addConsoleLine(text, type = 'response') { const consoleOutput = document.getElementById('consoleOutput'); if (!consoleOutput) return; const line = document.createElement('div'); line.className = `console-${type}`; line.textContent = text; consoleOutput.appendChild(line); consoleOutput.scrollTop = consoleOutput.scrollHeight; } addConsoleMessage(message, type = 'info') { this.addConsoleLine(message, type); } clearConsole() { const consoleOutput = document.getElementById('consoleOutput'); if (!consoleOutput) return; consoleOutput.innerHTML = ''; this.addConsoleMessage('=== QL Console ===', 'success'); this.addConsoleMessage('Wpisz \'help\' aby uzyskać pomoc', 'info'); } showNotification(message, type = 'info') { // Utwórz powiadomienie Bootstrap const alert = document.createElement('div'); alert.className = `alert alert-${type} alert-dismissible fade show position-fixed`; alert.style.cssText = ` position: fixed; top: 20px; right: 20px; z-index: 9999; min-width: 300px; max-width: 400px; box-shadow: 0 4px 6px rgba(0,0,0,0.1); `; // Ikona w zależności od typu let icon = 'bi-info-circle'; if (type === 'success') icon = 'bi-check-circle'; if (type === 'warning') icon = 'bi-exclamation-triangle'; if (type === 'danger' || type === 'error') icon = 'bi-x-circle'; alert.innerHTML = `
${message}
`; document.body.appendChild(alert); // Auto-ukrywanie setTimeout(() => { if (alert.parentNode) { // Użyj Bootstrapowego zamykania const bsAlert = new bootstrap.Alert(alert); bsAlert.close(); } }, 3000); } // Czyszczenie interwału przy zamknięciu destroy() { if (this.autoSaveTimer) { clearInterval(this.autoSaveTimer); } } } // Inicjalizacja edytora po załadowaniu strony document.addEventListener('DOMContentLoaded', () => { const username = document.body.dataset.username; const project = document.body.dataset.project; const selectedFile = document.body.dataset.selectedFile; if (username && project && selectedFile) { window.qleditor = new QLEditor(username, project, selectedFile); } // Podświetlanie składni dla QL (podstawowe) const editor = document.getElementById('codeEditor'); if (editor) { // Zapisz oryginalny tekst dla podświetlania let originalText = editor.value; editor.addEventListener('input', function() { // Możesz dodać podświetlanie składni tutaj // Przykład prostego podświetlania: const keywords = ['func', 'use', 'if', 'else', 'while', 'for', 'return', 'print', 'true', 'false', 'null']; let text = this.value; // To tylko przykład - w prawdziwym edytorze użyj biblioteki jak CodeMirror lub Monaco keywords.forEach(keyword => { const regex = new RegExp(`\\b${keyword}\\b`, 'g'); text = text.replace(regex, `${keyword}`); }); // Zapisz oryginalny tekst originalText = this.value; }); } // Czyszczenie przy opuszczeniu strony window.addEventListener('unload', () => { if (window.qleditor) { window.qleditor.destroy(); } }); }); // Globalne funkcje dostępne z konsoli (opcjonalnie) window.Console = { run: function() { if (window.qleditor) { window.qleditor.runCode(); } }, save: function() { if (window.qleditor) { window.qleditor.saveFile(); } }, format: function() { if (window.qleditor) { window.qleditor.formatCode(); } }, clear: function() { if (window.qleditor) { window.qleditor.clearConsole(); } } };