✨ Добавлена поддержка спойлеров в редактор заметок и улучшены индексы базы данных
- Реализована функция вставки спойлеров в режиме редактирования заметок. - Обновлены стили для кнопок markdown в редакторе. - Добавлен новый индекс для колонки `pinned_at` в таблице `notes`. - Обновлены SQL-запросы для сортировки заметок с учетом нового поля `pinned_at`.
This commit is contained in:
parent
1479205261
commit
372cea2e92
6
.cursor/rules/rules.mdc
Normal file
6
.cursor/rules/rules.mdc
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
---
|
||||||
|
alwaysApply: true
|
||||||
|
---
|
||||||
|
|
||||||
|
Не создавай диагностические страницы и документации!
|
||||||
|
Если добавляешь новые функции или что-то редактируешь в редакторе создания заметок, то добавляй их и в редактор редактирования!
|
||||||
4
.gitignore
vendored
4
.gitignore
vendored
@ -34,8 +34,8 @@ Thumbs.db
|
|||||||
dist/
|
dist/
|
||||||
build/
|
build/
|
||||||
|
|
||||||
# Cursor IDE
|
# # Cursor IDE
|
||||||
.cursor/
|
# .cursor/
|
||||||
|
|
||||||
# Загруженные файлы пользователей
|
# Загруженные файлы пользователей
|
||||||
public/uploads/
|
public/uploads/
|
||||||
|
|||||||
@ -932,6 +932,32 @@ function insertMarkdownForEdit(textarea, tag) {
|
|||||||
textarea.focus();
|
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) ====================
|
// ==================== МУЛЬТИСТРОЧНЫЕ СПИСКИ (TOGGLE) ====================
|
||||||
function transformSelection(textarea, mode) {
|
function transformSelection(textarea, mode) {
|
||||||
const fullText = textarea.value;
|
const fullText = textarea.value;
|
||||||
@ -2118,7 +2144,10 @@ function addNoteEventListeners() {
|
|||||||
|
|
||||||
// Создаем контейнер для markdown кнопок
|
// Создаем контейнер для markdown кнопок
|
||||||
const markdownButtonsContainer = document.createElement("div");
|
const markdownButtonsContainer = document.createElement("div");
|
||||||
markdownButtonsContainer.classList.add("markdown-buttons");
|
markdownButtonsContainer.classList.add(
|
||||||
|
"markdown-buttons",
|
||||||
|
"markdown-buttons--edit"
|
||||||
|
);
|
||||||
|
|
||||||
// Создаем markdown кнопки
|
// Создаем markdown кнопки
|
||||||
const markdownButtons = [
|
const markdownButtons = [
|
||||||
@ -2130,6 +2159,7 @@ function addNoteEventListeners() {
|
|||||||
tag: "~~",
|
tag: "~~",
|
||||||
},
|
},
|
||||||
{ id: "editColorBtn", icon: "mdi:palette", tag: "color" },
|
{ 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: "editHeaderBtn", icon: "mdi:format-header-pound", tag: "header" },
|
||||||
{ id: "editListBtn", icon: "mdi:format-list-bulleted", tag: "- " },
|
{ id: "editListBtn", icon: "mdi:format-list-bulleted", tag: "- " },
|
||||||
{
|
{
|
||||||
@ -2589,6 +2619,9 @@ function addNoteEventListeners() {
|
|||||||
console.error("Ошибка:", error);
|
console.error("Ошибка:", error);
|
||||||
showNotification("Ошибка сохранения заметки", "error");
|
showNotification("Ошибка сохранения заметки", "error");
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
showNotification("Введите текст заметки", "warning");
|
||||||
|
textarea.focus();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -2711,6 +2744,9 @@ function addNoteEventListeners() {
|
|||||||
} else if (button.tag === "color") {
|
} else if (button.tag === "color") {
|
||||||
// Для кнопки цвета открываем диалог выбора цвета
|
// Для кнопки цвета открываем диалог выбора цвета
|
||||||
insertColorTagForEdit(textarea);
|
insertColorTagForEdit(textarea);
|
||||||
|
} else if (button.tag === "spoiler") {
|
||||||
|
// Вставка спойлера в режиме редактирования
|
||||||
|
insertSpoilerForEdit(textarea);
|
||||||
} else if (button.tag === "preview") {
|
} else if (button.tag === "preview") {
|
||||||
// Для кнопки предпросмотра переключаем режим
|
// Для кнопки предпросмотра переключаем режим
|
||||||
isEditPreviewMode = !isEditPreviewMode;
|
isEditPreviewMode = !isEditPreviewMode;
|
||||||
@ -2734,6 +2770,9 @@ function addNoteEventListeners() {
|
|||||||
editPreviewContent
|
editPreviewContent
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Добавляем обработчики спойлеров внутри предпросмотра редактирования
|
||||||
|
addSpoilerEventListeners();
|
||||||
|
|
||||||
// Инициализируем lazy loading для изображений в превью
|
// Инициализируем lazy loading для изображений в превью
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
initLazyLoading();
|
initLazyLoading();
|
||||||
@ -3045,8 +3084,12 @@ function addSpoilerEventListeners() {
|
|||||||
|
|
||||||
// Создаем новый обработчик
|
// Создаем новый обработчик
|
||||||
spoiler._clickHandler = function (event) {
|
spoiler._clickHandler = function (event) {
|
||||||
|
// Если уже раскрыт — не мешаем выделению текста
|
||||||
|
if (this.classList.contains("revealed")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
this.classList.toggle("revealed");
|
this.classList.add("revealed");
|
||||||
console.log("Спойлер кликнут:", this.textContent);
|
console.log("Спойлер кликнут:", this.textContent);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -3179,6 +3222,9 @@ async function saveNote() {
|
|||||||
|
|
||||||
showNotification("Ошибка сохранения заметки", "error");
|
showNotification("Ошибка сохранения заметки", "error");
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
showNotification("Введите текст заметки", "warning");
|
||||||
|
noteInput.focus();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -75,8 +75,12 @@ function addSpoilerEventListeners() {
|
|||||||
|
|
||||||
// Создаем новый обработчик
|
// Создаем новый обработчик
|
||||||
spoiler._clickHandler = function (event) {
|
spoiler._clickHandler = function (event) {
|
||||||
|
// Если уже раскрыт — не мешаем выделению текста
|
||||||
|
if (this.classList.contains("revealed")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
this.classList.toggle("revealed");
|
this.classList.add("revealed");
|
||||||
console.log("Спойлер кликнут:", this.textContent);
|
console.log("Спойлер кликнут:", this.textContent);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -1087,6 +1087,45 @@ textarea:focus {
|
|||||||
position: relative;
|
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 {
|
.markdown-buttons .btnMarkdown {
|
||||||
padding: 5px 10px;
|
padding: 5px 10px;
|
||||||
margin-right: 5px;
|
margin-right: 5px;
|
||||||
@ -3320,6 +3359,9 @@ textarea:focus {
|
|||||||
border-color: #c3e6cb;
|
border-color: #c3e6cb;
|
||||||
box-shadow: 0 0 0 2px rgba(40, 167, 69, 0.25);
|
box-shadow: 0 0 0 2px rgba(40, 167, 69, 0.25);
|
||||||
text-shadow: none;
|
text-shadow: none;
|
||||||
|
user-select: text;
|
||||||
|
-webkit-user-select: text;
|
||||||
|
cursor: text;
|
||||||
}
|
}
|
||||||
|
|
||||||
.spoiler.revealed::before {
|
.spoiler.revealed::before {
|
||||||
|
|||||||
27
server.js
27
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_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_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_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_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_users_username ON users(username)",
|
||||||
"CREATE INDEX IF NOT EXISTS idx_action_logs_user_id ON action_logs(user_id)",
|
"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 hasUpdatedAt = columns.some((col) => col.name === "updated_at");
|
||||||
const hasPinned = columns.some((col) => col.name === "is_pinned");
|
const hasPinned = columns.some((col) => col.name === "is_pinned");
|
||||||
const hasArchived = columns.some((col) => col.name === "is_archived");
|
const hasArchived = columns.some((col) => col.name === "is_archived");
|
||||||
|
const hasPinnedAt = columns.some((col) => col.name === "pinned_at");
|
||||||
|
|
||||||
// Добавляем updated_at если нужно
|
// Добавляем updated_at если нужно
|
||||||
if (!hasUpdatedAt) {
|
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) {
|
if (hasUpdatedAt && hasPinned && hasArchived) {
|
||||||
createIndexes();
|
createIndexes();
|
||||||
@ -870,7 +883,7 @@ app.get("/api/notes/search", requireApiAuth, (req, res) => {
|
|||||||
LEFT JOIN note_images ni ON n.id = ni.note_id
|
LEFT JOIN note_images ni ON n.id = ni.note_id
|
||||||
${whereClause}
|
${whereClause}
|
||||||
GROUP BY n.id
|
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) => {
|
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
|
LEFT JOIN note_images ni ON n.id = ni.note_id
|
||||||
WHERE n.user_id = ? AND n.is_archived = 0
|
WHERE n.user_id = ? AND n.is_archived = 0
|
||||||
GROUP BY n.id
|
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) => {
|
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 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) {
|
if (err) {
|
||||||
console.error("Ошибка изменения закрепления:", err.message);
|
console.error("Ошибка изменения закрепления:", err.message);
|
||||||
return res.status(500).json({ error: "Ошибка сервера" });
|
return res.status(500).json({ error: "Ошибка сервера" });
|
||||||
@ -1563,7 +1578,7 @@ app.put("/api/notes/:id/archive", requireApiAuth, (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const updateSql =
|
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) {
|
db.run(updateSql, [id], function (err) {
|
||||||
if (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
|
LEFT JOIN note_images ni ON n.id = ni.note_id
|
||||||
WHERE n.user_id = ? AND n.is_archived = 1
|
WHERE n.user_id = ? AND n.is_archived = 1
|
||||||
GROUP BY n.id
|
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) => {
|
db.all(sql, [req.session.userId], (err, rows) => {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user