Добавлены функции для работы с многострочными списками и улучшено отображение дат заметок

- Реализована возможность создания нумерованных списков и улучшены функции для работы с многострочными списками.
- Обновлены фильтры для отображения заметок по дате, используя поле created_at вместо date.
- Оптимизировано отображение дат создания и изменения заметок в единой строке.
- Добавлены новые кнопки и обработчики событий для поддержки новых функций в интерфейсе редактирования заметок.
This commit is contained in:
Fovway 2025-10-22 12:56:43 +07:00
parent a77bdd3e7b
commit 083ac11ab1
6 changed files with 224 additions and 22 deletions

View File

@ -9,6 +9,7 @@ const italicBtn = document.getElementById("italicBtn");
const colorBtn = document.getElementById("colorBtn");
const headerBtn = document.getElementById("headerBtn");
const listBtn = document.getElementById("listBtn");
const numberedListBtn = document.getElementById("numberedListBtn");
const quoteBtn = document.getElementById("quoteBtn");
const codeBtn = document.getElementById("codeBtn");
const linkBtn = document.getElementById("linkBtn");
@ -288,6 +289,20 @@ function insertMarkdown(tag) {
const selected = text.substring(start, end);
const after = text.substring(end);
// Мультистрочные преобразования списков (toggle)
if (
(tag === "1. " || tag === "- " || tag === "- [ ] ") &&
selected.includes("\n")
) {
const mode =
tag === "1. " ? "ordered" : tag === "- " ? "unordered" : "todo";
const transformed = transformSelection(noteInput, mode);
noteInput.value = transformed.newValue;
noteInput.setSelectionRange(transformed.newSelStart, transformed.newSelEnd);
noteInput.focus();
return;
}
if (selected.startsWith(tag) && selected.endsWith(tag)) {
// Если теги уже есть, удаляем их
noteInput.value = `${before}${selected.slice(
@ -304,6 +319,7 @@ function insertMarkdown(tag) {
noteInput.setSelectionRange(cursorPosition, cursorPosition + 12);
} else if (
tag === "- " ||
tag === "1. " ||
tag === "> " ||
tag === "# " ||
tag === "- [ ] "
@ -327,6 +343,7 @@ function insertMarkdown(tag) {
noteInput.setSelectionRange(cursorPosition, cursorPosition + 3);
} else if (
tag === "- " ||
tag === "1. " ||
tag === "> " ||
tag === "# " ||
tag === "- [ ] "
@ -408,6 +425,20 @@ function insertMarkdownForEdit(textarea, tag) {
const selected = text.substring(start, end);
const after = text.substring(end);
// Мультистрочные преобразования списков (toggle)
if (
(tag === "1. " || tag === "- " || tag === "- [ ] ") &&
selected.includes("\n")
) {
const mode =
tag === "1. " ? "ordered" : tag === "- " ? "unordered" : "todo";
const transformed = transformSelection(textarea, mode);
textarea.value = transformed.newValue;
textarea.setSelectionRange(transformed.newSelStart, transformed.newSelEnd);
textarea.focus();
return;
}
if (selected.startsWith(tag) && selected.endsWith(tag)) {
// Если теги уже есть, удаляем их
textarea.value = `${before}${selected.slice(
@ -424,6 +455,7 @@ function insertMarkdownForEdit(textarea, tag) {
textarea.setSelectionRange(cursorPosition, cursorPosition + 12);
} else if (
tag === "- " ||
tag === "1. " ||
tag === "> " ||
tag === "# " ||
tag === "- [ ] "
@ -447,6 +479,7 @@ function insertMarkdownForEdit(textarea, tag) {
textarea.setSelectionRange(cursorPosition, cursorPosition + 3);
} else if (
tag === "- " ||
tag === "1. " ||
tag === "> " ||
tag === "# " ||
tag === "- [ ] "
@ -466,6 +499,109 @@ function insertMarkdownForEdit(textarea, tag) {
textarea.focus();
}
// ==================== МУЛЬТИСТРОЧНЫЕ СПИСКИ (TOGGLE) ====================
function transformSelection(textarea, mode) {
const fullText = textarea.value;
const selStart = textarea.selectionStart;
const selEnd = textarea.selectionEnd;
// Расширяем до границ строк
let blockStart = fullText.lastIndexOf("\n", selStart - 1);
blockStart = blockStart === -1 ? 0 : blockStart + 1;
let blockEnd = fullText.indexOf("\n", selEnd);
blockEnd = blockEnd === -1 ? fullText.length : blockEnd;
const block = fullText.substring(blockStart, blockEnd);
const lines = block.split("\n");
const orderedRe = /^(\s*)(\d+)\.\s/;
const unorderedRe = /^(\s*)([-*+])\s/;
const todoRe = /^(\s*)- \[( |x)\] \s?/i;
const anyListPrefixRe = /^(\s*)(- \[(?: |x)\]\s?|[-*+]\s|\d+\.\s)/i;
function stripAnyPrefix(line) {
const m = line.match(anyListPrefixRe);
if (!m) return line;
return line.slice((m[1] + (m[2] || "")).length);
}
function toggleOrdered(inputLines) {
const nonEmpty = inputLines.filter((l) => l.trim() !== "");
const allHave =
nonEmpty.length > 0 && nonEmpty.every((l) => orderedRe.test(l));
if (allHave) {
return inputLines.map((l) => {
if (!l.trim()) return l;
const m = l.match(orderedRe);
if (!m) return l;
return m[1] + l.slice(m[0].length);
});
}
let index = 1;
return inputLines.map((l) => {
if (!l.trim()) return l;
// Снимаем любые префиксы и нумеруем заново
const indent = l.match(/^\s*/)?.[0] || "";
const content = stripAnyPrefix(l.trimStart());
const numbered = `${indent}${index}. ${content}`;
index += 1;
return numbered;
});
}
function toggleUnordered(inputLines) {
const nonEmpty = inputLines.filter((l) => l.trim() !== "");
const allHave =
nonEmpty.length > 0 && nonEmpty.every((l) => unorderedRe.test(l));
if (allHave) {
return inputLines.map((l) => {
if (!l.trim()) return l;
const m = l.match(unorderedRe);
if (!m) return l;
return m[1] + l.slice(m[0].length);
});
}
return inputLines.map((l) => {
if (!l.trim()) return l;
const indent = l.match(/^\s*/)?.[0] || "";
const content = stripAnyPrefix(l.trimStart());
return `${indent}- ${content}`;
});
}
function toggleTodo(inputLines) {
const nonEmpty = inputLines.filter((l) => l.trim() !== "");
const allHave =
nonEmpty.length > 0 && nonEmpty.every((l) => todoRe.test(l));
if (allHave) {
return inputLines.map((l) => {
if (!l.trim()) return l;
const m = l.match(todoRe);
if (!m) return l;
return m[1] + l.slice(m[0].length);
});
}
return inputLines.map((l) => {
if (!l.trim()) return l;
const indent = l.match(/^\s*/)?.[0] || "";
const content = stripAnyPrefix(l.trimStart());
return `${indent}- [ ] ${content}`;
});
}
let newLines;
if (mode === "ordered") newLines = toggleOrdered(lines);
else if (mode === "unordered") newLines = toggleUnordered(lines);
else newLines = toggleTodo(lines);
const newBlock = newLines.join("\n");
const newValue =
fullText.slice(0, blockStart) + newBlock + fullText.slice(blockEnd);
const newSelStart = blockStart;
const newSelEnd = blockStart + newBlock.length;
return { newValue, newSelStart, newSelEnd };
}
// Обработчики для кнопок markdown
boldBtn.addEventListener("click", function () {
insertMarkdown("**");
@ -487,6 +623,10 @@ listBtn.addEventListener("click", function () {
insertMarkdown("- ");
});
numberedListBtn.addEventListener("click", function () {
insertMarkdown("1. ");
});
quoteBtn.addEventListener("click", function () {
insertMarkdown("> ");
});
@ -934,9 +1074,12 @@ async function renderNotes(notes) {
let notesToDisplay = notes;
if (selectedDateFilter) {
notesToDisplay = notesToDisplay.filter(
(note) => note.date === selectedDateFilter
);
notesToDisplay = notesToDisplay.filter((note) => {
if (note.created_at) {
return formatDateFromTimestamp(note.created_at) === selectedDateFilter;
}
return false;
});
}
if (selectedTagFilter) {
@ -993,10 +1136,9 @@ async function renderNotes(notes) {
imagesHtml += "</div>";
}
// Форматируем дату создания и дату изменения
// Форматируем дату создания и дату изменения (в одну строку)
let dateDisplay = `${note.date} ${note.time}`;
if (note.updated_at && note.created_at !== note.updated_at) {
// Если дата изменения отличается от даты создания, показываем обе даты
const createdDate = new Date(note.created_at);
const updatedDate = new Date(note.updated_at);
@ -1011,19 +1153,19 @@ async function renderNotes(notes) {
dateDisplay = `Создано: ${formatDate(
createdDate
)}<br>Изменено: ${formatDate(updatedDate)}`;
)} Изменено: ${formatDate(updatedDate)}`;
}
const noteHtml = `
<div id="note" class="container" data-note-id="${note.id}">
<div class="date">
${dateDisplay}
<span class="date-text">${dateDisplay}</span>
<div id="editBtn" class="notesHeaderBtn" data-id="${
note.id
}">Редактировать</div>
}">Ред.</div>
<div id="deleteBtn" class="notesHeaderBtn" data-id="${
note.id
}">Удалить</div>
}">Удал.</div>
</div>
<div class="textNote" data-original-content="${note.content.replace(
/"/g,
@ -1151,6 +1293,11 @@ function addNoteEventListeners() {
{ id: "editColorBtn", icon: "mdi:palette", tag: "color" },
{ id: "editHeaderBtn", icon: "mdi:format-header-1", tag: "# " },
{ id: "editListBtn", icon: "mdi:format-list-bulleted", tag: "- " },
{
id: "editNumberedListBtn",
icon: "mdi:format-list-numbered",
tag: "1. ",
},
{ id: "editQuoteBtn", icon: "mdi:format-quote-close", tag: "> " },
{ id: "editCodeBtn", icon: "mdi:code-tags", tag: "`" },
{ id: "editLinkBtn", icon: "mdi:link", tag: "[Текст ссылки](URL)" },
@ -1231,6 +1378,9 @@ function addNoteEventListeners() {
pattern === listPatterns[4]
) {
listType = "unordered";
} else if (pattern === listPatterns[7]) {
// Нумерованный список всегда начинается с 1.
listType = "numbered";
} else {
listType = "ordered";
}
@ -1282,6 +1432,9 @@ function addNoteEventListeners() {
nextNumber.toString()
);
newMarker = indent + numberStr + ". ";
} else if (listType === "numbered") {
// Для нумерованного списка всегда начинаем с 1.
newMarker = indent + "1. ";
}
textarea.value = beforeCursor + "\n" + newMarker + afterCursor;
@ -1879,6 +2032,7 @@ noteInput.addEventListener("keydown", function (event) {
/^(\s*)\+ /, // Неупорядоченный список: +
/^(\s*)(\d+)\. /, // Упорядоченный список: 1. 2. 3.
/^(\s*)(\w+)\. /, // Буквенный список: a. b. c.
/^(\s*)1\. /, // Нумерованный список (начинается с 1.)
];
let listMatch = null;
@ -1896,6 +2050,9 @@ noteInput.addEventListener("keydown", function (event) {
pattern === listPatterns[4]
) {
listType = "unordered";
} else if (pattern === listPatterns[7]) {
// Нумерованный список всегда начинается с 1.
listType = "numbered";
} else {
listType = "ordered";
}
@ -1944,6 +2101,9 @@ noteInput.addEventListener("keydown", function (event) {
const nextNumber = number + 1;
const numberStr = listMatch[2].replace(/\d+/, nextNumber.toString());
newMarker = indent + numberStr + ". ";
} else if (listType === "numbered") {
// Для нумерованного списка всегда начинаем с 1.
newMarker = indent + "1. ";
}
textarea.value = beforeCursor + "\n" + newMarker + afterCursor;
@ -2068,6 +2228,15 @@ async function loadUserInfo() {
// Календарь
let currentDate = new Date();
// Функция для преобразования timestamp в формат dd.mm.yyyy
function formatDateFromTimestamp(timestamp) {
const date = new Date(timestamp);
const day = String(date.getDate()).padStart(2, "0");
const month = String(date.getMonth() + 1).padStart(2, "0");
const year = date.getFullYear();
return `${day}.${month}.${year}`;
}
// Функция для отображения календаря
function renderCalendar() {
const calendarDays = document.getElementById("calendarDays");
@ -2110,10 +2279,12 @@ function renderCalendar() {
// Очищаем календарь
calendarDays.innerHTML = "";
// Создаём Set дат, когда были созданы заметки
// Создаём Set дат, когда были созданы заметки (используем created_at)
const noteDates = new Set();
allNotes.forEach((note) => {
noteDates.add(note.date);
if (note.created_at) {
noteDates.add(formatDateFromTimestamp(note.created_at));
}
});
// Получаем последний день предыдущего месяца
@ -2429,10 +2600,12 @@ function renderCalendarMobile() {
// Очищаем календарь
calendarDays.innerHTML = "";
// Создаём Set дат, когда были созданы заметки
// Создаём Set дат, когда были созданы заметки (используем created_at)
const noteDates = new Set();
allNotes.forEach((note) => {
noteDates.add(note.date);
if (note.created_at) {
noteDates.add(formatDateFromTimestamp(note.created_at));
}
});
// Получаем последний день предыдущего месяца

View File

@ -8,6 +8,15 @@
/>
<title>Вход в систему заметок</title>
<script>
try {
if (localStorage.getItem("isAuthenticated") === "true") {
// Используем replace, чтобы не оставлять страницу логина в истории
window.location.replace("/notes");
}
} catch (e) {}
</script>
<!-- PWA Meta Tags -->
<meta
name="description"

View File

@ -267,6 +267,13 @@
<button class="btnMarkdown" id="listBtn" title="Список">
<span class="iconify" data-icon="mdi:format-list-bulleted"></span>
</button>
<button
class="btnMarkdown"
id="numberedListBtn"
title="Нумерованный список"
>
<span class="iconify" data-icon="mdi:format-list-numbered"></span>
</button>
<button class="btnMarkdown" id="quoteBtn" title="Цитата">
<span class="iconify" data-icon="mdi:format-quote-close"></span>
</button>

View File

@ -8,6 +8,14 @@
/>
<title>Регистрация - NoteJS</title>
<script>
try {
if (localStorage.getItem("isAuthenticated") === "true") {
window.location.replace("/notes");
}
} catch (e) {}
</script>
<!-- PWA Meta Tags -->
<meta
name="description"

View File

@ -376,11 +376,17 @@ textarea:focus {
.date {
font-size: 11px;
color: grey;
white-space: pre-line;
white-space: nowrap;
display: flex;
align-items: flex-start;
align-items: center;
gap: 10px;
flex-wrap: wrap;
flex-wrap: nowrap;
}
.date .date-text {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
}
.notesHeaderBtn {
@ -474,7 +480,7 @@ textarea:focus {
.textNote li {
margin: 0;
padding: 0;
line-height: 0.5;
line-height: 1;
word-wrap: break-word;
overflow-wrap: break-word;
}
@ -548,7 +554,7 @@ textarea:focus {
list-style-type: none;
margin-left: -20px;
padding: 0;
line-height: 0.5;
line-height: 1.5;
transition: all 0.3s ease;
}
@ -585,7 +591,7 @@ textarea:focus {
list-style-type: none;
margin-left: -20px;
padding: 0;
line-height: 0.5;
line-height: 1.5;
transition: all 0.3s ease;
}
@ -1257,7 +1263,6 @@ textarea:focus {
.mobile-sidebar .tag {
display: inline-block;
padding: 4px 8px;
background-color: #e7f3ff;
color: var(--accent-color, #007bff);
border: 1px solid var(--accent-color, #007bff);
border-radius: 12px;

View File

@ -563,9 +563,9 @@ app.get("/api/notes/search", requireAuth, (req, res) => {
params.push(`%#${tag.trim()}%`);
}
// Поиск по дате
// Поиск по дате (используем created_at вместо date)
if (date && date.trim()) {
whereClause += " AND n.date = ?";
whereClause += " AND strftime('%d.%m.%Y', n.created_at) = ?";
params.push(date.trim());
}