finish line!
This commit is contained in:
+46
-195
@@ -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
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
Executable
+3
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user