diff --git a/.cursor/rules/rules.mdc b/.cursor/rules/rules.mdc new file mode 100644 index 0000000..e632dc8 --- /dev/null +++ b/.cursor/rules/rules.mdc @@ -0,0 +1,6 @@ +--- +alwaysApply: true +--- + +Не создавай диагностические страницы и документации! +Если добавляешь новые функции или что-то редактируешь в редакторе создания заметок, то добавляй их и в редактор редактирования! diff --git a/.gitignore b/.gitignore index 9379722..2c70b3e 100644 --- a/.gitignore +++ b/.gitignore @@ -34,8 +34,8 @@ Thumbs.db dist/ build/ -# Cursor IDE -.cursor/ +# # Cursor IDE +# .cursor/ # Загруженные файлы пользователей public/uploads/ diff --git a/public/app.js b/public/app.js index 457effc..b31089c 100644 --- a/public/app.js +++ b/public/app.js @@ -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(); } } diff --git a/public/settings.js b/public/settings.js index 1a38835..0c1541a 100644 --- a/public/settings.js +++ b/public/settings.js @@ -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); }; diff --git a/public/style.css b/public/style.css index f33008e..b716763 100644 --- a/public/style.css +++ b/public/style.css @@ -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 { diff --git a/server.js b/server.js index baac0f9..d8cfb15 100644 --- a/server.js +++ b/server.js @@ -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) => {