- Добавлена кнопка открытия слайдера (☰) на мобильных устройствах - Реализован удобный боковой слайдер для мобильной версии - Слайдер содержит полностью функциональный календарь с навигацией - Поле поиска синхронизировано с ПК версией - Теги отображаются с количеством заметок - Возможность закрытия слайдера кнопкой или оверлеем - Все функции работают как в ПК версии - Добавлена синхронизация между мобильным и ПК календарями - Обновлена документация README с описанием мобильной версии
1483 lines
52 KiB
JavaScript
1483 lines
52 KiB
JavaScript
// 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");
|
||
|
||
// Глобальные переменные для заметок и фильтрации
|
||
let allNotes = [];
|
||
let selectedDateFilter = null;
|
||
let selectedTagFilter = null;
|
||
let searchQuery = "";
|
||
let searchResults = [];
|
||
|
||
// Функция для получения текущей даты и времени
|
||
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", handleTagClick);
|
||
});
|
||
}
|
||
|
||
// Обработчик клика на тег
|
||
function handleTagClick(event) {
|
||
const clickedTag = event.target.closest(".tag").dataset.tag;
|
||
|
||
// Если кликнули на тот же тег, снимаем фильтр
|
||
if (selectedTagFilter === clickedTag) {
|
||
selectedTagFilter = null;
|
||
} else {
|
||
selectedTagFilter = clickedTag;
|
||
}
|
||
|
||
// Перерисовываем заметки и теги
|
||
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)");
|
||
});
|
||
|
||
// Функция для загрузки заметок с сервера
|
||
async function loadNotes() {
|
||
try {
|
||
const response = await fetch("/api/notes");
|
||
if (!response.ok) {
|
||
throw new Error("Ошибка загрузки заметок");
|
||
}
|
||
const notes = await response.json();
|
||
allNotes = notes; // Сохраняем все заметки в глобальную переменную
|
||
renderNotes(notes);
|
||
renderCalendar(); // Обновляем календарь после загрузки заметок
|
||
renderTags(); // Обновляем теги после загрузки заметок
|
||
} catch (error) {
|
||
console.error("Ошибка:", error);
|
||
notesList.innerHTML = "<p>Ошибка загрузки заметок</p>";
|
||
}
|
||
}
|
||
|
||
// Функция для поиска заметок
|
||
async function searchNotes(query) {
|
||
if (!query || query.trim() === "") {
|
||
searchQuery = "";
|
||
searchResults = [];
|
||
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();
|
||
renderNotes(searchResults);
|
||
} catch (error) {
|
||
console.error("Ошибка поиска:", error);
|
||
searchResults = [];
|
||
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>');
|
||
}
|
||
|
||
// Функция для отображения заметок
|
||
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;
|
||
}
|
||
|
||
// Итерируемся по заметкам в обычном порядке, чтобы новые были сверху
|
||
notesToDisplay.forEach(function (note) {
|
||
let contentToProcess = note.content;
|
||
|
||
// Сначала подсвечиваем найденный текст в исходном markdown
|
||
if (searchQuery) {
|
||
contentToProcess = highlightSearchText(contentToProcess, searchQuery);
|
||
}
|
||
|
||
// Затем преобразуем теги в кликабельные элементы
|
||
const contentWithClickableTags = makeTagsClickable(contentToProcess);
|
||
|
||
const parsedContent = marked.parse(contentWithClickableTags);
|
||
|
||
const noteHtml = `
|
||
<div id="note" class="container">
|
||
<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,
|
||
"""
|
||
)}">${parsedContent}</div>
|
||
</div>
|
||
`;
|
||
notesList.insertAdjacentHTML("afterbegin", noteHtml);
|
||
});
|
||
|
||
// Добавляем обработчики событий для кнопок редактирования и удаления
|
||
addNoteEventListeners();
|
||
|
||
// Добавляем обработчики кликов для тегов в заметках
|
||
addTagClickListeners();
|
||
|
||
// Обрабатываем длинные заметки
|
||
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("Ошибка удаления заметки");
|
||
}
|
||
|
||
// Перезагружаем заметки
|
||
loadNotes();
|
||
} 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");
|
||
|
||
// Создаем контейнер для 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("Ошибка сохранения заметки");
|
||
}
|
||
|
||
// Перезагружаем заметки
|
||
loadNotes();
|
||
} 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", function (event) {
|
||
event.preventDefault();
|
||
event.stopPropagation();
|
||
|
||
const clickedTag = this.dataset.tag.toLowerCase();
|
||
|
||
// Если кликнули на тот же тег, снимаем фильтр
|
||
if (selectedTagFilter === clickedTag) {
|
||
selectedTagFilter = null;
|
||
} else {
|
||
selectedTagFilter = clickedTag;
|
||
}
|
||
|
||
// Перерисовываем заметки и теги
|
||
renderNotes(allNotes);
|
||
renderTags();
|
||
updateFilterIndicator();
|
||
});
|
||
});
|
||
}
|
||
|
||
// Функция сохранения заметки (вынесена отдельно для повторного использования)
|
||
async function saveNote() {
|
||
if (noteInput.value.trim() !== "") {
|
||
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("Ошибка сохранения заметки");
|
||
}
|
||
|
||
// Очищаем поле ввода и перезагружаем заметки
|
||
noteInput.value = "";
|
||
noteInput.style.height = "auto";
|
||
loadNotes();
|
||
} 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", handleDayClick);
|
||
|
||
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", handleDayClick);
|
||
|
||
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", handleDayClick);
|
||
|
||
calendarDays.appendChild(dayDiv);
|
||
}
|
||
}
|
||
|
||
// Обработчик клика на день в календаре
|
||
function handleDayClick(event) {
|
||
const clickedDate = event.target.dataset.date;
|
||
|
||
// Если кликнули на тот же день, снимаем фильтр
|
||
if (selectedDateFilter === clickedDate) {
|
||
selectedDateFilter = null;
|
||
} else {
|
||
selectedDateFilter = clickedDate;
|
||
}
|
||
|
||
// Перерисовываем заметки, календарь и теги
|
||
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 = 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";
|
||
}
|
||
|
||
renderNotes(allNotes);
|
||
renderCalendar();
|
||
renderTags();
|
||
updateFilterIndicator();
|
||
};
|
||
|
||
// Обработчики для кнопок навигации календаря
|
||
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", function () {
|
||
searchInput.value = "";
|
||
this.style.display = "none";
|
||
searchQuery = "";
|
||
searchResults = [];
|
||
renderNotes(allNotes);
|
||
updateFilterIndicator();
|
||
});
|
||
|
||
// Обработчик клавиши Escape для очистки поиска
|
||
searchInput.addEventListener("keydown", function (event) {
|
||
if (event.key === "Escape") {
|
||
this.value = "";
|
||
clearSearchBtn.style.display = "none";
|
||
searchQuery = "";
|
||
searchResults = [];
|
||
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", handleDayClickMobile);
|
||
|
||
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", handleDayClickMobile);
|
||
|
||
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", handleDayClickMobile);
|
||
|
||
calendarDays.appendChild(dayDiv);
|
||
}
|
||
}
|
||
|
||
// Обработчик клика на день в календаре для мобильной версии
|
||
function handleDayClickMobile(event) {
|
||
const clickedDate = event.target.dataset.date;
|
||
|
||
// Если кликнули на тот же день, снимаем фильтр
|
||
if (selectedDateFilter === clickedDate) {
|
||
selectedDateFilter = null;
|
||
} else {
|
||
selectedDateFilter = clickedDate;
|
||
}
|
||
|
||
// Перерисовываем заметки, оба календаря и теги
|
||
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", handleTagClickMobile);
|
||
});
|
||
}
|
||
|
||
// Обработчик клика на тег в мобильном слайдере
|
||
function handleTagClickMobile(event) {
|
||
const clickedTag = event.target.closest(".tag").dataset.tag;
|
||
|
||
// Если кликнули на тот же тег, снимаем фильтр
|
||
if (selectedTagFilter === clickedTag) {
|
||
selectedTagFilter = null;
|
||
} else {
|
||
selectedTagFilter = clickedTag;
|
||
}
|
||
|
||
// Перерисовываем заметки, теги и оба календаря
|
||
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", 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";
|
||
}
|
||
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();
|
||
}
|
||
});
|
||
}
|