feat: Добавлена функциональность тегов с фильтрацией
- Добавлена секция тегов под календарем с отображением всех уникальных тегов - Реализована фильтрация заметок по тегам при клике на тег - Добавлены кликабельные теги в самих заметках для интуитивной навигации - Теги автоматически извлекаются из текста заметок в формате #название - Добавлены счетчики для каждого тега, показывающие количество заметок - Реализован индикатор активного фильтра с возможностью сброса - Поддержка комбинированной фильтрации по дате и тегам - Стилизованные теги с hover-эффектами и анимациями - Обновление тегов в реальном времени при создании/редактировании заметок Файлы изменены: - public/notes.html: добавлена HTML-структура для секции тегов - public/style.css: стили для тегов в боковой панели и в заметках - public/app.js: логика извлечения тегов, фильтрации и обработчики кликов
This commit is contained in:
parent
493e1a57be
commit
034208fc56
BIN
clickable_tags_final_screenshot.png
Normal file
BIN
clickable_tags_final_screenshot.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 121 KiB |
167
public/app.js
167
public/app.js
@ -15,6 +15,7 @@ const linkBtn = document.getElementById("linkBtn");
|
||||
// Глобальные переменные для заметок и фильтрации
|
||||
let allNotes = [];
|
||||
let selectedDateFilter = null;
|
||||
let selectedTagFilter = null;
|
||||
|
||||
// Функция для получения текущей даты и времени
|
||||
function getFormattedDateTime() {
|
||||
@ -37,6 +38,90 @@ function autoExpandTextarea(textarea) {
|
||||
textarea.style.height = textarea.scrollHeight + "px";
|
||||
}
|
||||
|
||||
// Функция для извлечения тегов из текста заметки
|
||||
function extractTags(content) {
|
||||
const tagRegex = /#(\w+)/g;
|
||||
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) {
|
||||
const tagRegex = /#(\w+)/g;
|
||||
return content.replace(
|
||||
tagRegex,
|
||||
'<span class="tag-in-note" data-tag="$1">#$1</span>'
|
||||
);
|
||||
}
|
||||
|
||||
// Функция для получения всех уникальных тегов из заметок
|
||||
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);
|
||||
@ -201,6 +286,7 @@ async function loadNotes() {
|
||||
allNotes = notes; // Сохраняем все заметки в глобальную переменную
|
||||
renderNotes(notes);
|
||||
renderCalendar(); // Обновляем календарь после загрузки заметок
|
||||
renderTags(); // Обновляем теги после загрузки заметок
|
||||
} catch (error) {
|
||||
console.error("Ошибка:", error);
|
||||
notesList.innerHTML = "<p>Ошибка загрузки заметок</p>";
|
||||
@ -211,25 +297,44 @@ async function loadNotes() {
|
||||
function renderNotes(notes) {
|
||||
notesList.innerHTML = "";
|
||||
|
||||
// Фильтруем заметки, если выбрана дата
|
||||
// Фильтруем заметки по дате и тегам
|
||||
let notesToDisplay = notes;
|
||||
|
||||
if (selectedDateFilter) {
|
||||
notesToDisplay = notes.filter((note) => note.date === 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) {
|
||||
if (selectedDateFilter) {
|
||||
notesList.innerHTML = `<div class="container"><p style="text-align: center; color: #999;">Нет заметок за выбранную дату (${selectedDateFilter})</p></div>`;
|
||||
} else {
|
||||
notesList.innerHTML =
|
||||
'<div class="container"><p style="text-align: center; color: #999;">Заметок пока нет</p></div>';
|
||||
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) {
|
||||
// Преобразуем теги в кликабельные элементы перед парсингом markdown
|
||||
const contentWithClickableTags = makeTagsClickable(note.content);
|
||||
const parsedContent = marked.parse(contentWithClickableTags);
|
||||
|
||||
const noteHtml = `
|
||||
<div id="note" class="container">
|
||||
<div class="date">
|
||||
@ -244,7 +349,7 @@ function renderNotes(notes) {
|
||||
<div class="textNote" data-original-content="${note.content.replace(
|
||||
/"/g,
|
||||
"""
|
||||
)}">${marked.parse(note.content)}</div>
|
||||
)}">${parsedContent}</div>
|
||||
</div>
|
||||
`;
|
||||
notesList.insertAdjacentHTML("afterbegin", noteHtml);
|
||||
@ -252,6 +357,9 @@ function renderNotes(notes) {
|
||||
|
||||
// Добавляем обработчики событий для кнопок редактирования и удаления
|
||||
addNoteEventListeners();
|
||||
|
||||
// Добавляем обработчики кликов для тегов в заметках
|
||||
addTagClickListeners();
|
||||
}
|
||||
|
||||
// Функция для добавления обработчиков событий к заметкам
|
||||
@ -375,6 +483,30 @@ function addNoteEventListeners() {
|
||||
});
|
||||
}
|
||||
|
||||
// Функция для добавления обработчиков кликов на теги в заметках
|
||||
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() !== "") {
|
||||
@ -630,9 +762,10 @@ function handleDayClick(event) {
|
||||
selectedDateFilter = clickedDate;
|
||||
}
|
||||
|
||||
// Перерисовываем заметки и календарь
|
||||
// Перерисовываем заметки, календарь и теги
|
||||
renderNotes(allNotes);
|
||||
renderCalendar();
|
||||
renderTags();
|
||||
updateFilterIndicator();
|
||||
}
|
||||
|
||||
@ -641,9 +774,21 @@ function updateFilterIndicator() {
|
||||
const filterIndicator = document.getElementById("filter-indicator");
|
||||
if (!filterIndicator) return;
|
||||
|
||||
const filters = [];
|
||||
|
||||
if (selectedDateFilter) {
|
||||
filters.push(`Дата: ${selectedDateFilter}`);
|
||||
}
|
||||
|
||||
if (selectedTagFilter) {
|
||||
filters.push(`Тег: #${selectedTagFilter}`);
|
||||
}
|
||||
|
||||
if (filters.length > 0) {
|
||||
filterIndicator.style.display = "inline-block";
|
||||
filterIndicator.innerHTML = `Фильтр: ${selectedDateFilter} <button id="clear-filter-btn">✕</button>`;
|
||||
filterIndicator.innerHTML = `Фильтр: ${filters.join(
|
||||
", "
|
||||
)} <button id="clear-filter-btn">✕</button>`;
|
||||
|
||||
// Добавляем обработчик клика для кнопки сброса
|
||||
const clearBtn = document.getElementById("clear-filter-btn");
|
||||
@ -658,8 +803,10 @@ function updateFilterIndicator() {
|
||||
// Функция для сброса фильтра (глобальная)
|
||||
window.clearFilter = function () {
|
||||
selectedDateFilter = null;
|
||||
selectedTagFilter = null;
|
||||
renderNotes(allNotes);
|
||||
renderCalendar();
|
||||
renderTags();
|
||||
updateFilterIndicator();
|
||||
};
|
||||
|
||||
|
||||
@ -29,6 +29,16 @@
|
||||
</div>
|
||||
<div class="calendar-days" id="calendarDays"></div>
|
||||
</div>
|
||||
|
||||
<!-- Секция тегов -->
|
||||
<div class="tags-section">
|
||||
<div class="tags-header">
|
||||
<span class="tags-title">🏷️ Теги</span>
|
||||
</div>
|
||||
<div class="tags-container" id="tagsContainer">
|
||||
<!-- Теги будут добавлены динамически -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="center">
|
||||
<div class="container">
|
||||
|
||||
@ -601,3 +601,80 @@ textarea:focus {
|
||||
.calendar-day.today.has-notes::after {
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
/* Стили для секции тегов */
|
||||
.tags-section {
|
||||
margin-top: 15px;
|
||||
padding-top: 15px;
|
||||
border-top: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.tags-header {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.tags-title {
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.tags-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.tag {
|
||||
display: inline-block;
|
||||
padding: 4px 8px;
|
||||
background-color: #e7f3ff;
|
||||
color: #007bff;
|
||||
border: 1px solid #007bff;
|
||||
border-radius: 12px;
|
||||
font-size: 10px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.tag:hover {
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.tag.active {
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.tag-count {
|
||||
margin-left: 4px;
|
||||
font-size: 9px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
/* Стили для тегов в заметках */
|
||||
.textNote .tag-in-note {
|
||||
display: inline-block;
|
||||
padding: 2px 6px;
|
||||
background-color: #e7f3ff;
|
||||
color: #007bff;
|
||||
border: 1px solid #007bff;
|
||||
border-radius: 8px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
text-decoration: none;
|
||||
margin: 0 2px;
|
||||
}
|
||||
|
||||
.textNote .tag-in-note:hover {
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 2px 4px rgba(0, 123, 255, 0.3);
|
||||
}
|
||||
|
||||
BIN
tags_functionality_screenshot.png
Normal file
BIN
tags_functionality_screenshot.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 103 KiB |
Loading…
x
Reference in New Issue
Block a user