diff --git a/dev-dist/sw.js b/dev-dist/sw.js index ec8c3ed..6b0fcec 100644 --- a/dev-dist/sw.js +++ b/dev-dist/sw.js @@ -82,7 +82,7 @@ define(['./workbox-9dc17825'], (function (workbox) { 'use strict'; "revision": "3ca0b8505b4bec776b69afdba2768812" }, { "url": "index.html", - "revision": "0.nsn25edhihg" + "revision": "0.o51qplqi6t" }], {}); workbox.cleanupOutdatedCaches(); workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), { diff --git a/src/components/notes/FloatingToolbar.tsx b/src/components/notes/FloatingToolbar.tsx index e1fa7b6..1b8e030 100644 --- a/src/components/notes/FloatingToolbar.tsx +++ b/src/components/notes/FloatingToolbar.tsx @@ -296,6 +296,7 @@ export const FloatingToolbar: React.FC = ({ {hasSelection && ( <> +
- - - - )}
diff --git a/src/components/notes/NoteEditor.tsx b/src/components/notes/NoteEditor.tsx index 3d6f6ab..f59d1f7 100644 --- a/src/components/notes/NoteEditor.tsx +++ b/src/components/notes/NoteEditor.tsx @@ -239,6 +239,128 @@ export const NoteEditor: React.FC = ({ onSave }) => { const end = textarea.selectionEnd; const selectedText = content.substring(start, end); + // Определяем маркеры списков и цитат, которые обрабатываются построчно + const listMarkers = ["- ", "1. ", "- [ ] ", "> "]; + const isListMarker = listMarkers.includes(before); + + // Если это маркер списка и выделено несколько строк, обрабатываем построчно + if (isListMarker && selectedText.includes("\n")) { + const lines = selectedText.split("\n"); + const beforeText = content.substring(0, start); + const afterText = content.substring(end); + + // Определяем, есть ли уже такие маркеры на всех строках + let allLinesHaveMarker = true; + let hasAnyMarker = false; + + for (const line of lines) { + const trimmedLine = line.trimStart(); + if (before === "- ") { + // Для маркированного списка проверяем различные варианты + if (trimmedLine.match(/^[-*+]\s/)) { + hasAnyMarker = true; + } else { + allLinesHaveMarker = false; + } + } else if (before === "1. ") { + // Для нумерованного списка + if (trimmedLine.match(/^\d+\.\s/)) { + hasAnyMarker = true; + } else { + allLinesHaveMarker = false; + } + } else if (before === "- [ ] ") { + // Для чекбокса + if (trimmedLine.match(/^-\s+\[[ xX]\]\s/)) { + hasAnyMarker = true; + } else { + allLinesHaveMarker = false; + } + } else if (before === "> ") { + // Для цитаты + if (trimmedLine.startsWith("> ")) { + hasAnyMarker = true; + } else { + allLinesHaveMarker = false; + } + } + } + + // Если все строки уже имеют маркер, удаляем их (переключение) + // Если некоторые имеют, но не все - добавляем к тем, у которых нет + const processedLines = lines.map((line, index) => { + const trimmedLine = line.trimStart(); + const leadingSpaces = line.substring(0, line.length - trimmedLine.length); + let shouldToggle = false; + + if (before === "- ") { + const match = trimmedLine.match(/^([-*+])\s/); + if (match) { + shouldToggle = true; + } + } else if (before === "1. ") { + if (trimmedLine.match(/^\d+\.\s/)) { + shouldToggle = true; + } + } else if (before === "- [ ] ") { + if (trimmedLine.match(/^-\s+\[[ xX]\]\s/)) { + shouldToggle = true; + } + } else if (before === "> ") { + if (trimmedLine.startsWith("> ")) { + shouldToggle = true; + } + } + + if (shouldToggle && allLinesHaveMarker) { + // Удаляем маркер + if (before === "- ") { + const match = trimmedLine.match(/^([-*+])\s(.*)$/); + return match ? leadingSpaces + match[2] : line; + } else if (before === "1. ") { + const match = trimmedLine.match(/^\d+\.\s(.*)$/); + return match ? leadingSpaces + match[1] : line; + } else if (before === "- [ ] ") { + const match = trimmedLine.match(/^-\s+\[[ xX]\]\s(.*)$/); + return match ? leadingSpaces + match[1] : line; + } else if (before === "> ") { + return trimmedLine.startsWith("> ") + ? leadingSpaces + trimmedLine.substring(2) + : line; + } + } else if (!shouldToggle || !allLinesHaveMarker) { + // Добавляем маркер + if (before === "1. ") { + // Для нумерованного списка добавляем правильный номер + const number = index + 1; + return leadingSpaces + `${number}. ` + trimmedLine; + } else { + return leadingSpaces + before + trimmedLine; + } + } + + return line; + }); + + const newSelectedText = processedLines.join("\n"); + const newText = beforeText + newSelectedText + afterText; + + // Вычисляем новую позицию курсора + const newStart = start; + const newEnd = start + newSelectedText.length; + + setContent(newText); + + setTimeout(() => { + textarea.focus(); + textarea.setSelectionRange(newStart, newEnd); + const formats = getActiveFormats(); + setActiveFormats(formats); + }, 0); + + return; + } + const tagLength = before.length; // Проверяем область вокруг выделения (расширяем для проверки тегов) diff --git a/src/components/notes/NoteItem.tsx b/src/components/notes/NoteItem.tsx index 691f35e..233691e 100644 --- a/src/components/notes/NoteItem.tsx +++ b/src/components/notes/NoteItem.tsx @@ -325,6 +325,128 @@ export const NoteItem: React.FC = ({ const end = textarea.selectionEnd; const selectedText = editContent.substring(start, end); + // Определяем маркеры списков и цитат, которые обрабатываются построчно + const listMarkers = ["- ", "1. ", "- [ ] ", "> "]; + const isListMarker = listMarkers.includes(before); + + // Если это маркер списка и выделено несколько строк, обрабатываем построчно + if (isListMarker && selectedText.includes("\n")) { + const lines = selectedText.split("\n"); + const beforeText = editContent.substring(0, start); + const afterText = editContent.substring(end); + + // Определяем, есть ли уже такие маркеры на всех строках + let allLinesHaveMarker = true; + let hasAnyMarker = false; + + for (const line of lines) { + const trimmedLine = line.trimStart(); + if (before === "- ") { + // Для маркированного списка проверяем различные варианты + if (trimmedLine.match(/^[-*+]\s/)) { + hasAnyMarker = true; + } else { + allLinesHaveMarker = false; + } + } else if (before === "1. ") { + // Для нумерованного списка + if (trimmedLine.match(/^\d+\.\s/)) { + hasAnyMarker = true; + } else { + allLinesHaveMarker = false; + } + } else if (before === "- [ ] ") { + // Для чекбокса + if (trimmedLine.match(/^-\s+\[[ xX]\]\s/)) { + hasAnyMarker = true; + } else { + allLinesHaveMarker = false; + } + } else if (before === "> ") { + // Для цитаты + if (trimmedLine.startsWith("> ")) { + hasAnyMarker = true; + } else { + allLinesHaveMarker = false; + } + } + } + + // Если все строки уже имеют маркер, удаляем их (переключение) + // Если некоторые имеют, но не все - добавляем к тем, у которых нет + const processedLines = lines.map((line, index) => { + const trimmedLine = line.trimStart(); + const leadingSpaces = line.substring(0, line.length - trimmedLine.length); + let shouldToggle = false; + + if (before === "- ") { + const match = trimmedLine.match(/^([-*+])\s/); + if (match) { + shouldToggle = true; + } + } else if (before === "1. ") { + if (trimmedLine.match(/^\d+\.\s/)) { + shouldToggle = true; + } + } else if (before === "- [ ] ") { + if (trimmedLine.match(/^-\s+\[[ xX]\]\s/)) { + shouldToggle = true; + } + } else if (before === "> ") { + if (trimmedLine.startsWith("> ")) { + shouldToggle = true; + } + } + + if (shouldToggle && allLinesHaveMarker) { + // Удаляем маркер + if (before === "- ") { + const match = trimmedLine.match(/^([-*+])\s(.*)$/); + return match ? leadingSpaces + match[2] : line; + } else if (before === "1. ") { + const match = trimmedLine.match(/^\d+\.\s(.*)$/); + return match ? leadingSpaces + match[1] : line; + } else if (before === "- [ ] ") { + const match = trimmedLine.match(/^-\s+\[[ xX]\]\s(.*)$/); + return match ? leadingSpaces + match[1] : line; + } else if (before === "> ") { + return trimmedLine.startsWith("> ") + ? leadingSpaces + trimmedLine.substring(2) + : line; + } + } else if (!shouldToggle || !allLinesHaveMarker) { + // Добавляем маркер + if (before === "1. ") { + // Для нумерованного списка добавляем правильный номер + const number = index + 1; + return leadingSpaces + `${number}. ` + trimmedLine; + } else { + return leadingSpaces + before + trimmedLine; + } + } + + return line; + }); + + const newSelectedText = processedLines.join("\n"); + const newText = beforeText + newSelectedText + afterText; + + // Вычисляем новую позицию курсора + const newStart = start; + const newEnd = start + newSelectedText.length; + + setEditContent(newText); + + setTimeout(() => { + textarea.focus(); + textarea.setSelectionRange(newStart, newEnd); + const formats = getActiveFormats(); + setActiveFormats(formats); + }, 0); + + return; + } + const tagLength = before.length; // Проверяем область вокруг выделения (расширяем для проверки тегов) diff --git a/src/styles/style.css b/src/styles/style.css index c368cbc..32f9652 100644 --- a/src/styles/style.css +++ b/src/styles/style.css @@ -1612,22 +1612,29 @@ textarea:focus { border-radius: 0 0 5px 5px; } -/* Плавающая панель форматирования */ +/* Плавающая панель форматирования - улучшенная версия */ .floating-toolbar-wrapper { overflow-x: auto; overflow-y: hidden; - /* Скрываем скроллбар */ scrollbar-width: none; - /* Плавная прокрутка */ scroll-behavior: smooth; - /* Максимальная ширина с учетом отступов */ max-width: calc(100vw - 20px); + /* Предотвращаем выбор текста при перетаскивании */ + user-select: none; + -webkit-user-select: none; + /* Улучшаем производительность */ + will-change: transform; } .floating-toolbar-wrapper::-webkit-scrollbar { display: none; } +/* Мобильная версия */ +.floating-toolbar-wrapper.mobile { + max-width: calc(100vw - 32px); +} + .floating-toolbar { display: flex; gap: 4px; @@ -1638,7 +1645,6 @@ textarea:focus { box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); animation: fadeIn 0.2s ease-out; align-items: center; - /* Предотвращаем сжатие кнопок */ flex-shrink: 0; min-width: fit-content; width: max-content; @@ -1669,6 +1675,28 @@ textarea:focus { transition: all 0.2s ease; min-width: 32px; min-height: 32px; + /* Улучшение для touch */ + touch-action: manipulation; + -webkit-tap-highlight-color: transparent; +} + +/* Увеличенные кнопки на мобильных */ +@media (max-width: 768px) { + .floating-toolbar-wrapper.mobile .floating-toolbar-btn { + min-width: 44px; + min-height: 44px; + padding: 8px 12px; + font-size: 18px; + } + + .floating-toolbar-wrapper.mobile .floating-toolbar { + padding: 8px; + gap: 6px; + } + + .floating-toolbar-wrapper.mobile .floating-toolbar-btn .iconify { + font-size: 20px; + } } .floating-toolbar-btn:hover { diff --git a/src/utils/markdown.ts b/src/utils/markdown.ts index 83b2075..6f5f4df 100644 --- a/src/utils/markdown.ts +++ b/src/utils/markdown.ts @@ -45,9 +45,22 @@ const renderer: any = { }, // Кастомный renderer для элементов списка с чекбоксами listitem(token: any) { - const text = token.text; const task = token.task; const checked = token.checked; + + // Используем tokens для правильной обработки форматирования внутри элементов списка + // token.tokens содержит массив токенов для вложенного содержимого + const tokens = token.tokens || []; + let text: string; + + if (tokens.length > 0) { + // Используем this.parser.parseInline для правильной обработки вложенного форматирования + // this указывает на экземпляр Parser в контексте renderer + text = this.parser.parseInline(tokens); + } else { + // Fallback на token.text, если tokens отсутствуют + text = token.text || ''; + } if (task) { const checkbox = ``;