From 083ac11ab1ae6650bf776384142bfad6a0452f3f Mon Sep 17 00:00:00 2001 From: Fovway Date: Wed, 22 Oct 2025 12:56:43 +0700 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20=D0=94=D0=BE=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D1=8B=20=D1=84=D1=83=D0=BD=D0=BA=D1=86=D0=B8?= =?UTF-8?q?=D0=B8=20=D0=B4=D0=BB=D1=8F=20=D1=80=D0=B0=D0=B1=D0=BE=D1=82?= =?UTF-8?q?=D1=8B=20=D1=81=20=D0=BC=D0=BD=D0=BE=D0=B3=D0=BE=D1=81=D1=82?= =?UTF-8?q?=D1=80=D0=BE=D1=87=D0=BD=D1=8B=D0=BC=D0=B8=20=D1=81=D0=BF=D0=B8?= =?UTF-8?q?=D1=81=D0=BA=D0=B0=D0=BC=D0=B8=20=D0=B8=20=D1=83=D0=BB=D1=83?= =?UTF-8?q?=D1=87=D1=88=D0=B5=D0=BD=D0=BE=20=D0=BE=D1=82=D0=BE=D0=B1=D1=80?= =?UTF-8?q?=D0=B0=D0=B6=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=B4=D0=B0=D1=82=20?= =?UTF-8?q?=D0=B7=D0=B0=D0=BC=D0=B5=D1=82=D0=BE=D0=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Реализована возможность создания нумерованных списков и улучшены функции для работы с многострочными списками. - Обновлены фильтры для отображения заметок по дате, используя поле created_at вместо date. - Оптимизировано отображение дат создания и изменения заметок в единой строке. - Добавлены новые кнопки и обработчики событий для поддержки новых функций в интерфейсе редактирования заметок. --- public/app.js | 199 ++++++++++++++++++++++++++++++++++++++++--- public/index.html | 9 ++ public/notes.html | 7 ++ public/register.html | 8 ++ public/style.css | 19 +++-- server.js | 4 +- 6 files changed, 224 insertions(+), 22 deletions(-) diff --git a/public/app.js b/public/app.js index 3fecf47..1dc0199 100644 --- a/public/app.js +++ b/public/app.js @@ -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 += ""; } - // Форматируем дату создания и дату изменения + // Форматируем дату создания и дату изменения (в одну строку) 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 - )}
Изменено: ${formatDate(updatedDate)}`; + )} • Изменено: ${formatDate(updatedDate)}`; } const noteHtml = `
- ${dateDisplay} + ${dateDisplay}
Редактировать
+ }">Ред.
Удалить
+ }">Удал.
" }, { 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)); + } }); // Получаем последний день предыдущего месяца diff --git a/public/index.html b/public/index.html index a1b2dd3..cd3d727 100644 --- a/public/index.html +++ b/public/index.html @@ -8,6 +8,15 @@ /> Вход в систему заметок + + + diff --git a/public/register.html b/public/register.html index b5baa99..8123957 100644 --- a/public/register.html +++ b/public/register.html @@ -8,6 +8,14 @@ /> Регистрация - NoteJS + + { 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()); }