Compare commits
No commits in common. "3af0f18d5510683dc16c35a2c41f3659c02943f0" and "15ab9d1947884bb3eac9b5adabb4c311665c10b0" have entirely different histories.
3af0f18d55
...
15ab9d1947
|
|
@ -19,27 +19,3 @@
|
||||||
height: 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;
|
|
||||||
}
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
// editor.js
|
|
||||||
class DialogueFormatter {
|
class DialogueFormatter {
|
||||||
constructor(quill) {
|
constructor(quill) {
|
||||||
this.quill = quill;
|
this.quill = quill;
|
||||||
|
|
@ -20,6 +19,7 @@ class DialogueFormatter {
|
||||||
|
|
||||||
getLineStart(index) {
|
getLineStart(index) {
|
||||||
let lineStart = index;
|
let lineStart = index;
|
||||||
|
// Безопасный поиск начала строки (защита от отрицательных индексов)
|
||||||
while (lineStart > 0) {
|
while (lineStart > 0) {
|
||||||
const prevChar = this.quill.getText(lineStart - 1, 1);
|
const prevChar = this.quill.getText(lineStart - 1, 1);
|
||||||
if (prevChar === '\n') break;
|
if (prevChar === '\n') break;
|
||||||
|
|
@ -30,49 +30,65 @@ class DialogueFormatter {
|
||||||
|
|
||||||
checkForDialogue() {
|
checkForDialogue() {
|
||||||
const selection = this.quill.getSelection();
|
const selection = this.quill.getSelection();
|
||||||
|
// Защита от некорректных позиций курсора
|
||||||
if (!selection || selection.index < 2) return;
|
if (!selection || selection.index < 2) return;
|
||||||
|
|
||||||
const lineStart = this.getLineStart(selection.index);
|
const lineStart = this.getLineStart(selection.index);
|
||||||
const charsFromStart = selection.index - lineStart;
|
const charsFromStart = selection.index - lineStart;
|
||||||
|
|
||||||
|
// Работаем ТОЛЬКО когда:
|
||||||
|
// 1. Ровно 2 символа от начала строки до курсора
|
||||||
|
// 2. Эти символы - "- "
|
||||||
if (charsFromStart !== 2) return;
|
if (charsFromStart !== 2) return;
|
||||||
|
|
||||||
const text = this.quill.getText(lineStart, 2);
|
const text = this.quill.getText(lineStart, 2);
|
||||||
if (text !== '- ') return;
|
if (text !== '- ') return;
|
||||||
|
|
||||||
|
// Атомарная операция замены
|
||||||
this.quill.updateContents([
|
this.quill.updateContents([
|
||||||
{ retain: lineStart },
|
{ retain: lineStart },
|
||||||
{ delete: 2 },
|
{ delete: 2 }, // Удаляем "- "
|
||||||
{ insert: '— ' }
|
{ insert: '— ' } // Вставляем "— "
|
||||||
], 'user');
|
], 'user');
|
||||||
|
|
||||||
|
// Явно устанавливаем курсор ПОСЛЕ пробела
|
||||||
this.quill.setSelection(lineStart + 2, 0, 'silent');
|
this.quill.setSelection(lineStart + 2, 0, 'silent');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Простой метод для форматирования диалогов
|
||||||
formatSelectionAsDialogue() {
|
formatSelectionAsDialogue() {
|
||||||
const range = this.quill.getSelection();
|
const range = this.quill.getSelection();
|
||||||
if (!range || range.length === 0) return;
|
if (!range || range.length === 0) return;
|
||||||
|
|
||||||
const selectedText = this.quill.getText(range.index, range.length);
|
const selectedText = this.quill.getText(range.index, range.length);
|
||||||
|
|
||||||
|
// Простая замена всех "- " на "— " в выделенном тексте
|
||||||
const formattedText = selectedText.replace(/(^|\n)- /g, '$1— ');
|
const formattedText = selectedText.replace(/(^|\n)- /g, '$1— ');
|
||||||
|
|
||||||
if (formattedText !== selectedText) {
|
if (formattedText !== selectedText) {
|
||||||
this.quill.deleteText(range.index, range.length, 'user');
|
this.quill.deleteText(range.index, range.length, 'user');
|
||||||
this.quill.insertText(range.index, formattedText, 'user');
|
this.quill.insertText(range.index, formattedText, 'user');
|
||||||
|
|
||||||
|
// Восстанавливаем выделение
|
||||||
this.quill.setSelection(range.index, formattedText.length, 'silent');
|
this.quill.setSelection(range.index, formattedText.length, 'silent');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Простой метод для отмены форматирования диалогов
|
||||||
unformatSelectionAsDialogue() {
|
unformatSelectionAsDialogue() {
|
||||||
const range = this.quill.getSelection();
|
const range = this.quill.getSelection();
|
||||||
if (!range || range.length === 0) return;
|
if (!range || range.length === 0) return;
|
||||||
|
|
||||||
const selectedText = this.quill.getText(range.index, range.length);
|
const selectedText = this.quill.getText(range.index, range.length);
|
||||||
|
|
||||||
|
// Простая замена всех "— " на "- " в выделенном тексте
|
||||||
const unformattedText = selectedText.replace(/(^|\n)— /g, '$1- ');
|
const unformattedText = selectedText.replace(/(^|\n)— /g, '$1- ');
|
||||||
|
|
||||||
if (unformattedText !== selectedText) {
|
if (unformattedText !== selectedText) {
|
||||||
this.quill.deleteText(range.index, range.length, 'user');
|
this.quill.deleteText(range.index, range.length, 'user');
|
||||||
this.quill.insertText(range.index, unformattedText, 'user');
|
this.quill.insertText(range.index, unformattedText, 'user');
|
||||||
|
|
||||||
|
// Восстанавливаем выделение
|
||||||
this.quill.setSelection(range.index, unformattedText.length, 'silent');
|
this.quill.setSelection(range.index, unformattedText.length, 'silent');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -83,8 +99,6 @@ class WriterEditor {
|
||||||
this.form = document.querySelector(formSelector);
|
this.form = document.querySelector(formSelector);
|
||||||
this.editorContainer = document.getElementById(editorContainerId);
|
this.editorContainer = document.getElementById(editorContainerId);
|
||||||
this.textarea = document.getElementById(textareaId);
|
this.textarea = document.getElementById(textareaId);
|
||||||
this.isFullscreen = false;
|
|
||||||
this.originalStyles = {};
|
|
||||||
this.init();
|
this.init();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -109,7 +123,6 @@ class WriterEditor {
|
||||||
[{ 'font': [] }],
|
[{ 'font': [] }],
|
||||||
['link','image','video'],
|
['link','image','video'],
|
||||||
['clean'],
|
['clean'],
|
||||||
['fullscreen'] // Добавляем кнопку полноэкранного режима
|
|
||||||
],
|
],
|
||||||
handlers: {
|
handlers: {
|
||||||
'dialogue': () => {
|
'dialogue': () => {
|
||||||
|
|
@ -121,9 +134,6 @@ class WriterEditor {
|
||||||
if (this.dialogueFormatter) {
|
if (this.dialogueFormatter) {
|
||||||
this.dialogueFormatter.unformatSelectionAsDialogue();
|
this.dialogueFormatter.unformatSelectionAsDialogue();
|
||||||
}
|
}
|
||||||
},
|
|
||||||
'fullscreen': () => {
|
|
||||||
this.toggleFullscreen();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -146,14 +156,20 @@ class WriterEditor {
|
||||||
placeholder: 'Введите текст главы...'
|
placeholder: 'Введите текст главы...'
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Добавляем кастомные кнопки в тулбар после инициализации Quill
|
||||||
this.addCustomButtonsToToolbar();
|
this.addCustomButtonsToToolbar();
|
||||||
|
|
||||||
|
// Инициализируем автоформатер диалогов
|
||||||
this.dialogueFormatter = new DialogueFormatter(this.quill);
|
this.dialogueFormatter = new DialogueFormatter(this.quill);
|
||||||
|
|
||||||
|
// Загружаем текст
|
||||||
const rawContent = this.editorContainer.dataset.content || '';
|
const rawContent = this.editorContainer.dataset.content || '';
|
||||||
if (rawContent.trim()) this.quill.root.innerHTML = rawContent.trim();
|
if (rawContent.trim()) this.quill.root.innerHTML = rawContent.trim();
|
||||||
|
|
||||||
|
// Обрабатываем уже существующие диалоги при загрузке
|
||||||
setTimeout(() => this.formatExistingDialogues(), 100);
|
setTimeout(() => this.formatExistingDialogues(), 100);
|
||||||
|
|
||||||
|
// Синхронизация с textarea
|
||||||
const sync = () => {
|
const sync = () => {
|
||||||
let html = this.quill.root.innerHTML;
|
let html = this.quill.root.innerHTML;
|
||||||
html = html.replace(/^(<p><br><\/p>)+/, '').replace(/(<p><br><\/p>)+$/, '');
|
html = html.replace(/^(<p><br><\/p>)+/, '').replace(/(<p><br><\/p>)+$/, '');
|
||||||
|
|
@ -163,32 +179,21 @@ class WriterEditor {
|
||||||
this.quill.on('text-change', sync);
|
this.quill.on('text-change', sync);
|
||||||
this.form.addEventListener('submit', 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.quillEditorInstance = this.quill;
|
||||||
window.quillTextarea = this.textarea;
|
window.quillTextarea = this.textarea;
|
||||||
}
|
}
|
||||||
|
|
||||||
addCustomButtonsToToolbar() {
|
addCustomButtonsToToolbar() {
|
||||||
|
// Находим тулбар Quill
|
||||||
const toolbar = this.quill.container.previousSibling;
|
const toolbar = this.quill.container.previousSibling;
|
||||||
if (!toolbar) return;
|
if (!toolbar) return;
|
||||||
|
|
||||||
|
// Находим кнопки по классам Quill
|
||||||
const dialogueBtn = toolbar.querySelector('.ql-dialogue');
|
const dialogueBtn = toolbar.querySelector('.ql-dialogue');
|
||||||
const undoDialogueBtn = toolbar.querySelector('.ql-undodialogue');
|
const undoDialogueBtn = toolbar.querySelector('.ql-undodialogue');
|
||||||
const fullscreenBtn = toolbar.querySelector('.ql-fullscreen');
|
|
||||||
|
|
||||||
|
// Заменяем содержимое кнопок на наши символы
|
||||||
if (dialogueBtn) {
|
if (dialogueBtn) {
|
||||||
dialogueBtn.innerHTML = '—';
|
dialogueBtn.innerHTML = '—';
|
||||||
dialogueBtn.title = 'Форматировать диалоги (—)';
|
dialogueBtn.title = 'Форматировать диалоги (—)';
|
||||||
|
|
@ -200,137 +205,6 @@ class WriterEditor {
|
||||||
undoDialogueBtn.title = 'Убрать форматирование диалогов (-)';
|
undoDialogueBtn.title = 'Убрать форматирование диалогов (-)';
|
||||||
undoDialogueBtn.style.fontWeight = 'bold';
|
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() {
|
formatExistingDialogues() {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue