finish line!

This commit is contained in:
2025-11-26 17:25:27 +08:00
parent 833d125f64
commit 14434bc2ac
261 changed files with 4530 additions and 1014 deletions
+46 -195
View File
@@ -1,222 +1,73 @@
// assets/js/autosave.js
document.addEventListener('DOMContentLoaded', function() {
// Ждем инициализации редактора
setTimeout(() => {
initializeAutoSave();
}, 1000);
});
document.addEventListener('DOMContentLoaded', () => {
const quill = window.quillEditorInstance;
const textarea = window.quillTextarea;
if (!quill || !textarea) return;
function initializeAutoSave() {
console.log('AutoSave: Initializing...');
// Ищем активные редакторы Quill
const quillEditors = document.querySelectorAll('.ql-editor');
const textareas = document.querySelectorAll('textarea.writer-editor');
if (quillEditors.length === 0 || textareas.length === 0) {
console.log('AutoSave: No Quill editors found, retrying in 1s...');
setTimeout(initializeAutoSave, 1000);
return;
}
console.log(`AutoSave: Found ${quillEditors.length} Quill editor(s)`);
// Для каждого редактора настраиваем автосейв
quillEditors.forEach((quillEditor, index) => {
const textarea = textareas[index];
if (!textarea) return;
setupAutoSaveForEditor(quillEditor, textarea, index);
});
}
function setupAutoSaveForEditor(quillEditor, textarea, editorIndex) {
let saveTimeout;
let isSaving = false;
let lastSavedContent = textarea.value;
let changeCount = 0;
let saveTimeout;
// Получаем экземпляр Quill из контейнера
const quillContainer = quillEditor.closest('.ql-container');
const quillInstance = quillContainer ? Quill.find(quillContainer) : null;
if (!quillInstance) {
console.error(`AutoSave: Could not find Quill instance for editor ${editorIndex}`);
return;
}
console.log(`AutoSave: Setting up for editor ${editorIndex}`);
function showSaveMessage(message) {
let messageEl = document.getElementById('autosave-message');
if (!messageEl) {
messageEl = document.createElement('div');
messageEl.id = 'autosave-message';
messageEl.style.cssText = `
position: fixed;
top: 70px;
right: 10px;
padding: 8px 12px;
background: #28a745;
color: white;
border-radius: 3px;
z-index: 10000;
function showMessage(message, isError = false) {
let msgEl = document.getElementById('autosave-message');
if (!msgEl) {
msgEl = document.createElement('div');
msgEl.id = 'autosave-message';
msgEl.style.cssText = `
position: fixed;
top: 70px;
right: 10px;
padding: 8px 12px;
background: ${isError ? '#dc3545' : '#28a745'};
color: white;
border-radius: 3px;
z-index: 10000;
font-size: 0.8rem;
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
`;
document.body.appendChild(messageEl);
document.body.appendChild(msgEl);
}
messageEl.textContent = message;
messageEl.style.display = 'block';
setTimeout(() => {
messageEl.style.display = 'none';
}, 2000);
msgEl.textContent = message;
msgEl.style.background = isError ? '#dc3545' : '#28a745';
msgEl.style.display = 'block';
setTimeout(() => msgEl.style.display = 'none', 2000);
}
function showError(message) {
let messageEl = document.getElementById('autosave-message');
if (!messageEl) {
messageEl = document.createElement('div');
messageEl.id = 'autosave-message';
messageEl.style.cssText = `
position: fixed;
top: 70px;
right: 10px;
padding: 8px 12px;
background: #dc3545;
color: white;
border-radius: 3px;
z-index: 10000;
font-size: 0.8rem;
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
`;
document.body.appendChild(messageEl);
}
messageEl.textContent = message;
messageEl.style.background = '#dc3545';
messageEl.style.display = 'block';
setTimeout(() => {
messageEl.style.display = 'none';
messageEl.style.background = '#28a745';
}, 3000);
}
function autoSave() {
if (isSaving) {
console.log('AutoSave: Already saving, skipping...');
return;
}
const autoSave = () => {
const currentContent = textarea.value;
// Проверяем, изменилось ли содержимое
if (currentContent === lastSavedContent) {
console.log('AutoSave: No changes detected');
return;
}
if (currentContent === lastSavedContent) return;
changeCount++;
console.log(`AutoSave: Changes detected (${changeCount}), saving...`);
isSaving = true;
// Показываем индикатор сохранения
showSaveMessage('Сохранение...');
const formData = new FormData();
formData.append('content', currentContent);
// Добавляем title если есть
const titleInput = document.querySelector('input[name="title"]');
if (titleInput) {
formData.append('title', titleInput.value);
}
// Добавляем status если есть
const statusSelect = document.querySelector('select[name="status"]');
if (statusSelect) {
formData.append('status', statusSelect.value);
}
const form = document.getElementById('chapter-form');
const formData = new FormData(form);
formData.append('autosave', 'true');
formData.append('csrf_token', document.querySelector('input[name="csrf_token"]')?.value || '');
const currentUrl = window.location.href;
fetch(currentUrl, {
showMessage('Сохранение...');
fetch(window.location.href, {
method: 'POST',
body: formData
})
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
.then(res => {
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
})
.then(data => {
if (data.success) {
lastSavedContent = currentContent;
showSaveMessage('Автосохранено: ' + new Date().toLocaleTimeString());
console.log('AutoSave: Successfully saved');
showMessage('Автосохранено: ' + new Date().toLocaleTimeString());
} else {
throw new Error(data.error || 'Unknown error');
throw new Error(data.error || 'Ошибка сервера');
}
})
.catch(error => {
console.error('AutoSave Error:', error);
showError('Ошибка автосохранения: ' + error.message);
})
.finally(() => {
isSaving = false;
.catch(err => {
console.error(err);
showMessage('Ошибка автосохранения: ' + err.message, true);
});
}
};
// Слушаем изменения в Quill редакторе
quillInstance.on('text-change', function(delta, oldDelta, source) {
if (source === 'user') {
console.log('AutoSave: Text changed by user');
clearTimeout(saveTimeout);
saveTimeout = setTimeout(autoSave, 2000); // Сохраняем через 2 секунды после изменения
}
quill.on('text-change', () => {
clearTimeout(saveTimeout);
saveTimeout = setTimeout(autoSave, 2000);
});
// Также слушаем изменения в title и status
const titleInput = document.querySelector('input[name="title"]');
if (titleInput) {
titleInput.addEventListener('input', function() {
clearTimeout(saveTimeout);
saveTimeout = setTimeout(autoSave, 2000);
});
}
const statusSelect = document.querySelector('select[name="status"]');
if (statusSelect) {
statusSelect.addEventListener('change', function() {
clearTimeout(saveTimeout);
saveTimeout = setTimeout(autoSave, 1000);
});
}
// Предупреждение при закрытии страницы с несохраненными изменениями
window.addEventListener('beforeunload', function(e) {
if (textarea.value !== lastSavedContent && !isSaving) {
e.preventDefault();
e.returnValue = 'У вас есть несохраненные изменения. Вы уверены, что хотите уйти?';
return e.returnValue;
}
});
// Периодическое сохранение каждые 30 секунд (на всякий случай)
setInterval(() => {
if (textarea.value !== lastSavedContent && !isSaving) {
console.log('AutoSave: Periodic save triggered');
autoSave();
}
}, 30000);
console.log(`AutoSave: Successfully set up for editor ${editorIndex}`);
}
// Периодическая автосохранение
setInterval(autoSave, 30000);
});
Regular → Executable
+29 -77
View File
@@ -1,102 +1,54 @@
// assets/js/editor.js
class WriterEditor {
constructor() {
this.editors = [];
constructor(formSelector = '#chapter-form', editorContainerId = 'quill-editor', textareaId = 'content') {
this.form = document.querySelector(formSelector);
this.editorContainer = document.getElementById(editorContainerId);
this.textarea = document.getElementById(textareaId);
this.init();
}
init() {
// Инициализируем редакторы для текстовых областей с классом .writer-editor
document.querySelectorAll('textarea.writer-editor').forEach(textarea => {
this.initEditor(textarea);
});
}
if (!this.editorContainer || !this.textarea || !this.form) return;
initEditor(textarea) {
// Создаем контейнер для Quill
const editorContainer = document.createElement('div');
editorContainer.className = 'writer-editor-container';
editorContainer.style.height = '500px';
editorContainer.style.marginBottom = '20px';
// Вставляем контейнер перед textarea
textarea.parentNode.insertBefore(editorContainer, textarea);
// Скрываем оригинальный textarea
textarea.style.display = 'none';
// Настройки Quill
const quill = new Quill(editorContainer, {
this.quill = new Quill(this.editorContainer, {
theme: 'snow',
modules: {
toolbar: [
[{ 'header': [1, 2, 3, false] }],
['bold', 'italic', 'underline', 'strike'],
['blockquote', 'code-block'],
[{ 'list': 'ordered'}, { 'list': 'bullet' }],
[{ 'header': [1, 2, 3, 4, 5, 6, false] }],
['bold','italic','underline','strike'],
['blockquote','code-block'],
[{ 'list': 'ordered' }, { 'list': 'bullet' }],
[{ 'script': 'sub'}, { 'script': 'super' }],
[{ 'indent': '-1'}, { 'indent': '+1' }],
[{ 'direction': 'rtl' }],
[{ 'size': ['small', false, 'large', 'huge'] }],
[{ 'color': [] }, { 'background': [] }],
[{ 'font': [] }],
[{ 'align': [] }],
['link', 'image', 'video'],
['link','image','video'],
['clean']
],
history: {
delay: 1000,
maxStack: 100,
userOnly: true
}
history: { delay: 1000, maxStack: 100, userOnly: true }
},
placeholder: 'Начните писать вашу главу...',
formats: [
'header', 'bold', 'italic', 'underline', 'strike',
'blockquote', 'code-block', 'list', 'bullet',
'script', 'indent', 'direction', 'size',
'color', 'background', 'font', 'align',
'link', 'image', 'video'
]
placeholder: 'Введите текст главы...'
});
// Устанавливаем начальное содержимое
if (textarea.value) {
quill.root.innerHTML = textarea.value;
}
// Загружаем текст
const rawContent = this.editorContainer.dataset.content || '';
if (rawContent.trim()) this.quill.root.innerHTML = rawContent.trim();
// Обновляем textarea при изменении содержимого
quill.on('text-change', () => {
textarea.value = quill.root.innerHTML;
});
// Синхронизация с textarea
const sync = () => {
let html = this.quill.root.innerHTML;
html = html.replace(/^(<p><br><\/p>)+/, '').replace(/(<p><br><\/p>)+$/, '');
this.textarea.value = html;
};
// Сохраняем ссылку на редактор
this.editors.push({
quill: quill,
textarea: textarea
});
this.quill.on('text-change', sync);
this.form.addEventListener('submit', sync);
return quill;
}
// Метод для получения HTML содержимого
getContent(editorIndex = 0) {
if (this.editors[editorIndex]) {
return this.editors[editorIndex].quill.root.innerHTML;
}
return '';
}
// Метод для установки содержимого
setContent(content, editorIndex = 0) {
if (this.editors[editorIndex]) {
this.editors[editorIndex].quill.root.innerHTML = content;
}
// Делаем глобально доступным для автосейва
window.quillEditorInstance = this.quill;
window.quillTextarea = this.textarea;
}
}
// Инициализация редактора при загрузке страницы
document.addEventListener('DOMContentLoaded', function() {
document.addEventListener('DOMContentLoaded', () => {
window.writerEditor = new WriterEditor();
});
});
+3
View File
File diff suppressed because one or more lines are too long