Обновлены компоненты редактирования заметок для поддержки обработки маркеров списков и цитат. Добавлена логика для переключения маркеров на нескольких строках, улучшена производительность и адаптивность интерфейса. Обновлены стили плавающей панели форматирования для улучшения пользовательского опыта на мобильных устройствах.
This commit is contained in:
parent
a5f4e87056
commit
3c2b23c699
@ -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"), {
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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;
|
||||
|
||||
// Проверяем область вокруг выделения (расширяем для проверки тегов)
|
||||
|
||||
@ -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;
|
||||
|
||||
// Проверяем область вокруг выделения (расширяем для проверки тегов)
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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" : ""} />`;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user