diff --git a/assets/css/style.css b/assets/css/style.css index ae91af5..f512f33 100755 --- a/assets/css/style.css +++ b/assets/css/style.css @@ -18,4 +18,28 @@ width: 16px; height: 16px; } +} + +/* editor styles */ +@media (max-width: 768px) { + .writer-editor-container .ql-container { + font-size: 16px !important; /* Предотвращает масштабирование в iOS */ + } + + .writer-editor-container .ql-toolbar { + padding: 8px !important; + } + + .writer-editor-container .ql-toolbar .ql-formats { + margin-right: 8px !important; + } +} + +/* Стили для полноэкранного режима */ +.ql-toolbar.ql-snow { + transition: all 0.3s ease; +} + +.writer-editor-container.fullscreen { + transition: all 0.3s ease; } \ No newline at end of file diff --git a/assets/js/editor.js b/assets/js/editor.js index fbaa67c..00d5e76 100755 --- a/assets/js/editor.js +++ b/assets/js/editor.js @@ -1,3 +1,4 @@ +// editor.js class DialogueFormatter { constructor(quill) { this.quill = quill; @@ -19,7 +20,6 @@ class DialogueFormatter { getLineStart(index) { let lineStart = index; - // Безопасный поиск начала строки (защита от отрицательных индексов) while (lineStart > 0) { const prevChar = this.quill.getText(lineStart - 1, 1); if (prevChar === '\n') break; @@ -30,65 +30,49 @@ class DialogueFormatter { checkForDialogue() { const selection = this.quill.getSelection(); - // Защита от некорректных позиций курсора if (!selection || selection.index < 2) return; const lineStart = this.getLineStart(selection.index); const charsFromStart = selection.index - lineStart; - // Работаем ТОЛЬКО когда: - // 1. Ровно 2 символа от начала строки до курсора - // 2. Эти символы - "- " if (charsFromStart !== 2) return; const text = this.quill.getText(lineStart, 2); if (text !== '- ') return; - // Атомарная операция замены this.quill.updateContents([ { retain: lineStart }, - { delete: 2 }, // Удаляем "- " - { insert: '— ' } // Вставляем "— " + { delete: 2 }, + { insert: '— ' } ], 'user'); - // Явно устанавливаем курсор ПОСЛЕ пробела this.quill.setSelection(lineStart + 2, 0, 'silent'); } - // Простой метод для форматирования диалогов formatSelectionAsDialogue() { const range = this.quill.getSelection(); if (!range || range.length === 0) return; const selectedText = this.quill.getText(range.index, range.length); - - // Простая замена всех "- " на "— " в выделенном тексте const formattedText = selectedText.replace(/(^|\n)- /g, '$1— '); if (formattedText !== selectedText) { this.quill.deleteText(range.index, range.length, 'user'); this.quill.insertText(range.index, formattedText, 'user'); - - // Восстанавливаем выделение this.quill.setSelection(range.index, formattedText.length, 'silent'); } } - // Простой метод для отмены форматирования диалогов unformatSelectionAsDialogue() { const range = this.quill.getSelection(); if (!range || range.length === 0) return; const selectedText = this.quill.getText(range.index, range.length); - - // Простая замена всех "— " на "- " в выделенном тексте const unformattedText = selectedText.replace(/(^|\n)— /g, '$1- '); if (unformattedText !== selectedText) { this.quill.deleteText(range.index, range.length, 'user'); this.quill.insertText(range.index, unformattedText, 'user'); - - // Восстанавливаем выделение this.quill.setSelection(range.index, unformattedText.length, 'silent'); } } @@ -99,6 +83,8 @@ class WriterEditor { this.form = document.querySelector(formSelector); this.editorContainer = document.getElementById(editorContainerId); this.textarea = document.getElementById(textareaId); + this.isFullscreen = false; + this.originalStyles = {}; this.init(); } @@ -123,6 +109,7 @@ class WriterEditor { [{ 'font': [] }], ['link','image','video'], ['clean'], + ['fullscreen'] // Добавляем кнопку полноэкранного режима ], handlers: { 'dialogue': () => { @@ -134,6 +121,9 @@ class WriterEditor { if (this.dialogueFormatter) { this.dialogueFormatter.unformatSelectionAsDialogue(); } + }, + 'fullscreen': () => { + this.toggleFullscreen(); } } }, @@ -156,20 +146,14 @@ class WriterEditor { placeholder: 'Введите текст главы...' }); - // Добавляем кастомные кнопки в тулбар после инициализации Quill this.addCustomButtonsToToolbar(); - - // Инициализируем автоформатер диалогов this.dialogueFormatter = new DialogueFormatter(this.quill); - // Загружаем текст const rawContent = this.editorContainer.dataset.content || ''; if (rawContent.trim()) this.quill.root.innerHTML = rawContent.trim(); - // Обрабатываем уже существующие диалоги при загрузке setTimeout(() => this.formatExistingDialogues(), 100); - // Синхронизация с textarea const sync = () => { let html = this.quill.root.innerHTML; html = html.replace(/^(


<\/p>)+/, '').replace(/(


<\/p>)+$/, ''); @@ -179,21 +163,32 @@ class WriterEditor { this.quill.on('text-change', sync); this.form.addEventListener('submit', sync); - // Делаем глобально доступным для автосейва + // Обработчик изменения ориентации для мобильных устройств + window.addEventListener('orientationchange', () => { + if (this.isFullscreen) { + setTimeout(() => this.adjustFullscreenHeight(), 300); + } + }); + + // Обработчик изменения размера окна + window.addEventListener('resize', () => { + if (this.isFullscreen) { + this.adjustFullscreenHeight(); + } + }); + window.quillEditorInstance = this.quill; window.quillTextarea = this.textarea; } addCustomButtonsToToolbar() { - // Находим тулбар Quill const toolbar = this.quill.container.previousSibling; if (!toolbar) return; - // Находим кнопки по классам Quill const dialogueBtn = toolbar.querySelector('.ql-dialogue'); const undoDialogueBtn = toolbar.querySelector('.ql-undodialogue'); + const fullscreenBtn = toolbar.querySelector('.ql-fullscreen'); - // Заменяем содержимое кнопок на наши символы if (dialogueBtn) { dialogueBtn.innerHTML = '—'; dialogueBtn.title = 'Форматировать диалоги (—)'; @@ -205,6 +200,137 @@ class WriterEditor { undoDialogueBtn.title = 'Убрать форматирование диалогов (-)'; undoDialogueBtn.style.fontWeight = 'bold'; } + + if (fullscreenBtn) { + fullscreenBtn.innerHTML = '⛶'; + fullscreenBtn.title = 'Полноэкранный режим'; + } + } + + toggleFullscreen() { + if (this.isFullscreen) { + this.exitFullscreen(); + } else { + this.enterFullscreen(); + } + } + + enterFullscreen() { + this.isFullscreen = true; + + // Сохраняем оригинальные стили + this.originalStyles = { + container: this.editorContainer.style.cssText, + body: document.body.style.cssText, + toolbar: this.quill.container.previousSibling.style.cssText + }; + + // Получаем тулбар + const toolbar = this.quill.container.previousSibling; + + // Применяем стили для полноэкранного режима + document.body.style.overflow = 'hidden'; + + this.editorContainer.style.position = 'fixed'; + this.editorContainer.style.top = '0'; + this.editorContainer.style.left = '0'; + this.editorContainer.style.width = '100vw'; + this.editorContainer.style.zIndex = '9999'; + this.editorContainer.style.background = 'white'; + this.editorContainer.style.paddingTop = toolbar.offsetHeight + 'px'; + + // Стили для тулбара + toolbar.style.position = 'fixed'; + toolbar.style.top = '0'; + toolbar.style.left = '0'; + toolbar.style.width = '100%'; + toolbar.style.zIndex = '10000'; + toolbar.style.background = 'white'; + toolbar.style.borderBottom = '1px solid #ccc'; + + this.adjustFullscreenHeight(); + + // Добавляем обработчик ESC + this.escapeHandler = (e) => { + if (e.key === 'Escape') { + this.exitFullscreen(); + } + }; + document.addEventListener('keydown', this.escapeHandler); + + // Добавляем кнопку выхода из полноэкранного режима + this.addFullscreenExitButton(); + } + + exitFullscreen() { + this.isFullscreen = false; + + // Восстанавливаем оригинальные стили + this.editorContainer.style.cssText = this.originalStyles.container || ''; + document.body.style.cssText = this.originalStyles.body || ''; + + const toolbar = this.quill.container.previousSibling; + if (toolbar) { + toolbar.style.cssText = this.originalStyles.toolbar || ''; + } + + // Удаляем обработчик ESC + if (this.escapeHandler) { + document.removeEventListener('keydown', this.escapeHandler); + } + + // Удаляем кнопку выхода + this.removeFullscreenExitButton(); + } + + adjustFullscreenHeight() { + // Корректируем высоту с учетом видимой области и возможной клавиатуры + const visualViewport = window.visualViewport || window; + const height = visualViewport.height || window.innerHeight; + + this.editorContainer.style.height = height + 'px'; + + // Пересчитываем размеры Quill + setTimeout(() => { + this.quill.root.style.height = '100%'; + this.quill.root.querySelector('.ql-editor').style.height = '100%'; + }, 50); + } + + addFullscreenExitButton() { + // Создаем кнопку выхода из полноэкранного режима + this.exitButton = document.createElement('button'); + this.exitButton.innerHTML = '✕'; + this.exitButton.title = 'Выйти из полноэкранного режима (ESC)'; + this.exitButton.style.cssText = ` + position: fixed; + top: 10px; + right: 10px; + z-index: 10001; + background: #dc3545; + color: white; + border: none; + border-radius: 50%; + width: 40px; + height: 40px; + font-size: 18px; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + `; + + this.exitButton.addEventListener('click', () => { + this.exitFullscreen(); + }); + + document.body.appendChild(this.exitButton); + } + + removeFullscreenExitButton() { + if (this.exitButton && this.exitButton.parentNode) { + this.exitButton.parentNode.removeChild(this.exitButton); + } } formatExistingDialogues() {