Pathl.Stona/static/js/editor.js
2026-01-30 21:41:10 +01:00

681 lines
22 KiB
JavaScript

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 = `
<div class="d-flex align-items-center">
<i class="bi ${icon} me-2"></i>
<span>${message}</span>
</div>
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
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, `<span class="text-primary fw-bold">${keyword}</span>`);
});
// 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();
}
}
};