NoteJS/public/app.js
Fovway 092c01dff4 Добавлены индексы для оптимизации запросов и улучшена обработка заметок с изображениями
- Реализована функция создания индексов для таблиц в базе данных, что улучшает производительность запросов
- Обновлены API для получения и поиска заметок, теперь они возвращают изображения, связанные с заметками
- Добавлен кэш для заметок на клиенте с возможностью принудительной перезагрузки
- Внедрен индикатор загрузки при загрузке заметок для улучшения пользовательского опыта
2025-10-20 07:24:31 +07:00

1820 lines
64 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// DOM элементы
const noteInput = document.getElementById("noteInput");
const saveBtn = document.getElementById("saveBtn");
const notesList = document.getElementById("notes-container");
// Получаем кнопки markdown
const boldBtn = document.getElementById("boldBtn");
const italicBtn = document.getElementById("italicBtn");
const headerBtn = document.getElementById("headerBtn");
const listBtn = document.getElementById("listBtn");
const quoteBtn = document.getElementById("quoteBtn");
const codeBtn = document.getElementById("codeBtn");
const linkBtn = document.getElementById("linkBtn");
const imageBtn = document.getElementById("imageBtn");
// Элементы для загрузки изображений
const imageInput = document.getElementById("imageInput");
const imagePreviewContainer = document.getElementById("imagePreviewContainer");
const imagePreviewList = document.getElementById("imagePreviewList");
const clearImagesBtn = document.getElementById("clearImagesBtn");
// Модальное окно для просмотра изображений
const imageModal = document.getElementById("imageModal");
const modalImage = document.getElementById("modalImage");
const modalClose = document.querySelector(".image-modal-close");
// Массив для хранения выбранных изображений
let selectedImages = [];
// Глобальные переменные для заметок и фильтрации
let allNotes = [];
let selectedDateFilter = null;
let selectedTagFilter = null;
let searchQuery = "";
let searchResults = [];
let notesCache = null; // Кэш для заметок
let lastLoadTime = 0; // Время последней загрузки
// Функция для получения текущей даты и времени
function getFormattedDateTime() {
let now = new Date();
let day = String(now.getDate()).padStart(2, "0");
let month = String(now.getMonth() + 1).padStart(2, "0");
let year = now.getFullYear();
let hours = String(now.getHours()).padStart(2, "0");
let minutes = String(now.getMinutes()).padStart(2, "0");
return {
date: `${day}.${month}.${year}`,
time: `${hours}:${minutes}`,
};
}
// Функция для авторасширения текстового поля
function autoExpandTextarea(textarea) {
textarea.style.height = "auto";
textarea.style.height = textarea.scrollHeight + "px";
}
// Функция для извлечения тегов из текста заметки
function extractTags(content) {
const tagRegex = /#([а-яё\w]+)/gi;
const tags = [];
let match;
while ((match = tagRegex.exec(content)) !== null) {
const tag = match[1].toLowerCase();
if (!tags.includes(tag)) {
tags.push(tag);
}
}
return tags;
}
// Функция для преобразования тегов в заметках в кликабельные элементы
function makeTagsClickable(content) {
// Сначала находим все теги, которые еще не обернуты в HTML
const tagRegex = /#([а-яё\w]+)/gi;
let result = content;
let match;
// Создаем массив всех совпадений с их позициями
const matches = [];
while ((match = tagRegex.exec(content)) !== null) {
matches.push({
fullMatch: match[0],
tag: match[1],
index: match.index,
});
}
// Обрабатываем совпадения в обратном порядке, чтобы не сбить индексы
for (let i = matches.length - 1; i >= 0; i--) {
const match = matches[i];
const beforeTag = result.substring(0, match.index);
const afterTag = result.substring(match.index + match.fullMatch.length);
// Проверяем, не находится ли тег уже внутри HTML-тега
const lastOpenTag = beforeTag.lastIndexOf("<");
const lastCloseTag = beforeTag.lastIndexOf(">");
// Если последний открывающий тег идет после последнего закрывающего, значит мы внутри HTML-тега
if (lastOpenTag > lastCloseTag) {
continue; // Пропускаем этот тег
}
// Заменяем тег на кликабельный элемент
const replacement = `<span class="tag-in-note" data-tag="${match.tag}">${match.fullMatch}</span>`;
result = beforeTag + replacement + afterTag;
}
return result;
}
// Функция для получения всех уникальных тегов из заметок
function getAllTags(notes) {
const tagCounts = {};
notes.forEach((note) => {
const tags = extractTags(note.content);
tags.forEach((tag) => {
tagCounts[tag] = (tagCounts[tag] || 0) + 1;
});
});
return tagCounts;
}
// Функция для отображения тегов
function renderTags() {
const tagsContainer = document.getElementById("tagsContainer");
if (!tagsContainer) return;
const tagCounts = getAllTags(allNotes);
const sortedTags = Object.keys(tagCounts).sort();
if (sortedTags.length === 0) {
tagsContainer.innerHTML =
'<div style="font-size: 10px; color: #999; text-align: center;">Нет тегов</div>';
return;
}
tagsContainer.innerHTML = sortedTags
.map((tag) => {
const count = tagCounts[tag];
const isActive = selectedTagFilter === tag ? "active" : "";
return `<span class="tag ${isActive}" data-tag="${tag}">#${tag}<span class="tag-count">${count}</span></span>`;
})
.join("");
// Добавляем обработчики кликов для тегов
tagsContainer.querySelectorAll(".tag").forEach((tagElement) => {
tagElement.addEventListener("click", async (event) => await handleTagClick(event));
});
}
// Обработчик клика на тег
async function handleTagClick(event) {
const clickedTag = event.target.closest(".tag").dataset.tag;
// Если кликнули на тот же тег, снимаем фильтр
if (selectedTagFilter === clickedTag) {
selectedTagFilter = null;
} else {
selectedTagFilter = clickedTag;
}
// Перерисовываем заметки и теги
await renderNotes(allNotes);
renderTags();
updateFilterIndicator();
}
// Привязываем авторасширение к текстовому полю для создания заметки
noteInput.addEventListener("input", function () {
autoExpandTextarea(noteInput);
});
// Изначально запускаем для установки правильной высоты
autoExpandTextarea(noteInput);
// Функция для вставки markdown
function insertMarkdown(tag) {
const start = noteInput.selectionStart;
const end = noteInput.selectionEnd;
const text = noteInput.value;
const before = text.substring(0, start);
const selected = text.substring(start, end);
const after = text.substring(end);
if (selected.startsWith(tag) && selected.endsWith(tag)) {
// Если теги уже есть, удаляем их
noteInput.value = `${before}${selected.slice(
tag.length,
-tag.length
)}${after}`;
noteInput.setSelectionRange(start, end - 2 * tag.length);
} else if (selected.trim() === "") {
// Если текст не выделен
if (tag === "[Текст ссылки](URL)") {
// Для ссылок создаем шаблон с двумя кавычками
noteInput.value = `${before}[Текст ссылки](URL)${after}`;
const cursorPosition = start + 1; // Помещаем курсор внутрь текста ссылки
noteInput.setSelectionRange(cursorPosition, cursorPosition + 12);
} else if (tag === "- " || tag === "> " || tag === "# ") {
// Для списка, цитаты и заголовка помещаем курсор после `- `, `> ` или `# `
noteInput.value = `${before}${tag}${after}`;
const cursorPosition = start + tag.length;
noteInput.setSelectionRange(cursorPosition, cursorPosition);
} else {
// Для остальных типов создаем два тега
noteInput.value = `${before}${tag}${tag}${after}`;
const cursorPosition = start + tag.length;
noteInput.setSelectionRange(cursorPosition, cursorPosition);
}
} else {
// Если текст выделен
if (tag === "[Текст ссылки](URL)") {
// Для ссылок используем выделенный текст вместо "Текст ссылки"
noteInput.value = `${before}[${selected}](URL)${after}`;
const cursorPosition = start + selected.length + 3; // Помещаем курсор в URL
noteInput.setSelectionRange(cursorPosition, cursorPosition + 3);
} else if (tag === "- " || tag === "> " || tag === "# ") {
// Для списка, цитаты и заголовка добавляем `- `, `> ` или `# ` перед выделенным текстом
noteInput.value = `${before}${tag}${selected}${after}`;
const cursorPosition = start + tag.length + selected.length;
noteInput.setSelectionRange(cursorPosition, cursorPosition);
} else {
// Для остальных типов оборачиваем выделенный текст
noteInput.value = `${before}${tag}${selected}${tag}${after}`;
const cursorPosition = start + tag.length + selected.length + tag.length;
noteInput.setSelectionRange(cursorPosition, cursorPosition);
}
}
noteInput.focus();
}
// Функция для вставки markdown в режиме редактирования
function insertMarkdownForEdit(textarea, tag) {
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);
if (selected.startsWith(tag) && selected.endsWith(tag)) {
// Если теги уже есть, удаляем их
textarea.value = `${before}${selected.slice(
tag.length,
-tag.length
)}${after}`;
textarea.setSelectionRange(start, end - 2 * tag.length);
} else if (selected.trim() === "") {
// Если текст не выделен
if (tag === "[Текст ссылки](URL)") {
// Для ссылок создаем шаблон с двумя кавычками
textarea.value = `${before}[Текст ссылки](URL)${after}`;
const cursorPosition = start + 1; // Помещаем курсор внутрь текста ссылки
textarea.setSelectionRange(cursorPosition, cursorPosition + 12);
} else if (tag === "- " || tag === "> " || tag === "# ") {
// Для списка, цитаты и заголовка помещаем курсор после `- `, `> ` или `# `
textarea.value = `${before}${tag}${after}`;
const cursorPosition = start + tag.length;
textarea.setSelectionRange(cursorPosition, cursorPosition);
} else {
// Для остальных типов создаем два тега
textarea.value = `${before}${tag}${tag}${after}`;
const cursorPosition = start + tag.length;
textarea.setSelectionRange(cursorPosition, cursorPosition);
}
} else {
// Если текст выделен
if (tag === "[Текст ссылки](URL)") {
// Для ссылок используем выделенный текст вместо "Текст ссылки"
textarea.value = `${before}[${selected}](URL)${after}`;
const cursorPosition = start + selected.length + 3; // Помещаем курсор в URL
textarea.setSelectionRange(cursorPosition, cursorPosition + 3);
} else if (tag === "- " || tag === "> " || tag === "# ") {
// Для списка, цитаты и заголовка добавляем `- `, `> ` или `# ` перед выделенным текстом
textarea.value = `${before}${tag}${selected}${after}`;
const cursorPosition = start + tag.length + selected.length;
textarea.setSelectionRange(cursorPosition, cursorPosition);
} else {
// Для остальных типов оборачиваем выделенный текст
textarea.value = `${before}${tag}${selected}${tag}${after}`;
const cursorPosition = start + tag.length + selected.length + tag.length;
textarea.setSelectionRange(cursorPosition, cursorPosition);
}
}
textarea.focus();
}
// Обработчики для кнопок markdown
boldBtn.addEventListener("click", function () {
insertMarkdown("**");
});
italicBtn.addEventListener("click", function () {
insertMarkdown("*");
});
headerBtn.addEventListener("click", function () {
insertMarkdown("# ");
});
listBtn.addEventListener("click", function () {
insertMarkdown("- ");
});
quoteBtn.addEventListener("click", function () {
insertMarkdown("> ");
});
codeBtn.addEventListener("click", function () {
insertMarkdown("`");
});
linkBtn.addEventListener("click", function () {
insertMarkdown("[Текст ссылки](URL)");
});
// Обработчик для кнопки загрузки изображений
imageBtn.addEventListener("click", function () {
imageInput.click();
});
// Обработчик выбора файлов
imageInput.addEventListener("change", function (event) {
const files = Array.from(event.target.files);
files.forEach(file => {
if (file.type.startsWith('image/')) {
selectedImages.push(file);
}
});
updateImagePreview();
});
// Обработчик очистки всех изображений
clearImagesBtn.addEventListener("click", function () {
selectedImages = [];
updateImagePreview();
imageInput.value = "";
});
// Обработчики модального окна
modalClose.addEventListener("click", function () {
imageModal.style.display = "none";
});
imageModal.addEventListener("click", function (event) {
if (event.target === imageModal) {
imageModal.style.display = "none";
}
});
// Закрытие модального окна по Escape
document.addEventListener("keydown", function (event) {
if (event.key === "Escape" && imageModal.style.display === "block") {
imageModal.style.display = "none";
}
});
// Функция для обновления превью изображений
function updateImagePreview() {
if (selectedImages.length === 0) {
imagePreviewContainer.style.display = "none";
return;
}
imagePreviewContainer.style.display = "block";
imagePreviewList.innerHTML = "";
selectedImages.forEach((file, index) => {
const reader = new FileReader();
reader.onload = function (e) {
const previewItem = document.createElement("div");
previewItem.className = "image-preview-item";
previewItem.innerHTML = `
<img src="${e.target.result}" alt="Preview">
<button class="remove-image-btn" data-index="${index}">×</button>
<div class="image-info">${file.name}</div>
`;
imagePreviewList.appendChild(previewItem);
// Обработчик удаления изображения
const removeBtn = previewItem.querySelector(".remove-image-btn");
removeBtn.addEventListener("click", function () {
selectedImages.splice(index, 1);
updateImagePreview();
});
};
reader.readAsDataURL(file);
});
}
// Функция для отображения изображения в модальном окне
function showImageModal(imageSrc) {
console.log('showImageModal called with:', imageSrc);
try {
modalImage.src = imageSrc;
imageModal.style.display = "block";
console.log('Modal opened successfully');
} catch (error) {
console.error('Error in showImageModal:', error);
}
}
// Функция для загрузки изображений на сервер
async function uploadImages(noteId) {
if (selectedImages.length === 0) {
return [];
}
const formData = new FormData();
selectedImages.forEach(file => {
formData.append("images", file);
});
try {
const response = await fetch(`/api/notes/${noteId}/images`, {
method: "POST",
body: formData,
});
if (!response.ok) {
throw new Error("Ошибка загрузки изображений");
}
const result = await response.json();
return result.images || [];
} catch (error) {
console.error("Ошибка загрузки изображений:", error);
return [];
}
}
// Функция для получения изображений заметки
async function getNoteImages(noteId) {
try {
const response = await fetch(`/api/notes/${noteId}/images`);
if (!response.ok) {
throw new Error("Ошибка получения изображений");
}
return await response.json();
} catch (error) {
console.error("Ошибка получения изображений:", error);
return [];
}
}
// Функция для удаления изображения заметки
async function deleteNoteImage(noteId, imageId) {
try {
const response = await fetch(`/api/notes/${noteId}/images/${imageId}`, {
method: "DELETE",
});
if (!response.ok) {
throw new Error("Ошибка удаления изображения");
}
return true;
} catch (error) {
console.error("Ошибка удаления изображения:", error);
return false;
}
}
// Функция для загрузки заметок с сервера
async function loadNotes(forceReload = false) {
const now = Date.now();
const CACHE_DURATION = 30000; // 30 секунд кэширования
// Используем кэш, если он не устарел и не требуется принудительная перезагрузка
if (!forceReload && notesCache && (now - lastLoadTime) < CACHE_DURATION) {
allNotes = notesCache;
await renderNotes(notesCache);
renderCalendar();
renderTags();
renderCalendarMobile();
renderTagsMobile();
return;
}
// Показываем индикатор загрузки
showLoadingIndicator();
try {
const response = await fetch("/api/notes");
if (!response.ok) {
throw new Error("Ошибка загрузки заметок");
}
const notes = await response.json();
allNotes = notes; // Сохраняем все заметки в глобальную переменную
notesCache = notes; // Сохраняем в кэш
lastLoadTime = now;
await renderNotes(notes);
renderCalendar(); // Обновляем календарь после загрузки заметок
renderTags(); // Обновляем теги после загрузки заметок
renderCalendarMobile(); // Обновляем мобильный календарь после загрузки заметок
renderTagsMobile(); // Обновляем мобильные теги после загрузки заметок
} catch (error) {
console.error("Ошибка:", error);
notesList.innerHTML = "<p>Ошибка загрузки заметок</p>";
} finally {
// Скрываем индикатор загрузки
hideLoadingIndicator();
}
}
// Функция для показа индикатора загрузки
function showLoadingIndicator() {
if (!document.getElementById("loading-indicator")) {
const loadingDiv = document.createElement("div");
loadingDiv.id = "loading-indicator";
loadingDiv.innerHTML = `
<div style="
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: rgba(0, 0, 0, 0.8);
color: white;
padding: 20px;
border-radius: 10px;
z-index: 10000;
font-size: 16px;
">
<div style="text-align: center;">
<div style="
width: 40px;
height: 40px;
border: 4px solid #f3f3f3;
border-top: 4px solid #007bff;
border-radius: 50%;
animation: spin 1s linear infinite;
margin: 0 auto 10px;
"></div>
Загрузка заметок...
</div>
</div>
<style>
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
</style>
`;
document.body.appendChild(loadingDiv);
}
}
// Функция для скрытия индикатора загрузки
function hideLoadingIndicator() {
const loadingIndicator = document.getElementById("loading-indicator");
if (loadingIndicator) {
loadingIndicator.remove();
}
}
// Функция для поиска заметок
async function searchNotes(query) {
if (!query || query.trim() === "") {
searchQuery = "";
searchResults = [];
await renderNotes(allNotes);
return;
}
try {
const params = new URLSearchParams();
params.append("q", query.trim());
// Добавляем фильтры, если они активны
if (selectedTagFilter) {
params.append("tag", selectedTagFilter);
}
if (selectedDateFilter) {
params.append("date", selectedDateFilter);
}
const response = await fetch(`/api/notes/search?${params}`);
if (!response.ok) {
throw new Error("Ошибка поиска заметок");
}
searchResults = await response.json();
searchQuery = query.trim();
await renderNotes(searchResults);
} catch (error) {
console.error("Ошибка поиска:", error);
searchResults = [];
await renderNotes(allNotes);
}
}
// Функция для подсветки найденного текста
function highlightSearchText(content, query) {
if (!query || query.trim() === "") {
return content;
}
const regex = new RegExp(
`(${query.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")})`,
"gi"
);
return content.replace(regex, '<span class="search-highlight">$1</span>');
}
// Функция для отображения заметок
async function renderNotes(notes) {
notesList.innerHTML = "";
// Фильтруем заметки по дате и тегам
let notesToDisplay = notes;
if (selectedDateFilter) {
notesToDisplay = notesToDisplay.filter(
(note) => note.date === selectedDateFilter
);
}
if (selectedTagFilter) {
notesToDisplay = notesToDisplay.filter((note) => {
const tags = extractTags(note.content);
return tags.includes(selectedTagFilter);
});
}
// Если нет заметок для отображения
if (notesToDisplay.length === 0) {
let message = "Заметок пока нет";
if (selectedDateFilter && selectedTagFilter) {
message = `Нет заметок за ${selectedDateFilter} с тегом #${selectedTagFilter}`;
} else if (selectedDateFilter) {
message = `Нет заметок за выбранную дату (${selectedDateFilter})`;
} else if (selectedTagFilter) {
message = `Нет заметок с тегом #${selectedTagFilter}`;
}
notesList.innerHTML = `<div class="container"><p style="text-align: center; color: #999;">${message}</p></div>`;
return;
}
// Итерируемся по заметкам в обычном порядке, чтобы новые были сверху
for (const note of notesToDisplay) {
let contentToProcess = note.content;
// Сначала подсвечиваем найденный текст в исходном markdown
if (searchQuery) {
contentToProcess = highlightSearchText(contentToProcess, searchQuery);
}
// Затем преобразуем теги в кликабельные элементы
const contentWithClickableTags = makeTagsClickable(contentToProcess);
const parsedContent = marked.parse(contentWithClickableTags);
// Используем изображения, которые уже пришли с заметкой
const noteImages = Array.isArray(note.images) ? note.images : [];
let imagesHtml = "";
if (noteImages.length > 0) {
imagesHtml = '<div class="note-images-container">';
noteImages.forEach(image => {
imagesHtml += `
<div class="note-image-item">
<img src="${image.file_path}" alt="${image.original_name}" class="note-image" data-image-src="${image.file_path}">
<button class="remove-note-image-btn" data-note-id="${note.id}" data-image-id="${image.id}" title="Удалить изображение">×</button>
</div>
`;
});
imagesHtml += '</div>';
}
const noteHtml = `
<div id="note" class="container" data-note-id="${note.id}">
<div class="date">
${note.date} ${note.time}
<div id="editBtn" class="notesHeaderBtn" data-id="${
note.id
}">Редактировать</div>
<div id="deleteBtn" class="notesHeaderBtn" data-id="${
note.id
}">Удалить</div>
</div>
<div class="textNote" data-original-content="${note.content.replace(
/"/g,
"&quot;"
)}">${parsedContent}</div>
${imagesHtml}
</div>
`;
notesList.insertAdjacentHTML("afterbegin", noteHtml);
}
// Добавляем обработчики событий для кнопок редактирования и удаления
addNoteEventListeners();
// Добавляем обработчики кликов для тегов в заметках
addTagClickListeners();
// Добавляем обработчики для изображений в заметках
addImageEventListeners();
// Обрабатываем длинные заметки
handleLongNotes();
}
// Функция для обработки длинных заметок
function handleLongNotes() {
const MAX_HEIGHT = 300; // Максимальная высота в пикселях
document.querySelectorAll(".textNote").forEach((noteElement) => {
// Проверяем высоту контента
const contentHeight = noteElement.scrollHeight;
if (contentHeight > MAX_HEIGHT) {
// Добавляем класс для сворачивания
noteElement.classList.add("collapsed");
// Создаем кнопку "Показать все"
const showMoreBtn = document.createElement("button");
showMoreBtn.classList.add("show-more-btn");
showMoreBtn.textContent = "Показать полностью";
showMoreBtn.setAttribute("data-expanded", "false");
// Вставляем кнопку после заметки
noteElement.parentElement.insertBefore(
showMoreBtn,
noteElement.nextSibling
);
// Обработчик клика на кнопку
showMoreBtn.addEventListener("click", function () {
const isExpanded = this.getAttribute("data-expanded") === "true";
if (isExpanded) {
// Сворачиваем
noteElement.classList.add("collapsed");
this.textContent = "Показать полностью";
this.setAttribute("data-expanded", "false");
} else {
// Разворачиваем
noteElement.classList.remove("collapsed");
this.textContent = "Свернуть";
this.setAttribute("data-expanded", "true");
}
});
}
});
}
// Функция для добавления обработчиков событий к заметкам
function addNoteEventListeners() {
// Обработчик удаления
document.querySelectorAll("#deleteBtn").forEach((btn) => {
btn.addEventListener("click", async function (event) {
const noteId = event.target.dataset.id;
if (confirm("Вы уверены, что хотите удалить эту заметку?")) {
try {
const response = await fetch(`/api/notes/${noteId}`, {
method: "DELETE",
});
if (!response.ok) {
throw new Error("Ошибка удаления заметки");
}
// Перезагружаем заметки
await loadNotes(true);
} catch (error) {
console.error("Ошибка:", error);
alert("Ошибка удаления заметки");
}
}
});
});
// Обработчик редактирования
document.querySelectorAll("#editBtn").forEach((btn) => {
btn.addEventListener("click", function (event) {
const noteId = event.target.dataset.id;
const noteContainer = event.target.closest("#note");
const noteContent = noteContainer.querySelector(".textNote");
// Разворачиваем заметку при редактировании
noteContent.classList.remove("collapsed");
// Скрываем кнопку "Показать полностью" если она есть
const showMoreBtn = noteContainer.querySelector(".show-more-btn");
if (showMoreBtn) {
showMoreBtn.style.display = "none";
}
// Создаем контейнер для markdown кнопок
const markdownButtonsContainer = document.createElement("div");
markdownButtonsContainer.classList.add("markdown-buttons");
// Создаем markdown кнопки
const markdownButtons = [
{ id: "editBoldBtn", icon: "mdi:format-bold", tag: "**" },
{ id: "editItalicBtn", icon: "mdi:format-italic", tag: "*" },
{ id: "editHeaderBtn", icon: "mdi:format-header-1", tag: "# " },
{ id: "editListBtn", icon: "mdi:format-list-bulleted", tag: "- " },
{ id: "editQuoteBtn", icon: "mdi:format-quote-close", tag: "> " },
{ id: "editCodeBtn", icon: "mdi:code-tags", tag: "`" },
{ id: "editLinkBtn", icon: "mdi:link", tag: "[Текст ссылки](URL)" },
];
markdownButtons.forEach((button) => {
const btn = document.createElement("button");
btn.classList.add("btnMarkdown");
btn.id = button.id;
btn.innerHTML = `<span class="iconify" data-icon="${button.icon}"></span>`;
markdownButtonsContainer.appendChild(btn);
});
// Создаем textarea с уже существующим классом textInput
const textarea = document.createElement("textarea");
textarea.classList.add("textInput");
// Получаем исходный markdown контент из data-атрибута или используем textContent как fallback
textarea.value =
noteContent.dataset.originalContent || noteContent.textContent;
// Привязываем авторасширение к textarea для редактирования
textarea.addEventListener("input", function () {
autoExpandTextarea(textarea);
});
// Контейнер для кнопки сохранения и подсказки
const saveButtonContainer = document.createElement("div");
saveButtonContainer.classList.add("save-button-container");
// Кнопка сохранить
const saveEditBtn = document.createElement("button");
saveEditBtn.textContent = "Сохранить";
saveEditBtn.classList.add("btnSave");
// Подсказка о горячей клавише
const saveHint = document.createElement("span");
saveHint.classList.add("save-hint");
saveHint.textContent = "или нажмите Alt + Enter";
saveButtonContainer.appendChild(saveEditBtn);
saveButtonContainer.appendChild(saveHint);
// Функция сохранения для редактирования
const saveEditNote = async function () {
if (textarea.value.trim() !== "") {
try {
const { date, time } = getFormattedDateTime();
const response = await fetch(`/api/notes/${noteId}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
content: textarea.value,
date: date,
time: time,
}),
});
if (!response.ok) {
throw new Error("Ошибка сохранения заметки");
}
// Перезагружаем заметки
await loadNotes(true);
} catch (error) {
console.error("Ошибка:", error);
alert("Ошибка сохранения заметки");
}
}
};
// Обработчик горячей клавиши Alt+Enter для сохранения редактирования
textarea.addEventListener("keydown", function (event) {
if (event.altKey && event.key === "Enter") {
event.preventDefault();
saveEditNote();
}
});
// Очищаем текущий контент и вставляем markdown кнопки, textarea и контейнер с кнопкой сохранить
noteContent.innerHTML = "";
noteContent.appendChild(markdownButtonsContainer);
noteContent.appendChild(textarea);
noteContent.appendChild(saveButtonContainer);
// Применяем авторасширение после добавления в DOM
setTimeout(() => {
autoExpandTextarea(textarea);
textarea.focus();
}, 0);
// Добавляем обработчики для markdown кнопок редактирования
markdownButtons.forEach((button) => {
const btn = document.getElementById(button.id);
btn.addEventListener("click", function () {
insertMarkdownForEdit(textarea, button.tag);
});
});
// Обработчик сохранения редактирования
saveEditBtn.addEventListener("click", saveEditNote);
});
});
}
// Функция для добавления обработчиков кликов на теги в заметках
function addTagClickListeners() {
document.querySelectorAll(".textNote .tag-in-note").forEach((tagElement) => {
tagElement.addEventListener("click", async function (event) {
event.preventDefault();
event.stopPropagation();
const clickedTag = this.dataset.tag.toLowerCase();
// Если кликнули на тот же тег, снимаем фильтр
if (selectedTagFilter === clickedTag) {
selectedTagFilter = null;
} else {
selectedTagFilter = clickedTag;
}
// Перерисовываем заметки и теги
await renderNotes(allNotes);
renderTags();
updateFilterIndicator();
});
});
}
// Функция для добавления обработчиков для изображений в заметках
function addImageEventListeners() {
// Обработчики для кликов на изображения (открытие в модальном окне)
document.querySelectorAll(".note-image").forEach((imageElement) => {
// Проверяем, не добавлен ли уже обработчик
if (imageElement._clickHandler) {
return; // Пропускаем, если обработчик уже добавлен
}
// Создаем новый обработчик
imageElement._clickHandler = function (event) {
event.preventDefault();
event.stopPropagation();
const imageSrc = this.dataset.imageSrc;
console.log('Image clicked, src:', imageSrc); // Для отладки
if (imageSrc) {
showImageModal(imageSrc);
}
};
imageElement.addEventListener("click", imageElement._clickHandler);
});
// Обработчики для кнопок удаления изображений
document.querySelectorAll(".remove-note-image-btn").forEach((buttonElement) => {
// Удаляем старые обработчики, если они есть
if (buttonElement._clickHandler) {
buttonElement.removeEventListener("click", buttonElement._clickHandler);
}
// Создаем новый обработчик
buttonElement._clickHandler = async function (event) {
event.preventDefault();
event.stopPropagation();
const noteId = this.dataset.noteId;
const imageId = this.dataset.imageId;
if (noteId && imageId && confirm("Вы уверены, что хотите удалить это изображение?")) {
const success = await deleteNoteImage(noteId, imageId);
if (success) {
await loadNotes(true); // Перезагружаем заметки
} else {
alert("Ошибка удаления изображения");
}
}
};
buttonElement.addEventListener("click", buttonElement._clickHandler);
});
}
// Функция сохранения заметки (вынесена отдельно для повторного использования)
async function saveNote() {
if (noteInput.value.trim() !== "" || selectedImages.length > 0) {
try {
const { date, time } = getFormattedDateTime();
const response = await fetch("/api/notes", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
content: noteInput.value || " ", // Минимальный контент, если только изображения
date: date,
time: time,
}),
});
if (!response.ok) {
throw new Error("Ошибка сохранения заметки");
}
const noteData = await response.json();
const noteId = noteData.id;
// Загружаем изображения, если они есть
if (selectedImages.length > 0) {
await uploadImages(noteId);
}
// Очищаем поле ввода и изображения, перезагружаем заметки
noteInput.value = "";
noteInput.style.height = "auto";
selectedImages = [];
updateImagePreview();
imageInput.value = "";
await loadNotes(true);
} catch (error) {
console.error("Ошибка:", error);
alert("Ошибка сохранения заметки");
}
}
}
// Обработчик сохранения новой заметки
saveBtn.addEventListener("click", saveNote);
// Обработчик горячей клавиши Alt+Enter для сохранения заметки
noteInput.addEventListener("keydown", function (event) {
if (event.altKey && event.key === "Enter") {
event.preventDefault(); // Предотвращаем стандартное поведение
saveNote();
}
});
// Загружаем заметки при загрузке страницы
document.addEventListener("DOMContentLoaded", function () {
// Проверяем аутентификацию при загрузке страницы
checkAuthentication();
loadUserInfo();
loadNotes();
updateFilterIndicator();
// Добавляем обработчик для кнопки выхода
setupLogoutHandler();
});
// Функция для настройки обработчика выхода
function setupLogoutHandler() {
const logoutForms = document.querySelectorAll('form[action="/logout"]');
logoutForms.forEach(form => {
form.addEventListener('submit', function(e) {
// Очищаем localStorage перед выходом
localStorage.removeItem('isAuthenticated');
localStorage.removeItem('username');
});
});
}
// Функция для проверки аутентификации
async function checkAuthentication() {
const isAuthenticated = localStorage.getItem('isAuthenticated');
if (isAuthenticated !== 'true') {
// Если пользователь не аутентифицирован, перенаправляем на страницу входа
window.location.href = "/";
return;
}
// Проверяем, что сессия на сервере еще действительна
try {
const response = await fetch("/api/auth/status");
if (!response.ok) {
// Если сессия недействительна, очищаем localStorage и перенаправляем
localStorage.removeItem('isAuthenticated');
localStorage.removeItem('username');
window.location.href = "/";
return;
}
const authData = await response.json();
if (!authData.authenticated) {
// Если сервер говорит, что пользователь не аутентифицирован
localStorage.removeItem('isAuthenticated');
localStorage.removeItem('username');
window.location.href = "/";
return;
}
} catch (error) {
console.error("Ошибка проверки аутентификации:", error);
// В случае ошибки сети, оставляем пользователя на странице
// но показываем предупреждение
console.warn("Не удалось проверить статус аутентификации");
}
}
// Функция для загрузки информации о пользователе
async function loadUserInfo() {
try {
const response = await fetch("/api/user");
if (response.ok) {
const user = await response.json();
const usernameDisplay = document.getElementById("username-display");
const userAvatar = document.getElementById("user-avatar");
const userAvatarContainer = document.getElementById(
"user-avatar-container"
);
if (usernameDisplay) {
usernameDisplay.innerHTML = `<span class="iconify" data-icon="mdi:account"></span> ${user.username}`;
// Делаем ник кликабельным для перехода в личный кабинет
usernameDisplay.style.cursor = "pointer";
usernameDisplay.addEventListener("click", function () {
window.location.href = "/profile";
});
}
// Аватарка скрыта на странице заметок
if (userAvatarContainer) {
userAvatarContainer.style.display = "none";
}
}
} catch (error) {
console.error("Ошибка загрузки информации о пользователе:", error);
}
}
// Календарь
let currentDate = new Date();
// Функция для отображения календаря
function renderCalendar() {
const calendarDays = document.getElementById("calendarDays");
const monthYear = document.getElementById("monthYear");
// Проверяем, существуют ли элементы календаря
if (!calendarDays || !monthYear) return;
const year = currentDate.getFullYear();
const month = currentDate.getMonth();
// Массив названий месяцев
const monthNames = [
"Январь",
"Февраль",
"Март",
"Апрель",
"Май",
"Июнь",
"Июль",
"Август",
"Сентябрь",
"Октябрь",
"Ноябрь",
"Декабрь",
];
// Устанавливаем заголовок месяца и года
monthYear.textContent = `${monthNames[month]} ${year}`;
// Получаем первый день месяца
const firstDay = new Date(year, month, 1);
// Получаем последний день месяца
const lastDay = new Date(year, month + 1, 0);
// Получаем день недели первого дня (0 - воскресенье, 1 - понедельник и т.д.)
let firstDayOfWeek = firstDay.getDay();
// Преобразуем так, чтобы понедельник был первым днем (0)
firstDayOfWeek = firstDayOfWeek === 0 ? 6 : firstDayOfWeek - 1;
// Очищаем календарь
calendarDays.innerHTML = "";
// Создаём Set дат, когда были созданы заметки
const noteDates = new Set();
allNotes.forEach((note) => {
noteDates.add(note.date);
});
// Получаем последний день предыдущего месяца
const prevMonthLastDay = new Date(year, month, 0).getDate();
const prevMonth = month === 0 ? 11 : month - 1;
const prevYear = month === 0 ? year - 1 : year;
// Добавляем дни предыдущего месяца
for (let i = firstDayOfWeek - 1; i >= 0; i--) {
const day = prevMonthLastDay - i;
const dateStr = `${String(day).padStart(2, "0")}.${String(
prevMonth + 1
).padStart(2, "0")}.${prevYear}`;
const dayDiv = document.createElement("div");
dayDiv.classList.add("calendar-day", "other-month");
dayDiv.textContent = day;
dayDiv.dataset.date = dateStr;
// Проверяем, есть ли заметки на этот день
if (noteDates.has(dateStr)) {
dayDiv.classList.add("has-notes");
}
// Проверяем, выбран ли этот день
if (selectedDateFilter === dateStr) {
dayDiv.classList.add("selected");
}
// Добавляем обработчик клика
dayDiv.addEventListener("click", async (event) => await handleDayClick(event));
calendarDays.appendChild(dayDiv);
}
// Добавляем дни текущего месяца
const today = new Date();
for (let day = 1; day <= lastDay.getDate(); day++) {
const dateStr = `${String(day).padStart(2, "0")}.${String(
month + 1
).padStart(2, "0")}.${year}`;
const dayDiv = document.createElement("div");
dayDiv.classList.add("calendar-day");
dayDiv.textContent = day;
dayDiv.dataset.date = dateStr;
// Проверяем, является ли день сегодняшним
if (
day === today.getDate() &&
month === today.getMonth() &&
year === today.getFullYear()
) {
dayDiv.classList.add("today");
}
// Проверяем, есть ли заметки на этот день
if (noteDates.has(dateStr)) {
dayDiv.classList.add("has-notes");
}
// Проверяем, выбран ли этот день
if (selectedDateFilter === dateStr) {
dayDiv.classList.add("selected");
}
// Добавляем обработчик клика
dayDiv.addEventListener("click", async (event) => await handleDayClick(event));
calendarDays.appendChild(dayDiv);
}
// Добавляем дни следующего месяца
const totalCells = calendarDays.children.length;
const remainingCells = 42 - totalCells; // 6 недель по 7 дней
const nextMonth = month === 11 ? 0 : month + 1;
const nextYear = month === 11 ? year + 1 : year;
for (let day = 1; day <= remainingCells; day++) {
const dateStr = `${String(day).padStart(2, "0")}.${String(
nextMonth + 1
).padStart(2, "0")}.${nextYear}`;
const dayDiv = document.createElement("div");
dayDiv.classList.add("calendar-day", "other-month");
dayDiv.textContent = day;
dayDiv.dataset.date = dateStr;
// Проверяем, есть ли заметки на этот день
if (noteDates.has(dateStr)) {
dayDiv.classList.add("has-notes");
}
// Проверяем, выбран ли этот день
if (selectedDateFilter === dateStr) {
dayDiv.classList.add("selected");
}
// Добавляем обработчик клика
dayDiv.addEventListener("click", async (event) => await handleDayClick(event));
calendarDays.appendChild(dayDiv);
}
}
// Обработчик клика на день в календаре
async function handleDayClick(event) {
const clickedDate = event.target.dataset.date;
// Если кликнули на тот же день, снимаем фильтр
if (selectedDateFilter === clickedDate) {
selectedDateFilter = null;
} else {
selectedDateFilter = clickedDate;
}
// Перерисовываем заметки, календарь и теги
await renderNotes(allNotes);
renderCalendar();
renderTags();
updateFilterIndicator();
}
// Функция для обновления индикатора фильтра
function updateFilterIndicator() {
const filterIndicator = document.getElementById("filter-indicator");
if (!filterIndicator) return;
const filters = [];
if (searchQuery) {
filters.push(`Поиск: "${searchQuery}"`);
}
if (selectedDateFilter) {
filters.push(`Дата: ${selectedDateFilter}`);
}
if (selectedTagFilter) {
filters.push(`Тег: #${selectedTagFilter}`);
}
if (filters.length > 0) {
filterIndicator.style.display = "inline-block";
filterIndicator.innerHTML = `Фильтр: ${filters.join(
", "
)} <button id="clear-filter-btn">✕</button>`;
// Добавляем обработчик клика для кнопки сброса
const clearBtn = document.getElementById("clear-filter-btn");
if (clearBtn) {
clearBtn.addEventListener("click", clearFilter);
}
} else {
filterIndicator.style.display = "none";
}
}
// Функция для сброса фильтра (глобальная)
window.clearFilter = async function () {
selectedDateFilter = null;
selectedTagFilter = null;
searchQuery = "";
searchResults = [];
// Очищаем поле поиска
const searchInput = document.getElementById("searchInput");
if (searchInput) {
searchInput.value = "";
}
// Скрываем кнопку очистки поиска
const clearSearchBtn = document.getElementById("clearSearchBtn");
if (clearSearchBtn) {
clearSearchBtn.style.display = "none";
}
await renderNotes(allNotes);
renderCalendar();
renderTags();
updateFilterIndicator();
};
// Глобальные функции для работы с изображениями (оставляем только showImageModal для совместимости)
// window.showImageModal удалена, так как она создавала рекурсию
// Обработчики для кнопок навигации календаря
const prevMonthBtn = document.getElementById("prevMonth");
const nextMonthBtn = document.getElementById("nextMonth");
if (prevMonthBtn) {
prevMonthBtn.addEventListener("click", function () {
currentDate.setMonth(currentDate.getMonth() - 1);
renderCalendar();
});
}
if (nextMonthBtn) {
nextMonthBtn.addEventListener("click", function () {
currentDate.setMonth(currentDate.getMonth() + 1);
renderCalendar();
});
}
// Инициализируем календарь при загрузке страницы
document.addEventListener("DOMContentLoaded", function () {
renderCalendar();
// Инициализируем поиск
initSearch();
});
// Функция для инициализации поиска
function initSearch() {
const searchInput = document.getElementById("searchInput");
const clearSearchBtn = document.getElementById("clearSearchBtn");
if (!searchInput || !clearSearchBtn) return;
// Обработчик ввода в поле поиска с задержкой
let searchTimeout;
searchInput.addEventListener("input", function () {
clearTimeout(searchTimeout);
const query = this.value;
// Показываем/скрываем кнопку очистки
if (query.trim()) {
clearSearchBtn.style.display = "block";
} else {
clearSearchBtn.style.display = "none";
}
// Задержка перед поиском для оптимизации
searchTimeout = setTimeout(() => {
searchNotes(query);
updateFilterIndicator();
}, 300);
});
// Обработчик клика на кнопку очистки поиска
clearSearchBtn.addEventListener("click", async function () {
searchInput.value = "";
this.style.display = "none";
searchQuery = "";
searchResults = [];
await renderNotes(allNotes);
updateFilterIndicator();
});
// Обработчик клавиши Escape для очистки поиска
searchInput.addEventListener("keydown", async function (event) {
if (event.key === "Escape") {
this.value = "";
clearSearchBtn.style.display = "none";
searchQuery = "";
searchResults = [];
await renderNotes(allNotes);
updateFilterIndicator();
}
});
}
// ==================== МОБИЛЬНЫЙ СЛАЙДЕР ====================
// Функция для отображения календаря в мобильном слайдере
function renderCalendarMobile() {
const calendarDays = document.getElementById("calendarDaysMobile");
const monthYear = document.getElementById("monthYearMobile");
// Проверяем, существуют ли элементы календаря для мобильной версии
if (!calendarDays || !monthYear) return;
const year = currentDate.getFullYear();
const month = currentDate.getMonth();
// Массив названий месяцев
const monthNames = [
"Январь",
"Февраль",
"Март",
"Апрель",
"Май",
"Июнь",
"Июль",
"Август",
"Сентябрь",
"Октябрь",
"Ноябрь",
"Декабрь",
];
// Устанавливаем заголовок месяца и года
monthYear.textContent = `${monthNames[month]} ${year}`;
// Получаем первый день месяца
const firstDay = new Date(year, month, 1);
// Получаем последний день месяца
const lastDay = new Date(year, month + 1, 0);
// Получаем день недели первого дня (0 - воскресенье, 1 - понедельник и т.д.)
let firstDayOfWeek = firstDay.getDay();
// Преобразуем так, чтобы понедельник был первым днем (0)
firstDayOfWeek = firstDayOfWeek === 0 ? 6 : firstDayOfWeek - 1;
// Очищаем календарь
calendarDays.innerHTML = "";
// Создаём Set дат, когда были созданы заметки
const noteDates = new Set();
allNotes.forEach((note) => {
noteDates.add(note.date);
});
// Получаем последний день предыдущего месяца
const prevMonthLastDay = new Date(year, month, 0).getDate();
const prevMonth = month === 0 ? 11 : month - 1;
const prevYear = month === 0 ? year - 1 : year;
// Добавляем дни предыдущего месяца
for (let i = firstDayOfWeek - 1; i >= 0; i--) {
const day = prevMonthLastDay - i;
const dateStr = `${String(day).padStart(2, "0")}.${String(
prevMonth + 1
).padStart(2, "0")}.${prevYear}`;
const dayDiv = document.createElement("div");
dayDiv.classList.add("calendar-day", "other-month");
dayDiv.textContent = day;
dayDiv.dataset.date = dateStr;
// Проверяем, есть ли заметки на этот день
if (noteDates.has(dateStr)) {
dayDiv.classList.add("has-notes");
}
// Проверяем, выбран ли этот день
if (selectedDateFilter === dateStr) {
dayDiv.classList.add("selected");
}
// Добавляем обработчик клика
dayDiv.addEventListener("click", async (event) => await handleDayClickMobile(event));
calendarDays.appendChild(dayDiv);
}
// Добавляем дни текущего месяца
const today = new Date();
for (let day = 1; day <= lastDay.getDate(); day++) {
const dateStr = `${String(day).padStart(2, "0")}.${String(
month + 1
).padStart(2, "0")}.${year}`;
const dayDiv = document.createElement("div");
dayDiv.classList.add("calendar-day");
dayDiv.textContent = day;
dayDiv.dataset.date = dateStr;
// Проверяем, является ли день сегодняшним
if (
day === today.getDate() &&
month === today.getMonth() &&
year === today.getFullYear()
) {
dayDiv.classList.add("today");
}
// Проверяем, есть ли заметки на этот день
if (noteDates.has(dateStr)) {
dayDiv.classList.add("has-notes");
}
// Проверяем, выбран ли этот день
if (selectedDateFilter === dateStr) {
dayDiv.classList.add("selected");
}
// Добавляем обработчик клика
dayDiv.addEventListener("click", async (event) => await handleDayClickMobile(event));
calendarDays.appendChild(dayDiv);
}
// Добавляем дни следующего месяца
const totalCells = calendarDays.children.length;
const remainingCells = 42 - totalCells; // 6 недель по 7 дней
const nextMonth = month === 11 ? 0 : month + 1;
const nextYear = month === 11 ? year + 1 : year;
for (let day = 1; day <= remainingCells; day++) {
const dateStr = `${String(day).padStart(2, "0")}.${String(
nextMonth + 1
).padStart(2, "0")}.${nextYear}`;
const dayDiv = document.createElement("div");
dayDiv.classList.add("calendar-day", "other-month");
dayDiv.textContent = day;
dayDiv.dataset.date = dateStr;
// Проверяем, есть ли заметки на этот день
if (noteDates.has(dateStr)) {
dayDiv.classList.add("has-notes");
}
// Проверяем, выбран ли этот день
if (selectedDateFilter === dateStr) {
dayDiv.classList.add("selected");
}
// Добавляем обработчик клика
dayDiv.addEventListener("click", async (event) => await handleDayClickMobile(event));
calendarDays.appendChild(dayDiv);
}
}
// Обработчик клика на день в календаре для мобильной версии
async function handleDayClickMobile(event) {
const clickedDate = event.target.dataset.date;
// Если кликнули на тот же день, снимаем фильтр
if (selectedDateFilter === clickedDate) {
selectedDateFilter = null;
} else {
selectedDateFilter = clickedDate;
}
// Перерисовываем заметки, оба календаря и теги
await renderNotes(allNotes);
renderCalendar();
renderCalendarMobile();
renderTags();
renderTagsMobile();
updateFilterIndicator();
}
// Функция для отображения тегов в мобильном слайдере
function renderTagsMobile() {
const tagsContainer = document.getElementById("tagsContainerMobile");
if (!tagsContainer) return;
const tagCounts = getAllTags(allNotes);
const sortedTags = Object.keys(tagCounts).sort();
if (sortedTags.length === 0) {
tagsContainer.innerHTML =
'<div style="font-size: 10px; color: #999; text-align: center;">Нет тегов</div>';
return;
}
tagsContainer.innerHTML = sortedTags
.map((tag) => {
const count = tagCounts[tag];
const isActive = selectedTagFilter === tag ? "active" : "";
return `<span class="tag ${isActive}" data-tag="${tag}">#${tag}<span class="tag-count">${count}</span></span>`;
})
.join("");
// Добавляем обработчики кликов для тегов
tagsContainer.querySelectorAll(".tag").forEach((tagElement) => {
tagElement.addEventListener("click", async (event) => await handleTagClickMobile(event));
});
}
// Обработчик клика на тег в мобильном слайдере
async function handleTagClickMobile(event) {
const clickedTag = event.target.closest(".tag").dataset.tag;
// Если кликнули на тот же тег, снимаем фильтр
if (selectedTagFilter === clickedTag) {
selectedTagFilter = null;
} else {
selectedTagFilter = clickedTag;
}
// Перерисовываем заметки, теги и оба календаря
await renderNotes(allNotes);
renderTags();
renderTagsMobile();
renderCalendar();
renderCalendarMobile();
updateFilterIndicator();
}
// Инициализация мобильного слайдера
document.addEventListener("DOMContentLoaded", function () {
const mobileMenuBtn = document.getElementById("mobileMenuBtn");
const sidebarCloseBtn = document.getElementById("sidebarCloseBtn");
const mobileSidebar = document.getElementById("mobileSidebar");
const sidebarOverlay = document.getElementById("sidebarOverlay");
// Открытие слайдера при клике на кнопку меню
if (mobileMenuBtn) {
mobileMenuBtn.addEventListener("click", function () {
mobileSidebar.classList.add("open");
sidebarOverlay.classList.add("open");
document.body.style.overflow = "hidden";
});
}
// Закрытие слайдера при клике на кнопку закрытия
if (sidebarCloseBtn) {
sidebarCloseBtn.addEventListener("click", function () {
mobileSidebar.classList.remove("open");
sidebarOverlay.classList.remove("open");
document.body.style.overflow = "auto";
});
}
// Закрытие слайдера при клике на оверлей
if (sidebarOverlay) {
sidebarOverlay.addEventListener("click", function () {
mobileSidebar.classList.remove("open");
sidebarOverlay.classList.remove("open");
document.body.style.overflow = "auto";
});
}
// Инициализация мобильного поиска
initSearchMobile();
// Инициализация мобильного календаря
renderCalendarMobile();
renderTagsMobile();
// Обработчики для кнопок навигации мобильного календаря
const prevMonthBtnMobile = document.getElementById("prevMonthMobile");
const nextMonthBtnMobile = document.getElementById("nextMonthMobile");
if (prevMonthBtnMobile) {
prevMonthBtnMobile.addEventListener("click", function () {
currentDate.setMonth(currentDate.getMonth() - 1);
renderCalendar();
renderCalendarMobile();
});
}
if (nextMonthBtnMobile) {
nextMonthBtnMobile.addEventListener("click", function () {
currentDate.setMonth(currentDate.getMonth() + 1);
renderCalendar();
renderCalendarMobile();
});
}
});
// Функция для инициализации мобильного поиска
function initSearchMobile() {
const searchInput = document.getElementById("searchInputMobile");
const clearSearchBtn = document.getElementById("clearSearchBtnMobile");
if (!searchInput || !clearSearchBtn) return;
// Обработчик ввода в поле поиска с задержкой
let searchTimeout;
searchInput.addEventListener("input", function () {
clearTimeout(searchTimeout);
const query = this.value;
// Показываем/скрываем кнопку очистки
if (query.trim()) {
clearSearchBtn.style.display = "block";
} else {
clearSearchBtn.style.display = "none";
}
// Задержка перед поиском для оптимизации
searchTimeout = setTimeout(() => {
searchNotes(query);
// Обновляем основное поле поиска
const mainSearchInput = document.getElementById("searchInput");
if (mainSearchInput) {
mainSearchInput.value = query;
}
const mainClearSearchBtn = document.getElementById("clearSearchBtn");
if (mainClearSearchBtn) {
if (query.trim()) {
mainClearSearchBtn.style.display = "block";
} else {
mainClearSearchBtn.style.display = "none";
}
}
updateFilterIndicator();
}, 300);
});
// Обработчик клика на кнопку очистки поиска
clearSearchBtn.addEventListener("click", async function () {
searchInput.value = "";
this.style.display = "none";
searchQuery = "";
searchResults = [];
// Обновляем основное поле поиска
const mainSearchInput = document.getElementById("searchInput");
if (mainSearchInput) {
mainSearchInput.value = "";
}
const mainClearSearchBtn = document.getElementById("clearSearchBtn");
if (mainClearSearchBtn) {
mainClearSearchBtn.style.display = "none";
}
await renderNotes(allNotes);
updateFilterIndicator();
});
// Обработчик клавиши Escape для очистки поиска
searchInput.addEventListener("keydown", function (event) {
if (event.key === "Escape") {
this.value = "";
clearSearchBtn.style.display = "none";
searchQuery = "";
searchResults = [];
// Обновляем основное поле поиска
const mainSearchInput = document.getElementById("searchInput");
if (mainSearchInput) {
mainSearchInput.value = "";
}
const mainClearSearchBtn = document.getElementById("clearSearchBtn");
if (mainClearSearchBtn) {
mainClearSearchBtn.style.display = "none";
}
renderNotes(allNotes);
updateFilterIndicator();
}
});
}