Обновлены компоненты редактирования заметок для поддержки обработки маркеров списков и цитат. Добавлена логика для переключения маркеров на нескольких строках, улучшена производительность и адаптивность интерфейса. Обновлены стили плавающей панели форматирования для улучшения пользовательского опыта на мобильных устройствах.

This commit is contained in:
Fovway 2025-11-04 10:28:16 +07:00
parent a5f4e87056
commit 3c2b23c699
6 changed files with 293 additions and 23 deletions

View File

@ -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"), {

View File

@ -296,6 +296,7 @@ export const FloatingToolbar: React.FC<FloatingToolbarProps> = ({
{hasSelection && (
<>
<div className="floating-toolbar-separator" />
<button
className="floating-toolbar-btn"
onClick={handleCopy}
@ -367,22 +368,6 @@ export const FloatingToolbar: React.FC<FloatingToolbarProps> = ({
>
<Icon icon="mdi:eye-off" />
</button>
<button
className="floating-toolbar-btn"
onClick={() => handleFormat("`", "`")}
title="Код"
>
<Icon icon="mdi:code-tags" />
</button>
<button
className="floating-toolbar-btn"
onClick={() => handleFormat("> ", "")}
title="Цитата"
>
<Icon icon="mdi:format-quote-close" />
</button>
</>
)}
</div>

View File

@ -239,6 +239,128 @@ export const NoteEditor: React.FC<NoteEditorProps> = ({ 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;
// Проверяем область вокруг выделения (расширяем для проверки тегов)

View File

@ -325,6 +325,128 @@ export const NoteItem: React.FC<NoteItemProps> = ({
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;
// Проверяем область вокруг выделения (расширяем для проверки тегов)

View File

@ -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 {

View File

@ -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 = `<input type="checkbox" ${checked ? "checked" : ""} />`;