Добавлена поддержка спойлеров в редактор заметок и улучшены индексы базы данных

- Реализована функция вставки спойлеров в режиме редактирования заметок.
- Обновлены стили для кнопок markdown в редакторе.
- Добавлен новый индекс для колонки `pinned_at` в таблице `notes`.
- Обновлены SQL-запросы для сортировки заметок с учетом нового поля `pinned_at`.
This commit is contained in:
Fovway 2025-10-28 21:37:21 +07:00
parent 1479205261
commit 372cea2e92
6 changed files with 124 additions and 11 deletions

6
.cursor/rules/rules.mdc Normal file
View File

@ -0,0 +1,6 @@
---
alwaysApply: true
---
Не создавай диагностические страницы и документации!
Если добавляешь новые функции или что-то редактируешь в редакторе создания заметок, то добавляй их и в редактор редактирования!

4
.gitignore vendored
View File

@ -34,8 +34,8 @@ Thumbs.db
dist/
build/
# Cursor IDE
.cursor/
# # Cursor IDE
# .cursor/
# Загруженные файлы пользователей
public/uploads/

View File

@ -932,6 +932,32 @@ function insertMarkdownForEdit(textarea, tag) {
textarea.focus();
}
// Функция для вставки спойлера в режиме редактирования
function insertSpoilerForEdit(textarea) {
const start = textarea.selectionStart;
const end = textarea.selectionEnd;
const text = textarea.value;
const before = text.substring(0, start);
const selected = text.substring(start, end);
const after = text.substring(end);
let newText;
let newCursorPos;
if (selected) {
newText = before + "||" + selected + "||" + after;
newCursorPos = start + selected.length + 4;
} else {
newText = before + "||скрытый текст||" + after;
newCursorPos = start + 2;
}
textarea.value = newText;
textarea.setSelectionRange(newCursorPos, newCursorPos);
textarea.focus();
}
// ==================== МУЛЬТИСТРОЧНЫЕ СПИСКИ (TOGGLE) ====================
function transformSelection(textarea, mode) {
const fullText = textarea.value;
@ -2118,7 +2144,10 @@ function addNoteEventListeners() {
// Создаем контейнер для markdown кнопок
const markdownButtonsContainer = document.createElement("div");
markdownButtonsContainer.classList.add("markdown-buttons");
markdownButtonsContainer.classList.add(
"markdown-buttons",
"markdown-buttons--edit"
);
// Создаем markdown кнопки
const markdownButtons = [
@ -2130,6 +2159,7 @@ function addNoteEventListeners() {
tag: "~~",
},
{ id: "editColorBtn", icon: "mdi:palette", tag: "color" },
{ id: "editSpoilerBtn", icon: "mdi:eye-off", tag: "spoiler" },
{ id: "editHeaderBtn", icon: "mdi:format-header-pound", tag: "header" },
{ id: "editListBtn", icon: "mdi:format-list-bulleted", tag: "- " },
{
@ -2589,6 +2619,9 @@ function addNoteEventListeners() {
console.error("Ошибка:", error);
showNotification("Ошибка сохранения заметки", "error");
}
} else {
showNotification("Введите текст заметки", "warning");
textarea.focus();
}
};
@ -2711,6 +2744,9 @@ function addNoteEventListeners() {
} else if (button.tag === "color") {
// Для кнопки цвета открываем диалог выбора цвета
insertColorTagForEdit(textarea);
} else if (button.tag === "spoiler") {
// Вставка спойлера в режиме редактирования
insertSpoilerForEdit(textarea);
} else if (button.tag === "preview") {
// Для кнопки предпросмотра переключаем режим
isEditPreviewMode = !isEditPreviewMode;
@ -2734,6 +2770,9 @@ function addNoteEventListeners() {
editPreviewContent
);
// Добавляем обработчики спойлеров внутри предпросмотра редактирования
addSpoilerEventListeners();
// Инициализируем lazy loading для изображений в превью
setTimeout(() => {
initLazyLoading();
@ -3045,8 +3084,12 @@ function addSpoilerEventListeners() {
// Создаем новый обработчик
spoiler._clickHandler = function (event) {
// Если уже раскрыт — не мешаем выделению текста
if (this.classList.contains("revealed")) {
return;
}
event.stopPropagation();
this.classList.toggle("revealed");
this.classList.add("revealed");
console.log("Спойлер кликнут:", this.textContent);
};
@ -3179,6 +3222,9 @@ async function saveNote() {
showNotification("Ошибка сохранения заметки", "error");
}
} else {
showNotification("Введите текст заметки", "warning");
noteInput.focus();
}
}

View File

@ -75,8 +75,12 @@ function addSpoilerEventListeners() {
// Создаем новый обработчик
spoiler._clickHandler = function (event) {
// Если уже раскрыт — не мешаем выделению текста
if (this.classList.contains("revealed")) {
return;
}
event.stopPropagation();
this.classList.toggle("revealed");
this.classList.add("revealed");
console.log("Спойлер кликнут:", this.textContent);
};

View File

@ -1087,6 +1087,45 @@ textarea:focus {
position: relative;
}
/* Кнопки markdown в редакторе редактирования: те же отступы и поведение */
.markdown-buttons.markdown-buttons--edit {
display: flex;
flex-wrap: wrap;
gap: 5px;
}
.markdown-buttons.markdown-buttons--edit .btnMarkdown {
padding: 5px 10px;
margin-right: 5px;
border: 1px solid var(--border-secondary);
background-color: var(--bg-tertiary);
color: var(--text-primary);
border-radius: 5px;
font-size: 14px;
touch-action: manipulation;
-webkit-tap-highlight-color: transparent;
user-select: none;
-webkit-user-select: none;
min-height: 44px;
display: inline-flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
}
.markdown-buttons.markdown-buttons--edit .btnMarkdown:hover {
background-color: var(--bg-quaternary);
border-color: var(--border-focus);
}
@media (max-width: 768px) {
.markdown-buttons.markdown-buttons--edit .btnMarkdown {
min-height: 48px;
padding: 8px 12px;
margin: 2px;
}
}
.markdown-buttons .btnMarkdown {
padding: 5px 10px;
margin-right: 5px;
@ -3320,6 +3359,9 @@ textarea:focus {
border-color: #c3e6cb;
box-shadow: 0 0 0 2px rgba(40, 167, 69, 0.25);
text-shadow: none;
user-select: text;
-webkit-user-select: text;
cursor: text;
}
.spoiler.revealed::before {

View File

@ -266,6 +266,7 @@ function createIndexes() {
"CREATE INDEX IF NOT EXISTS idx_notes_date ON notes(date)",
"CREATE INDEX IF NOT EXISTS idx_notes_is_pinned ON notes(is_pinned)",
"CREATE INDEX IF NOT EXISTS idx_notes_is_archived ON notes(is_archived)",
"CREATE INDEX IF NOT EXISTS idx_notes_pinned_at ON notes(pinned_at)",
"CREATE INDEX IF NOT EXISTS idx_note_images_note_id ON note_images(note_id)",
"CREATE INDEX IF NOT EXISTS idx_users_username ON users(username)",
"CREATE INDEX IF NOT EXISTS idx_action_logs_user_id ON action_logs(user_id)",
@ -510,6 +511,7 @@ function runMigrations() {
const hasUpdatedAt = columns.some((col) => col.name === "updated_at");
const hasPinned = columns.some((col) => col.name === "is_pinned");
const hasArchived = columns.some((col) => col.name === "is_archived");
const hasPinnedAt = columns.some((col) => col.name === "pinned_at");
// Добавляем updated_at если нужно
if (!hasUpdatedAt) {
@ -564,6 +566,17 @@ function runMigrations() {
);
}
// Добавляем pinned_at если нужно
if (!hasPinnedAt) {
db.run("ALTER TABLE notes ADD COLUMN pinned_at DATETIME", (err) => {
if (err) {
console.error("Ошибка добавления колонки pinned_at:", err.message);
} else {
console.log("Колонка pinned_at добавлена в таблицу notes");
}
});
}
// Создаем индексы после всех изменений
if (hasUpdatedAt && hasPinned && hasArchived) {
createIndexes();
@ -870,7 +883,7 @@ app.get("/api/notes/search", requireApiAuth, (req, res) => {
LEFT JOIN note_images ni ON n.id = ni.note_id
${whereClause}
GROUP BY n.id
ORDER BY n.created_at DESC
ORDER BY n.is_pinned DESC, n.pinned_at DESC, n.created_at DESC
`;
db.all(sql, params, (err, rows) => {
@ -912,7 +925,7 @@ app.get("/api/notes", requireApiAuth, (req, res) => {
LEFT JOIN note_images ni ON n.id = ni.note_id
WHERE n.user_id = ? AND n.is_archived = 0
GROUP BY n.id
ORDER BY n.is_pinned DESC, n.created_at DESC
ORDER BY n.is_pinned DESC, n.pinned_at DESC, n.created_at DESC
`;
db.all(sql, [req.session.userId], (err, rows) => {
@ -1519,9 +1532,11 @@ app.put("/api/notes/:id/pin", requireApiAuth, (req, res) => {
}
const newPinState = row.is_pinned ? 0 : 1;
const updateSql = "UPDATE notes SET is_pinned = ? WHERE id = ?";
const updateSql = newPinState
? "UPDATE notes SET is_pinned = 1, pinned_at = CURRENT_TIMESTAMP WHERE id = ?"
: "UPDATE notes SET is_pinned = 0, pinned_at = NULL WHERE id = ?";
db.run(updateSql, [newPinState, id], function (err) {
db.run(updateSql, [id], function (err) {
if (err) {
console.error("Ошибка изменения закрепления:", err.message);
return res.status(500).json({ error: "Ошибка сервера" });
@ -1563,7 +1578,7 @@ app.put("/api/notes/:id/archive", requireApiAuth, (req, res) => {
}
const updateSql =
"UPDATE notes SET is_archived = 1, is_pinned = 0 WHERE id = ?";
"UPDATE notes SET is_archived = 1, is_pinned = 0, pinned_at = NULL WHERE id = ?";
db.run(updateSql, [id], function (err) {
if (err) {
@ -1650,7 +1665,7 @@ app.get("/api/notes/archived", requireApiAuth, (req, res) => {
LEFT JOIN note_images ni ON n.id = ni.note_id
WHERE n.user_id = ? AND n.is_archived = 1
GROUP BY n.id
ORDER BY n.created_at DESC
ORDER BY n.is_pinned DESC, n.pinned_at DESC, n.created_at DESC
`;
db.all(sql, [req.session.userId], (err, rows) => {