✨ Добавлены функции для работы с многострочными списками и улучшено отображение дат заметок
- Реализована возможность создания нумерованных списков и улучшены функции для работы с многострочными списками. - Обновлены фильтры для отображения заметок по дате, используя поле created_at вместо date. - Оптимизировано отображение дат создания и изменения заметок в единой строке. - Добавлены новые кнопки и обработчики событий для поддержки новых функций в интерфейсе редактирования заметок.
This commit is contained in:
parent
a77bdd3e7b
commit
083ac11ab1
199
public/app.js
199
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 += "</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));
|
||||
}
|
||||
});
|
||||
|
||||
// Получаем последний день предыдущего месяца
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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());
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user