continue
This commit is contained in:
+165
-41
@@ -1,28 +1,69 @@
|
||||
// assets/js/autosave.js
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const contentTextarea = document.getElementById('content');
|
||||
const titleInput = document.getElementById('title');
|
||||
const statusSelect = document.getElementById('status');
|
||||
// Ждем инициализации редактора
|
||||
setTimeout(() => {
|
||||
initializeAutoSave();
|
||||
}, 1000);
|
||||
});
|
||||
|
||||
function initializeAutoSave() {
|
||||
console.log('AutoSave: Initializing...');
|
||||
|
||||
// Проверяем, что это редактирование существующей главы
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const isEditMode = urlParams.has('id');
|
||||
// Ищем активные редакторы Quill
|
||||
const quillEditors = document.querySelectorAll('.ql-editor');
|
||||
const textareas = document.querySelectorAll('textarea.writer-editor');
|
||||
|
||||
if (!contentTextarea || !isEditMode) {
|
||||
console.log('Автосохранение отключено: создание новой главы');
|
||||
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 = contentTextarea.value;
|
||||
let lastSavedContent = textarea.value;
|
||||
let changeCount = 0;
|
||||
|
||||
// Получаем экземпляр 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: 10px; right: 10px; padding: 8px 12px; background: #333; color: white; border-radius: 3px; z-index: 1000; font-size: 0.8rem;';
|
||||
messageEl.style.cssText = `
|
||||
position: fixed;
|
||||
top: 70px;
|
||||
right: 10px;
|
||||
padding: 8px 12px;
|
||||
background: #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);
|
||||
}
|
||||
|
||||
@@ -31,68 +72,151 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
|
||||
setTimeout(() => {
|
||||
messageEl.style.display = 'none';
|
||||
}, 1500);
|
||||
}, 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) return;
|
||||
|
||||
const currentContent = contentTextarea.value;
|
||||
const currentTitle = titleInput ? titleInput.value : '';
|
||||
const currentStatus = statusSelect ? statusSelect.value : 'draft';
|
||||
|
||||
if (currentContent === lastSavedContent) return;
|
||||
if (isSaving) {
|
||||
console.log('AutoSave: Already saving, skipping...');
|
||||
return;
|
||||
}
|
||||
|
||||
const currentContent = textarea.value;
|
||||
|
||||
// Проверяем, изменилось ли содержимое
|
||||
if (currentContent === lastSavedContent) {
|
||||
console.log('AutoSave: No changes detected');
|
||||
return;
|
||||
}
|
||||
|
||||
changeCount++;
|
||||
console.log(`AutoSave: Changes detected (${changeCount}), saving...`);
|
||||
|
||||
isSaving = true;
|
||||
|
||||
|
||||
// Показываем индикатор сохранения
|
||||
showSaveMessage('Сохранение...');
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('content', currentContent);
|
||||
formData.append('title', currentTitle);
|
||||
formData.append('status', currentStatus);
|
||||
formData.append('autosave', 'true');
|
||||
formData.append('csrf_token', document.querySelector('input[name="csrf_token"]').value);
|
||||
|
||||
fetch(window.location.href, {
|
||||
// Добавляем 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);
|
||||
}
|
||||
|
||||
formData.append('autosave', 'true');
|
||||
formData.append('csrf_token', document.querySelector('input[name="csrf_token"]')?.value || '');
|
||||
|
||||
const currentUrl = window.location.href;
|
||||
|
||||
fetch(currentUrl, {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
lastSavedContent = currentContent;
|
||||
showSaveMessage('Сохранено: ' + new Date().toLocaleTimeString());
|
||||
showSaveMessage('Автосохранено: ' + new Date().toLocaleTimeString());
|
||||
console.log('AutoSave: Successfully saved');
|
||||
} else {
|
||||
throw new Error(data.error || 'Unknown error');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Ошибка автосохранения:', error);
|
||||
console.error('AutoSave Error:', error);
|
||||
showError('Ошибка автосохранения: ' + error.message);
|
||||
})
|
||||
.finally(() => {
|
||||
isSaving = false;
|
||||
});
|
||||
}
|
||||
|
||||
contentTextarea.addEventListener('input', function() {
|
||||
clearTimeout(saveTimeout);
|
||||
saveTimeout = setTimeout(autoSave, 2000);
|
||||
|
||||
// Слушаем изменения в 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 секунды после изменения
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// Также слушаем изменения в 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', autoSave);
|
||||
statusSelect.addEventListener('change', function() {
|
||||
clearTimeout(saveTimeout);
|
||||
saveTimeout = setTimeout(autoSave, 1000);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// Предупреждение при закрытии страницы с несохраненными изменениями
|
||||
window.addEventListener('beforeunload', function(e) {
|
||||
if (contentTextarea.value !== lastSavedContent) {
|
||||
if (textarea.value !== lastSavedContent && !isSaving) {
|
||||
e.preventDefault();
|
||||
e.returnValue = 'У вас есть несохраненные изменения. Вы уверены, что хотите уйти?';
|
||||
return e.returnValue;
|
||||
}
|
||||
});
|
||||
|
||||
//console.log('Автосохранение включено для редактирования главы');
|
||||
});
|
||||
|
||||
// Периодическое сохранение каждые 30 секунд (на всякий случай)
|
||||
setInterval(() => {
|
||||
if (textarea.value !== lastSavedContent && !isSaving) {
|
||||
console.log('AutoSave: Periodic save triggered');
|
||||
autoSave();
|
||||
}
|
||||
}, 30000);
|
||||
|
||||
console.log(`AutoSave: Successfully set up for editor ${editorIndex}`);
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
// assets/js/editor.js
|
||||
class WriterEditor {
|
||||
constructor() {
|
||||
this.editors = [];
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
// Инициализируем редакторы для текстовых областей с классом .writer-editor
|
||||
document.querySelectorAll('textarea.writer-editor').forEach(textarea => {
|
||||
this.initEditor(textarea);
|
||||
});
|
||||
}
|
||||
|
||||
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, {
|
||||
theme: 'snow',
|
||||
modules: {
|
||||
toolbar: [
|
||||
[{ 'header': [1, 2, 3, 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'],
|
||||
['clean']
|
||||
],
|
||||
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'
|
||||
]
|
||||
});
|
||||
|
||||
// Устанавливаем начальное содержимое
|
||||
if (textarea.value) {
|
||||
quill.root.innerHTML = textarea.value;
|
||||
}
|
||||
|
||||
// Обновляем textarea при изменении содержимого
|
||||
quill.on('text-change', () => {
|
||||
textarea.value = quill.root.innerHTML;
|
||||
});
|
||||
|
||||
// Сохраняем ссылку на редактор
|
||||
this.editors.push({
|
||||
quill: quill,
|
||||
textarea: textarea
|
||||
});
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Инициализация редактора при загрузке страницы
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
window.writerEditor = new WriterEditor();
|
||||
});
|
||||
Regular → Executable
@@ -1,575 +0,0 @@
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const contentTextarea = document.getElementById('content');
|
||||
const previewForm = document.getElementById('preview-form');
|
||||
|
||||
if (!contentTextarea) return;
|
||||
|
||||
let isFullscreen = false;
|
||||
let originalStyles = {};
|
||||
let isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
|
||||
|
||||
|
||||
function normalizeContent(text) {
|
||||
// Заменяем множественные переносы на двойные
|
||||
text = text.replace(/\n{3,}/g, '\n\n');
|
||||
// Убираем пустые строки в начале и конце
|
||||
return text.trim();
|
||||
}
|
||||
|
||||
initEditor();
|
||||
|
||||
function initEditor() {
|
||||
// Нормализуем контент при загрузке
|
||||
if (contentTextarea.value) {
|
||||
contentTextarea.value = normalizeContent(contentTextarea.value);
|
||||
}
|
||||
autoResize();
|
||||
contentTextarea.addEventListener('input', autoResize);
|
||||
contentTextarea.addEventListener('input', processDialogues);
|
||||
contentTextarea.addEventListener('keydown', handleTab);
|
||||
contentTextarea.addEventListener('input', updatePreviewContent);
|
||||
|
||||
updatePreviewContent();
|
||||
addControlButtons();
|
||||
|
||||
// На мобильных устройствах добавляем обработчик изменения ориентации
|
||||
if (isMobile) {
|
||||
window.addEventListener('orientationchange', function() {
|
||||
if (isFullscreen) {
|
||||
setTimeout(adjustForMobileKeyboard, 300);
|
||||
}
|
||||
});
|
||||
|
||||
// Обработчик для виртуальной клавиатуры
|
||||
window.addEventListener('resize', function() {
|
||||
if (isFullscreen && isMobile) {
|
||||
adjustForMobileKeyboard();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function autoResize() {
|
||||
if (isFullscreen) return;
|
||||
|
||||
contentTextarea.style.height = 'auto';
|
||||
contentTextarea.style.height = contentTextarea.scrollHeight + 'px';
|
||||
}
|
||||
|
||||
function processDialogues() {
|
||||
const lines = contentTextarea.value.split('\n');
|
||||
let changed = false;
|
||||
|
||||
const processedLines = lines.map(line => {
|
||||
if (line.trim().startsWith('- ') && line.trim().length > 2) {
|
||||
const trimmed = line.trim();
|
||||
const restOfLine = trimmed.substring(2);
|
||||
if (/^[a-zA-Zа-яА-Я]/.test(restOfLine)) {
|
||||
changed = true;
|
||||
return line.replace(trimmed, `— ${restOfLine}`);
|
||||
}
|
||||
}
|
||||
return line;
|
||||
});
|
||||
|
||||
if (changed) {
|
||||
const cursorPos = contentTextarea.selectionStart;
|
||||
contentTextarea.value = processedLines.join('\n');
|
||||
contentTextarea.setSelectionRange(cursorPos, cursorPos);
|
||||
if (!isFullscreen) autoResize();
|
||||
}
|
||||
}
|
||||
|
||||
function handleTab(e) {
|
||||
if (e.key === 'Tab') {
|
||||
e.preventDefault();
|
||||
const start = contentTextarea.selectionStart;
|
||||
const end = contentTextarea.selectionEnd;
|
||||
|
||||
contentTextarea.value = contentTextarea.value.substring(0, start) + ' ' + contentTextarea.value.substring(end);
|
||||
contentTextarea.selectionStart = contentTextarea.selectionEnd = start + 4;
|
||||
if (!isFullscreen) autoResize();
|
||||
}
|
||||
}
|
||||
|
||||
function updatePreviewContent() {
|
||||
if (previewForm) {
|
||||
document.getElementById('preview-content').value = contentTextarea.value;
|
||||
}
|
||||
}
|
||||
|
||||
function adjustForMobileKeyboard() {
|
||||
if (!isMobile || !isFullscreen) return;
|
||||
|
||||
// На мобильных устройствах уменьшаем высоту textarea, чтобы клавиатура не перекрывала контент
|
||||
const viewportHeight = window.innerHeight;
|
||||
const keyboardHeight = viewportHeight * 0.4; // Предполагаемая высота клавиатуры (40% экрана)
|
||||
const availableHeight = viewportHeight - keyboardHeight - 80; // 80px для кнопок и отступов
|
||||
|
||||
contentTextarea.style.height = availableHeight + 'px';
|
||||
contentTextarea.style.paddingBottom = '20px';
|
||||
|
||||
// Прокручиваем к курсору
|
||||
setTimeout(() => {
|
||||
const cursorPos = contentTextarea.selectionStart;
|
||||
if (cursorPos > 0) {
|
||||
scrollToCursor();
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
|
||||
function scrollToCursor() {
|
||||
const textarea = contentTextarea;
|
||||
const cursorPos = textarea.selectionStart;
|
||||
|
||||
// Создаем временный элемент для измерения позиции курсора
|
||||
const tempDiv = document.createElement('div');
|
||||
tempDiv.style.cssText = `
|
||||
position: absolute;
|
||||
top: -1000px;
|
||||
left: -1000px;
|
||||
width: ${textarea.clientWidth}px;
|
||||
padding: ${textarea.style.padding};
|
||||
font: ${getComputedStyle(textarea).font};
|
||||
line-height: ${textarea.style.lineHeight};
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
visibility: hidden;
|
||||
`;
|
||||
|
||||
const textBeforeCursor = textarea.value.substring(0, cursorPos);
|
||||
tempDiv.textContent = textBeforeCursor;
|
||||
|
||||
document.body.appendChild(tempDiv);
|
||||
const textHeight = tempDiv.offsetHeight;
|
||||
document.body.removeChild(tempDiv);
|
||||
|
||||
// Прокручиваем так, чтобы курсор был виден
|
||||
const lineHeight = parseInt(getComputedStyle(textarea).lineHeight) || 24;
|
||||
const visibleHeight = textarea.clientHeight;
|
||||
const cursorLine = Math.floor(textHeight / lineHeight);
|
||||
const visibleLines = Math.floor(visibleHeight / lineHeight);
|
||||
|
||||
const targetScroll = Math.max(0, (cursorLine - Math.floor(visibleLines / 3)) * lineHeight);
|
||||
|
||||
textarea.scrollTop = targetScroll;
|
||||
}
|
||||
|
||||
function addControlButtons() {
|
||||
const container = contentTextarea.parentElement;
|
||||
|
||||
const controlsContainer = document.createElement('div');
|
||||
controlsContainer.className = 'editor-controls';
|
||||
|
||||
const fullscreenBtn = createButton('🔲', 'Полноэкранный режим', toggleFullscreen);
|
||||
const helpBtn = createButton('❓', 'Справка по Markdown', showHelp);
|
||||
|
||||
controlsContainer.appendChild(fullscreenBtn);
|
||||
controlsContainer.appendChild(helpBtn);
|
||||
|
||||
container.insertBefore(controlsContainer, contentTextarea);
|
||||
|
||||
function toggleFullscreen() {
|
||||
if (!isFullscreen) {
|
||||
enterFullscreen();
|
||||
} else {
|
||||
exitFullscreen();
|
||||
}
|
||||
}
|
||||
|
||||
function enterFullscreen() {
|
||||
originalStyles = {
|
||||
position: contentTextarea.style.position,
|
||||
top: contentTextarea.style.top,
|
||||
left: contentTextarea.style.left,
|
||||
width: contentTextarea.style.width,
|
||||
height: contentTextarea.style.height,
|
||||
zIndex: contentTextarea.style.zIndex,
|
||||
backgroundColor: contentTextarea.style.backgroundColor,
|
||||
border: contentTextarea.style.border,
|
||||
borderRadius: contentTextarea.style.borderRadius,
|
||||
fontSize: contentTextarea.style.fontSize,
|
||||
padding: contentTextarea.style.padding,
|
||||
margin: contentTextarea.style.margin
|
||||
};
|
||||
|
||||
if (isMobile) {
|
||||
// Для мобильных - адаптивный режим с учетом клавиатуры
|
||||
const viewportHeight = window.innerHeight;
|
||||
const availableHeight = viewportHeight - 100; // Оставляем место для кнопок
|
||||
|
||||
Object.assign(contentTextarea.style, {
|
||||
position: 'fixed',
|
||||
top: '50px',
|
||||
left: '0',
|
||||
width: '100vw',
|
||||
height: availableHeight + 'px',
|
||||
zIndex: '9998',
|
||||
backgroundColor: 'white',
|
||||
border: '2px solid #007bff',
|
||||
borderRadius: '0',
|
||||
fontSize: '18px',
|
||||
padding: '15px',
|
||||
margin: '0',
|
||||
boxSizing: 'border-box',
|
||||
resize: 'none'
|
||||
});
|
||||
|
||||
// На мобильных устройствах фокусируем textarea сразу
|
||||
setTimeout(() => {
|
||||
contentTextarea.focus();
|
||||
}, 300);
|
||||
} else {
|
||||
// Для ПК - классический полноэкранный режим
|
||||
Object.assign(contentTextarea.style, {
|
||||
position: 'fixed',
|
||||
top: '5vh',
|
||||
left: '5vw',
|
||||
width: '90vw',
|
||||
height: '90vh',
|
||||
zIndex: '9998',
|
||||
backgroundColor: 'white',
|
||||
border: '2px solid #007bff',
|
||||
borderRadius: '8px',
|
||||
fontSize: '16px',
|
||||
padding: '20px',
|
||||
margin: '0',
|
||||
boxSizing: 'border-box',
|
||||
resize: 'none'
|
||||
});
|
||||
}
|
||||
|
||||
controlsContainer.style.display = 'none';
|
||||
createFullscreenControls();
|
||||
|
||||
isFullscreen = true;
|
||||
document.body.style.overflow = 'hidden';
|
||||
}
|
||||
|
||||
function exitFullscreen() {
|
||||
Object.assign(contentTextarea.style, originalStyles);
|
||||
|
||||
controlsContainer.style.display = 'flex';
|
||||
removeFullscreenControls();
|
||||
|
||||
isFullscreen = false;
|
||||
document.body.style.overflow = '';
|
||||
|
||||
autoResize();
|
||||
}
|
||||
|
||||
function createFullscreenControls() {
|
||||
const fullscreenControls = document.createElement('div');
|
||||
fullscreenControls.id = 'fullscreen-controls';
|
||||
|
||||
const exitBtn = createButton('❌', 'Выйти из полноэкранного режима', exitFullscreen);
|
||||
const helpBtnFullscreen = createButton('❓', 'Справка по Markdown', showHelp);
|
||||
|
||||
// Для мобильных увеличиваем кнопки и добавляем отступы
|
||||
const buttonSize = isMobile ? '60px' : '50px';
|
||||
const fontSize = isMobile ? '24px' : '20px';
|
||||
const topPosition = isMobile ? '10px' : '15px';
|
||||
|
||||
[exitBtn, helpBtnFullscreen].forEach(btn => {
|
||||
btn.style.cssText = `
|
||||
width: ${buttonSize};
|
||||
height: ${buttonSize};
|
||||
border-radius: 50%;
|
||||
border: 1px solid #ddd;
|
||||
background: white;
|
||||
cursor: pointer;
|
||||
font-size: ${fontSize};
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 2px 5px rgba(0,0,0,0.3);
|
||||
transition: all 0.3s ease;
|
||||
color: #333333;
|
||||
touch-action: manipulation;
|
||||
`;
|
||||
});
|
||||
|
||||
fullscreenControls.appendChild(helpBtnFullscreen);
|
||||
fullscreenControls.appendChild(exitBtn);
|
||||
|
||||
fullscreenControls.style.cssText = `
|
||||
position: fixed;
|
||||
top: ${topPosition};
|
||||
right: 10px;
|
||||
z-index: 9999;
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
`;
|
||||
|
||||
document.body.appendChild(fullscreenControls);
|
||||
|
||||
// Предотвращаем всплытие событий от кнопок к textarea
|
||||
fullscreenControls.addEventListener('touchstart', function(e) {
|
||||
e.stopPropagation();
|
||||
});
|
||||
|
||||
fullscreenControls.addEventListener('touchend', function(e) {
|
||||
e.stopPropagation();
|
||||
});
|
||||
}
|
||||
|
||||
function removeFullscreenControls() {
|
||||
const fullscreenControls = document.getElementById('fullscreen-controls');
|
||||
if (fullscreenControls) {
|
||||
fullscreenControls.remove();
|
||||
}
|
||||
}
|
||||
|
||||
// Выход по ESC
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Escape' && isFullscreen) {
|
||||
exitFullscreen();
|
||||
}
|
||||
});
|
||||
|
||||
// На мобильных устройствах добавляем обработчик для выхода по тапу вне textarea
|
||||
if (isMobile) {
|
||||
document.addEventListener('touchstart', function(e) {
|
||||
if (isFullscreen && !contentTextarea.contains(e.target) &&
|
||||
!document.getElementById('fullscreen-controls')?.contains(e.target)) {
|
||||
exitFullscreen();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Обработчик фокуса для мобильных устройств
|
||||
if (isMobile) {
|
||||
contentTextarea.addEventListener('focus', function() {
|
||||
if (isFullscreen) {
|
||||
setTimeout(adjustForMobileKeyboard, 100);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function createButton(icon, title, onClick) {
|
||||
const button = document.createElement('button');
|
||||
button.innerHTML = icon;
|
||||
button.title = title;
|
||||
button.type = 'button';
|
||||
|
||||
const buttonSize = isMobile ? '50px' : '40px';
|
||||
const fontSize = isMobile ? '20px' : '16px';
|
||||
|
||||
button.style.cssText = `
|
||||
width: ${buttonSize};
|
||||
height: ${buttonSize};
|
||||
border-radius: 50%;
|
||||
border: 1px solid #ddd;
|
||||
background: white;
|
||||
cursor: pointer;
|
||||
font-size: ${fontSize};
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
|
||||
transition: all 0.3s ease;
|
||||
color: #333333;
|
||||
touch-action: manipulation;
|
||||
`;
|
||||
|
||||
button.addEventListener('mouseenter', function() {
|
||||
this.style.transform = 'scale(1.1)';
|
||||
this.style.backgroundColor = '#f8f9fa';
|
||||
this.style.boxShadow = '0 4px 8px rgba(0,0,0,0.3)';
|
||||
});
|
||||
|
||||
button.addEventListener('mouseleave', function() {
|
||||
this.style.transform = 'scale(1)';
|
||||
this.style.backgroundColor = 'white';
|
||||
this.style.boxShadow = '0 2px 5px rgba(0,0,0,0.2)';
|
||||
});
|
||||
|
||||
button.addEventListener('click', onClick);
|
||||
|
||||
// Для мобильных устройств
|
||||
button.addEventListener('touchstart', function(e) {
|
||||
e.stopPropagation();
|
||||
this.style.transform = 'scale(1.1)';
|
||||
this.style.backgroundColor = '#f8f9fa';
|
||||
this.style.boxShadow = '0 4px 8px rgba(0,0,0,0.3)';
|
||||
});
|
||||
|
||||
button.addEventListener('touchend', function(e) {
|
||||
e.stopPropagation();
|
||||
this.style.transform = 'scale(1)';
|
||||
this.style.backgroundColor = 'white';
|
||||
this.style.boxShadow = '0 2px 5px rgba(0,0,0,0.2)';
|
||||
onClick();
|
||||
});
|
||||
|
||||
return button;
|
||||
}
|
||||
|
||||
function showHelp() {
|
||||
const helpContent = `
|
||||
<div style="font-family: system-ui, sans-serif; line-height: 1.6; color: #333;">
|
||||
<h1 style="color: #007bff; margin-top: 0; border-bottom: 2px solid #007bff; padding-bottom: 10px;">Справка по Markdown</h1>
|
||||
|
||||
<div style="margin-bottom: 20px;">
|
||||
<h2 style="color: #555;">Основное форматирование</h2>
|
||||
<div style="background: #f8f9fa; padding: 15px; border-radius: 5px; border-left: 4px solid #007bff;">
|
||||
<p><strong>Жирный текст:</strong> **текст** или __текст__</p>
|
||||
<p><em>Наклонный текст:</em> *текст* или _текст_</p>
|
||||
<p><u>Подчеркнутый текст:</u> <u>текст</u></p>
|
||||
<p><del>Зачеркнутый текст:</del> ~~текст~~</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 20px;">
|
||||
<h2 style="color: #555;">Заголовки</h2>
|
||||
<div style="background: #f8f9fa; padding: 15px; border-radius: 5px; border-left: 4px solid #28a745;">
|
||||
<h1 style="margin: 10px 0; font-size: 1.5em;">Заголовок 1 (# Заголовок)</h1>
|
||||
<h2 style="margin: 10px 0; font-size: 1.3em;">Заголовок 2 (## Заголовок)</h2>
|
||||
<h3 style="margin: 10px 0; font-size: 1.1em;">Заголовок 3 (### Заголовок)</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 20px;">
|
||||
<h2 style="color: #555;">Цитаты</h2>
|
||||
<div style="background: #f8f9fa; padding: 15px; border-radius: 5px; border-left: 4px solid #ffc107;">
|
||||
<blockquote style="margin: 0; padding-left: 15px; border-left: 3px solid #ddd; color: #666;">
|
||||
> Это цитата
|
||||
</blockquote>
|
||||
<blockquote style="margin: 10px 0 0 20px; padding-left: 15px; border-left: 3px solid #ddd; color: #666;">
|
||||
> > Вложенная цитата
|
||||
</blockquote>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 20px;">
|
||||
<h2 style="color: #555;">Диалоги</h2>
|
||||
<div style="background: #f8f9fa; padding: 15px; border-radius: 5px; border-left: 4px solid #dc3545;">
|
||||
<p><strong>Автоматическое преобразование:</strong></p>
|
||||
<p><code>- Привет!</code> → <em>— Привет!</em></p>
|
||||
<p style="font-size: 0.9em; color: #666; margin-top: 5px;">
|
||||
Дефис в начале строки автоматически заменяется на тире с пробелом
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 20px;">
|
||||
<h2 style="color: #555;">Списки</h2>
|
||||
<div style="background: #f8f9fa; padding: 15px; border-radius: 5px; border-left: 4px solid #6f42c1;">
|
||||
<p><strong>Маркированный список:</strong></p>
|
||||
<ul style="margin: 10px 0; padding-left: 20px;">
|
||||
<li>- Элемент списка</li>
|
||||
<li>- Другой элемент</li>
|
||||
</ul>
|
||||
<p><strong>Нумерованный список:</strong></p>
|
||||
<ol style="margin: 10px 0; padding-left: 20px;">
|
||||
<li>1. Первый элемент</li>
|
||||
<li>2. Второй элемент</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 20px;">
|
||||
<h2 style="color: #555;">Код</h2>
|
||||
<div style="background: #f8f9fa; padding: 15px; border-radius: 5px; border-left: 4px solid #fd7e14;">
|
||||
<p><strong>Код в строке:</strong></p>
|
||||
<p><code>\`код в строке\`</code></p>
|
||||
<p><strong>Блок кода:</strong></p>
|
||||
<pre style="background: #e9ecef; padding: 10px; border-radius: 3px; overflow-x: auto; margin: 10px 0;">
|
||||
\`\`\`
|
||||
блок кода
|
||||
многострочный
|
||||
\`\`\`</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="background: #e7f3ff; padding: 15px; border-radius: 5px; border-left: 4px solid #007bff; margin-top: 20px;">
|
||||
<p style="margin: 0; font-size: 0.9em;"><strong>💡 Подсказка:</strong> Используйте кнопку "👁️ Предпросмотр" чтобы увидеть как будет выглядеть готовый текст!</p>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const modal = document.createElement('div');
|
||||
modal.style.cssText = `
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
background: white;
|
||||
border: 2px solid #007bff;
|
||||
border-radius: 10px;
|
||||
padding: 25px;
|
||||
z-index: 10000;
|
||||
width: 90%;
|
||||
max-width: 700px;
|
||||
max-height: 85vh;
|
||||
overflow-y: auto;
|
||||
box-shadow: 0 10px 30px rgba(0,0,0,0.3);
|
||||
`;
|
||||
|
||||
const closeBtn = document.createElement('button');
|
||||
closeBtn.innerHTML = '✕';
|
||||
closeBtn.title = 'Закрыть справку';
|
||||
closeBtn.style.cssText = `
|
||||
position: absolute;
|
||||
top: 15px;
|
||||
right: 15px;
|
||||
background: #ff4444;
|
||||
color: white;
|
||||
border: none;
|
||||
font-size: 18px;
|
||||
cursor: pointer;
|
||||
width: 35px;
|
||||
height: 35px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: background 0.3s ease;
|
||||
`;
|
||||
|
||||
closeBtn.addEventListener('mouseenter', function() {
|
||||
this.style.background = '#cc0000';
|
||||
});
|
||||
|
||||
closeBtn.addEventListener('mouseleave', function() {
|
||||
this.style.background = '#ff4444';
|
||||
});
|
||||
|
||||
closeBtn.addEventListener('click', function() {
|
||||
modal.remove();
|
||||
overlay.remove();
|
||||
});
|
||||
|
||||
modal.innerHTML = helpContent;
|
||||
modal.appendChild(closeBtn);
|
||||
|
||||
const overlay = document.createElement('div');
|
||||
overlay.style.cssText = `
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background: rgba(0,0,0,0.5);
|
||||
z-index: 9999;
|
||||
`;
|
||||
|
||||
overlay.addEventListener('click', function() {
|
||||
modal.remove();
|
||||
overlay.remove();
|
||||
});
|
||||
|
||||
document.body.appendChild(overlay);
|
||||
document.body.appendChild(modal);
|
||||
|
||||
const closeHandler = function(e) {
|
||||
if (e.key === 'Escape') {
|
||||
modal.remove();
|
||||
overlay.remove();
|
||||
document.removeEventListener('keydown', closeHandler);
|
||||
}
|
||||
};
|
||||
document.addEventListener('keydown', closeHandler);
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user