// editor.js class DialogueFormatter { constructor(quill) { this.quill = quill; this.lastKey = null; this.setupEventListeners(); } setupEventListeners() { this.quill.root.addEventListener('keydown', (e) => { this.lastKey = e.key; }); this.quill.on('text-change', (delta, oldDelta, source) => { if (source === 'user' && this.lastKey === ' ') { this.checkForDialogue(); } }); } getLineStart(index) { let lineStart = index; while (lineStart > 0) { const prevChar = this.quill.getText(lineStart - 1, 1); if (prevChar === '\n') break; lineStart--; } return lineStart; } checkForDialogue() { const selection = this.quill.getSelection(); if (!selection || selection.index < 2) return; const lineStart = this.getLineStart(selection.index); const charsFromStart = selection.index - lineStart; if (charsFromStart !== 2) return; const text = this.quill.getText(lineStart, 2); if (text !== '- ') return; this.quill.updateContents([ { retain: lineStart }, { 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'); } } } class WriterEditor { 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.isFullscreen = false; this.originalStyles = {}; this.init(); } init() { if (!this.editorContainer || !this.textarea || !this.form) return; this.quill = new Quill(this.editorContainer, { theme: 'snow', modules: { toolbar: { container: [ [{ 'header': [1, 2, 3, 4, 5, 6, false] }], ['bold','italic','underline','strike'], [{ 'align': [] }], [{ 'color': [] }, { 'background': [] }], ['blockquote','code-block'], [{ 'list': 'ordered' }, { 'list': 'bullet' }], [{ 'script': 'sub'}, { 'script': 'super' }], [{ 'indent': '-1'}, { 'indent': '+1' }], ['dialogue', 'undodialogue'], [{ 'size': ['small', false, 'large', 'huge'] }], [{ 'font': [] }], ['link','image','video'], ['clean'], ['fullscreen'] // Добавляем кнопку полноэкранного режима ], handlers: { 'dialogue': () => { if (this.dialogueFormatter) { this.dialogueFormatter.formatSelectionAsDialogue(); } }, 'undodialogue': () => { if (this.dialogueFormatter) { this.dialogueFormatter.unformatSelectionAsDialogue(); } }, 'fullscreen': () => { this.toggleFullscreen(); } } }, history: { delay: 1000, maxStack: 100, userOnly: true }, keyboard: { bindings: { 'list autofill': { key: ' ', format: ['list'], handler: function(range, context) { if (context.prefix && context.prefix.trim() === '-') { return true; } return Quill.import('modules/keyboard').bindings['list autofill'].handler.call(this, range, context); } } } } }, placeholder: 'Введите текст главы...' }); 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); const sync = () => { let html = this.quill.root.innerHTML; html = html.replace(/^(


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


<\/p>)+$/, ''); this.textarea.value = html; }; 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() { const toolbar = this.quill.container.previousSibling; if (!toolbar) return; const dialogueBtn = toolbar.querySelector('.ql-dialogue'); const undoDialogueBtn = toolbar.querySelector('.ql-undodialogue'); const fullscreenBtn = toolbar.querySelector('.ql-fullscreen'); if (dialogueBtn) { dialogueBtn.innerHTML = '—'; dialogueBtn.title = 'Форматировать диалоги (—)'; dialogueBtn.style.fontWeight = 'bold'; } if (undoDialogueBtn) { undoDialogueBtn.innerHTML = '-'; 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() { const text = this.quill.getText(); const lines = text.split('\n'); let totalOffset = 0; lines.forEach((line, index) => { if (line.startsWith('- ')) { const replacePosition = totalOffset; this.quill.deleteText(replacePosition, 1, 'silent'); this.quill.insertText(replacePosition, '—', 'silent'); } totalOffset += line.length + 1; }); } } document.addEventListener('DOMContentLoaded', () => { window.writerEditor = new WriterEditor(); });