Обновлены компоненты редактирования заметок для поддержки обработки маркеров списков и цитат. Добавлена логика для переключения маркеров на нескольких строках, улучшена производительность и адаптивность интерфейса. Обновлены стили плавающей панели форматирования для улучшения пользовательского опыта на мобильных устройствах.
This commit is contained in:
parent
a5f4e87056
commit
3c2b23c699
@ -82,7 +82,7 @@ define(['./workbox-9dc17825'], (function (workbox) { 'use strict';
|
|||||||
"revision": "3ca0b8505b4bec776b69afdba2768812"
|
"revision": "3ca0b8505b4bec776b69afdba2768812"
|
||||||
}, {
|
}, {
|
||||||
"url": "index.html",
|
"url": "index.html",
|
||||||
"revision": "0.nsn25edhihg"
|
"revision": "0.o51qplqi6t"
|
||||||
}], {});
|
}], {});
|
||||||
workbox.cleanupOutdatedCaches();
|
workbox.cleanupOutdatedCaches();
|
||||||
workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), {
|
workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), {
|
||||||
|
|||||||
@ -296,6 +296,7 @@ export const FloatingToolbar: React.FC<FloatingToolbarProps> = ({
|
|||||||
|
|
||||||
{hasSelection && (
|
{hasSelection && (
|
||||||
<>
|
<>
|
||||||
|
<div className="floating-toolbar-separator" />
|
||||||
<button
|
<button
|
||||||
className="floating-toolbar-btn"
|
className="floating-toolbar-btn"
|
||||||
onClick={handleCopy}
|
onClick={handleCopy}
|
||||||
@ -367,22 +368,6 @@ export const FloatingToolbar: React.FC<FloatingToolbarProps> = ({
|
|||||||
>
|
>
|
||||||
<Icon icon="mdi:eye-off" />
|
<Icon icon="mdi:eye-off" />
|
||||||
</button>
|
</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>
|
</div>
|
||||||
|
|||||||
@ -239,6 +239,128 @@ export const NoteEditor: React.FC<NoteEditorProps> = ({ onSave }) => {
|
|||||||
const end = textarea.selectionEnd;
|
const end = textarea.selectionEnd;
|
||||||
const selectedText = content.substring(start, end);
|
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;
|
const tagLength = before.length;
|
||||||
|
|
||||||
// Проверяем область вокруг выделения (расширяем для проверки тегов)
|
// Проверяем область вокруг выделения (расширяем для проверки тегов)
|
||||||
|
|||||||
@ -325,6 +325,128 @@ export const NoteItem: React.FC<NoteItemProps> = ({
|
|||||||
const end = textarea.selectionEnd;
|
const end = textarea.selectionEnd;
|
||||||
const selectedText = editContent.substring(start, end);
|
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;
|
const tagLength = before.length;
|
||||||
|
|
||||||
// Проверяем область вокруг выделения (расширяем для проверки тегов)
|
// Проверяем область вокруг выделения (расширяем для проверки тегов)
|
||||||
|
|||||||
@ -1612,22 +1612,29 @@ textarea:focus {
|
|||||||
border-radius: 0 0 5px 5px;
|
border-radius: 0 0 5px 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Плавающая панель форматирования */
|
/* Плавающая панель форматирования - улучшенная версия */
|
||||||
.floating-toolbar-wrapper {
|
.floating-toolbar-wrapper {
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
overflow-y: hidden;
|
overflow-y: hidden;
|
||||||
/* Скрываем скроллбар */
|
|
||||||
scrollbar-width: none;
|
scrollbar-width: none;
|
||||||
/* Плавная прокрутка */
|
|
||||||
scroll-behavior: smooth;
|
scroll-behavior: smooth;
|
||||||
/* Максимальная ширина с учетом отступов */
|
|
||||||
max-width: calc(100vw - 20px);
|
max-width: calc(100vw - 20px);
|
||||||
|
/* Предотвращаем выбор текста при перетаскивании */
|
||||||
|
user-select: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
/* Улучшаем производительность */
|
||||||
|
will-change: transform;
|
||||||
}
|
}
|
||||||
|
|
||||||
.floating-toolbar-wrapper::-webkit-scrollbar {
|
.floating-toolbar-wrapper::-webkit-scrollbar {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Мобильная версия */
|
||||||
|
.floating-toolbar-wrapper.mobile {
|
||||||
|
max-width: calc(100vw - 32px);
|
||||||
|
}
|
||||||
|
|
||||||
.floating-toolbar {
|
.floating-toolbar {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
@ -1638,7 +1645,6 @@ textarea:focus {
|
|||||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||||
animation: fadeIn 0.2s ease-out;
|
animation: fadeIn 0.2s ease-out;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
/* Предотвращаем сжатие кнопок */
|
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
min-width: fit-content;
|
min-width: fit-content;
|
||||||
width: max-content;
|
width: max-content;
|
||||||
@ -1669,6 +1675,28 @@ textarea:focus {
|
|||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
min-width: 32px;
|
min-width: 32px;
|
||||||
min-height: 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 {
|
.floating-toolbar-btn:hover {
|
||||||
|
|||||||
@ -45,10 +45,23 @@ const renderer: any = {
|
|||||||
},
|
},
|
||||||
// Кастомный renderer для элементов списка с чекбоксами
|
// Кастомный renderer для элементов списка с чекбоксами
|
||||||
listitem(token: any) {
|
listitem(token: any) {
|
||||||
const text = token.text;
|
|
||||||
const task = token.task;
|
const task = token.task;
|
||||||
const checked = token.checked;
|
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) {
|
if (task) {
|
||||||
const checkbox = `<input type="checkbox" ${checked ? "checked" : ""} />`;
|
const checkbox = `<input type="checkbox" ${checked ? "checked" : ""} />`;
|
||||||
return `<li class="task-list-item">${checkbox} ${text}</li>\n`;
|
return `<li class="task-list-item">${checkbox} ${text}</li>\n`;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user