Добавлены функции для работы с цветом текста и чекбоксами в заметках

- Реализована возможность вставки цветового тега в текст заметок с помощью диалога выбора цвета.
- Добавлены функции для работы с чекбоксами, включая автоматическое продолжение списков и визуальные эффекты для отмеченных задач.
- Обновлены стили для чекбоксов и элементов списка, улучшено отображение дат создания и изменения заметок.
- Обновлены обработчики событий для поддержки новых функций в интерфейсе редактирования заметок.
This commit is contained in:
Fovway 2025-10-22 08:04:41 +07:00
parent b831dcc52c
commit a77bdd3e7b
4 changed files with 715 additions and 28 deletions

View File

@ -6,11 +6,13 @@ const notesList = document.getElementById("notes-container");
// Получаем кнопки markdown
const boldBtn = document.getElementById("boldBtn");
const italicBtn = document.getElementById("italicBtn");
const colorBtn = document.getElementById("colorBtn");
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 checkboxBtn = document.getElementById("checkboxBtn");
const imageBtn = document.getElementById("imageBtn");
// Элементы для загрузки изображений
@ -224,6 +226,58 @@ noteInput.addEventListener("input", function () {
// Изначально запускаем для установки правильной высоты
autoExpandTextarea(noteInput);
// Функция для вставки цветового тега
function insertColorTag() {
// Создаем диалог выбора цвета
const colorDialog = document.createElement("input");
colorDialog.type = "color";
colorDialog.style.display = "none";
document.body.appendChild(colorDialog);
// Обработчик изменения цвета
colorDialog.addEventListener("change", function () {
const selectedColor = this.value;
insertColorMarkdown(selectedColor);
document.body.removeChild(this);
});
// Обработчик отмены
colorDialog.addEventListener("cancel", function () {
document.body.removeChild(this);
});
// Показываем диалог выбора цвета
colorDialog.click();
}
// Функция для вставки цветового markdown
function insertColorMarkdown(color) {
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);
let replacement;
if (selected.trim() === "") {
// Если текст не выделен, вставляем шаблон
replacement = `<span style="color: ${color}">Текст</span>`;
} else {
// Если текст выделен, оборачиваем его в цветовой тег
replacement = `<span style="color: ${color}">${selected}</span>`;
}
noteInput.value = before + replacement + after;
// Устанавливаем курсор после вставленного текста
const cursorPosition = start + replacement.length;
noteInput.setSelectionRange(cursorPosition, cursorPosition);
noteInput.focus();
}
// Функция для вставки markdown
function insertMarkdown(tag) {
const start = noteInput.selectionStart;
@ -248,8 +302,13 @@ function insertMarkdown(tag) {
noteInput.value = `${before}[Текст ссылки](URL)${after}`;
const cursorPosition = start + 1; // Помещаем курсор внутрь текста ссылки
noteInput.setSelectionRange(cursorPosition, cursorPosition + 12);
} else if (tag === "- " || tag === "> " || tag === "# ") {
// Для списка, цитаты и заголовка помещаем курсор после `- `, `> ` или `# `
} else if (
tag === "- " ||
tag === "> " ||
tag === "# " ||
tag === "- [ ] "
) {
// Для списка, цитаты, заголовка и чекбокса помещаем курсор после тега
noteInput.value = `${before}${tag}${after}`;
const cursorPosition = start + tag.length;
noteInput.setSelectionRange(cursorPosition, cursorPosition);
@ -266,8 +325,13 @@ function insertMarkdown(tag) {
noteInput.value = `${before}[${selected}](URL)${after}`;
const cursorPosition = start + selected.length + 3; // Помещаем курсор в URL
noteInput.setSelectionRange(cursorPosition, cursorPosition + 3);
} else if (tag === "- " || tag === "> " || tag === "# ") {
// Для списка, цитаты и заголовка добавляем `- `, `> ` или `# ` перед выделенным текстом
} else if (
tag === "- " ||
tag === "> " ||
tag === "# " ||
tag === "- [ ] "
) {
// Для списка, цитаты, заголовка и чекбокса добавляем тег перед выделенным текстом
noteInput.value = `${before}${tag}${selected}${after}`;
const cursorPosition = start + tag.length + selected.length;
noteInput.setSelectionRange(cursorPosition, cursorPosition);
@ -282,6 +346,58 @@ function insertMarkdown(tag) {
noteInput.focus();
}
// Функция для вставки цветового тега в режиме редактирования
function insertColorTagForEdit(textarea) {
// Создаем диалог выбора цвета
const colorDialog = document.createElement("input");
colorDialog.type = "color";
colorDialog.style.display = "none";
document.body.appendChild(colorDialog);
// Обработчик изменения цвета
colorDialog.addEventListener("change", function () {
const selectedColor = this.value;
insertColorMarkdownForEdit(textarea, selectedColor);
document.body.removeChild(this);
});
// Обработчик отмены
colorDialog.addEventListener("cancel", function () {
document.body.removeChild(this);
});
// Показываем диалог выбора цвета
colorDialog.click();
}
// Функция для вставки цветового markdown в режиме редактирования
function insertColorMarkdownForEdit(textarea, color) {
const start = textarea.selectionStart;
const end = textarea.selectionEnd;
const text = textarea.value;
const before = text.substring(0, start);
const selected = text.substring(start, end);
const after = text.substring(end);
let replacement;
if (selected.trim() === "") {
// Если текст не выделен, вставляем шаблон
replacement = `<span style="color: ${color}">Текст</span>`;
} else {
// Если текст выделен, оборачиваем его в цветовой тег
replacement = `<span style="color: ${color}">${selected}</span>`;
}
textarea.value = before + replacement + after;
// Устанавливаем курсор после вставленного текста
const cursorPosition = start + replacement.length;
textarea.setSelectionRange(cursorPosition, cursorPosition);
textarea.focus();
}
// Функция для вставки markdown в режиме редактирования
function insertMarkdownForEdit(textarea, tag) {
const start = textarea.selectionStart;
@ -306,8 +422,13 @@ function insertMarkdownForEdit(textarea, tag) {
textarea.value = `${before}[Текст ссылки](URL)${after}`;
const cursorPosition = start + 1; // Помещаем курсор внутрь текста ссылки
textarea.setSelectionRange(cursorPosition, cursorPosition + 12);
} else if (tag === "- " || tag === "> " || tag === "# ") {
// Для списка, цитаты и заголовка помещаем курсор после `- `, `> ` или `# `
} else if (
tag === "- " ||
tag === "> " ||
tag === "# " ||
tag === "- [ ] "
) {
// Для списка, цитаты, заголовка и чекбокса помещаем курсор после тега
textarea.value = `${before}${tag}${after}`;
const cursorPosition = start + tag.length;
textarea.setSelectionRange(cursorPosition, cursorPosition);
@ -324,8 +445,13 @@ function insertMarkdownForEdit(textarea, tag) {
textarea.value = `${before}[${selected}](URL)${after}`;
const cursorPosition = start + selected.length + 3; // Помещаем курсор в URL
textarea.setSelectionRange(cursorPosition, cursorPosition + 3);
} else if (tag === "- " || tag === "> " || tag === "# ") {
// Для списка, цитаты и заголовка добавляем `- `, `> ` или `# ` перед выделенным текстом
} else if (
tag === "- " ||
tag === "> " ||
tag === "# " ||
tag === "- [ ] "
) {
// Для списка, цитаты, заголовка и чекбокса добавляем тег перед выделенным текстом
textarea.value = `${before}${tag}${selected}${after}`;
const cursorPosition = start + tag.length + selected.length;
textarea.setSelectionRange(cursorPosition, cursorPosition);
@ -349,6 +475,10 @@ italicBtn.addEventListener("click", function () {
insertMarkdown("*");
});
colorBtn.addEventListener("click", function () {
insertColorTag();
});
headerBtn.addEventListener("click", function () {
insertMarkdown("# ");
});
@ -369,6 +499,10 @@ linkBtn.addEventListener("click", function () {
insertMarkdown("[Текст ссылки](URL)");
});
checkboxBtn.addEventListener("click", function () {
insertMarkdown("- [ ] ");
});
// Обработчик для кнопки загрузки изображений
imageBtn.addEventListener("click", function (event) {
event.preventDefault();
@ -768,6 +902,30 @@ function highlightSearchText(content, query) {
return content.replace(regex, '<span class="search-highlight">$1</span>');
}
// Настройка marked.js для поддержки чекбоксов
const renderer = new marked.Renderer();
// Переопределяем рендеринг списков, чтобы чекбоксы были кликабельными (без disabled)
const originalListItem = renderer.listitem.bind(renderer);
renderer.listitem = function (text, task, checked) {
if (task) {
// Удаляем disabled чекбокс из текста, если он есть
let cleanText = text.replace(/<input[^>]*disabled[^>]*>/gi, "").trim();
// Создаем чекбокс БЕЗ disabled атрибута
return `<li class="task-list-item"><input type="checkbox" ${
checked ? "checked" : ""
}> ${cleanText}</li>\n`;
}
return originalListItem(text, task, checked);
};
marked.setOptions({
gfm: true, // GitHub Flavored Markdown
breaks: true,
renderer: renderer,
html: true, // Разрешить HTML теги
});
// Функция для отображения заметок
async function renderNotes(notes) {
notesList.innerHTML = "";
@ -835,10 +993,31 @@ async function renderNotes(notes) {
imagesHtml += "</div>";
}
// Форматируем дату создания и дату изменения
let dateDisplay = `${note.date} ${note.time}`;
if (note.updated_at && note.created_at !== note.updated_at) {
// Если дата изменения отличается от даты создания, показываем обе даты
const createdDate = new Date(note.created_at);
const updatedDate = new Date(note.updated_at);
const formatDate = (date) => {
const day = String(date.getDate()).padStart(2, "0");
const month = String(date.getMonth() + 1).padStart(2, "0");
const year = date.getFullYear();
const hours = String(date.getHours()).padStart(2, "0");
const minutes = String(date.getMinutes()).padStart(2, "0");
return `${day}.${month}.${year} ${hours}:${minutes}`;
};
dateDisplay = `Создано: ${formatDate(
createdDate
)}<br>Изменено: ${formatDate(updatedDate)}`;
}
const noteHtml = `
<div id="note" class="container" data-note-id="${note.id}">
<div class="date">
${note.date} ${note.time}
${dateDisplay}
<div id="editBtn" class="notesHeaderBtn" data-id="${
note.id
}">Редактировать</div>
@ -853,7 +1032,7 @@ async function renderNotes(notes) {
${imagesHtml}
</div>
`;
notesList.insertAdjacentHTML("afterbegin", noteHtml);
notesList.insertAdjacentHTML("beforeend", noteHtml);
}
// Добавляем обработчики событий для кнопок редактирования и удаления
@ -865,6 +1044,9 @@ async function renderNotes(notes) {
// Добавляем обработчики для изображений в заметках
addImageEventListeners();
// Добавляем обработчики для чекбоксов в заметках
addCheckboxEventListeners();
// Обрабатываем длинные заметки
handleLongNotes();
@ -966,11 +1148,17 @@ function addNoteEventListeners() {
const markdownButtons = [
{ id: "editBoldBtn", icon: "mdi:format-bold", tag: "**" },
{ id: "editItalicBtn", icon: "mdi:format-italic", tag: "*" },
{ id: "editColorBtn", icon: "mdi:palette", tag: "color" },
{ 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)" },
{
id: "editCheckboxBtn",
icon: "mdi:checkbox-marked-outline",
tag: "- [ ] ",
},
{ id: "editImageBtn", icon: "mdi:image-plus", tag: "image" },
];
@ -994,6 +1182,121 @@ function addNoteEventListeners() {
autoExpandTextarea(textarea);
});
// Добавляем обработчик клавиатуры для автоматического продолжения списков
textarea.addEventListener("keydown", function (event) {
if (event.key === "Enter") {
// Автоматическое продолжение списков в режиме редактирования
const start = textarea.selectionStart;
const text = textarea.value;
const lines = text.split("\n");
// Определяем текущую строку
let currentLineIndex = 0;
let currentLineStart = 0;
let currentLine = "";
for (let i = 0; i < lines.length; i++) {
const lineLength = lines[i].length;
if (currentLineStart + lineLength >= start) {
currentLineIndex = i;
currentLine = lines[i];
break;
}
currentLineStart += lineLength + 1; // +1 для символа новой строки
}
// Проверяем, является ли текущая строка списком
const listPatterns = [
/^(\s*)- \[ \] /, // Чекбокс (не отмечен): - [ ]
/^(\s*)- \[x\] /i, // Чекбокс (отмечен): - [x]
/^(\s*)- /, // Неупорядоченный список: -
/^(\s*)\* /, // Неупорядоченный список: *
/^(\s*)\+ /, // Неупорядоченный список: +
/^(\s*)(\d+)\. /, // Упорядоченный список: 1. 2. 3.
/^(\s*)(\w+)\. /, // Буквенный список: a. b. c.
];
let listMatch = null;
let listType = null;
for (const pattern of listPatterns) {
const match = currentLine.match(pattern);
if (match) {
listMatch = match;
if (pattern === listPatterns[0] || pattern === listPatterns[1]) {
listType = "checkbox";
} else if (
pattern === listPatterns[2] ||
pattern === listPatterns[3] ||
pattern === listPatterns[4]
) {
listType = "unordered";
} else {
listType = "ordered";
}
break;
}
}
if (listMatch) {
event.preventDefault();
const indent = listMatch[1] || ""; // Отступы перед маркером
const marker = listMatch[0].slice(indent.length); // Маркер списка без отступов
// Получаем текст после маркера
const afterMarker = currentLine.slice(listMatch[0].length);
if (afterMarker.trim() === "") {
// Если строка пустая после маркера, выходим из списка
const beforeCursor = text.substring(0, start);
const afterCursor = text.substring(start);
// Удаляем маркер и отступы текущей строки
const newBefore = beforeCursor.replace(
/\n\s*- \[ \] \s*$|\n\s*- \[x\] \s*$|\n\s*[-*+]\s*$|\n\s*\d+\.\s*$|\n\s*\w+\.\s*$/i,
"\n"
);
textarea.value = newBefore + afterCursor;
// Устанавливаем курсор после удаленного маркера
const newCursorPos = newBefore.length;
textarea.setSelectionRange(newCursorPos, newCursorPos);
} else {
// Продолжаем список
const beforeCursor = text.substring(0, start);
const afterCursor = text.substring(start);
let newMarker = "";
if (listType === "checkbox") {
// Для чекбоксов всегда создаем новый пустой чекбокс
newMarker = indent + "- [ ] ";
} else if (listType === "unordered") {
newMarker = indent + marker;
} else if (listType === "ordered") {
// Для упорядоченных списков увеличиваем номер
const number = parseInt(listMatch[2]);
const nextNumber = number + 1;
const numberStr = listMatch[2].replace(
/\d+/,
nextNumber.toString()
);
newMarker = indent + numberStr + ". ";
}
textarea.value = beforeCursor + "\n" + newMarker + afterCursor;
// Устанавливаем курсор после нового маркера
const newCursorPos = start + 1 + newMarker.length;
textarea.setSelectionRange(newCursorPos, newCursorPos);
}
// Обновляем высоту textarea
autoExpandTextarea(textarea);
}
}
});
// Создаем элементы для загрузки изображений в режиме редактирования
const editImageInput = document.createElement("input");
editImageInput.type = "file";
@ -1109,7 +1412,6 @@ function addNoteEventListeners() {
const saveEditNote = async function () {
if (textarea.value.trim() !== "" || editSelectedImages.length > 0) {
try {
const { date, time } = getFormattedDateTime();
const response = await fetch(`/api/notes/${noteId}`, {
method: "PUT",
headers: {
@ -1117,8 +1419,6 @@ function addNoteEventListeners() {
},
body: JSON.stringify({
content: textarea.value || " ", // Минимальный контент, если только изображения
date: date,
time: time,
}),
});
@ -1190,6 +1490,9 @@ function addNoteEventListeners() {
if (button.tag === "image") {
// Для кнопки изображения открываем диалог выбора файлов
editImageInput.click();
} else if (button.tag === "color") {
// Для кнопки цвета открываем диалог выбора цвета
insertColorTagForEdit(textarea);
} else {
insertMarkdownForEdit(textarea, button.tag);
}
@ -1284,6 +1587,142 @@ function addImageEventListeners() {
});
}
// Функция для применения визуальных стилей к чекбоксу
function applyCheckboxStyles(checkbox, checked) {
const parentLi = checkbox.closest("li");
if (!parentLi) return;
if (checked) {
parentLi.style.opacity = "0.65";
// Находим все элементы кроме чекбокса
const textNodes = parentLi.querySelectorAll("*:not(input)");
textNodes.forEach((node) => {
node.style.textDecoration = "line-through";
node.style.color = "#999";
});
// Обрабатываем текстовые узлы
Array.from(parentLi.childNodes).forEach((node) => {
if (node.nodeType === 3 && node.textContent.trim()) {
const span = document.createElement("span");
span.style.textDecoration = "line-through";
span.style.color = "#999";
span.textContent = node.textContent;
parentLi.replaceChild(span, node);
}
});
} else {
parentLi.style.opacity = "1";
const textNodes = parentLi.querySelectorAll("*:not(input)");
textNodes.forEach((node) => {
node.style.textDecoration = "none";
node.style.color = "";
});
}
}
// Функция для добавления обработчиков для чекбоксов в заметках
function addCheckboxEventListeners() {
// Находим все чекбоксы в заметках
document
.querySelectorAll(".textNote input[type='checkbox']")
.forEach((checkbox) => {
// Применяем начальные стили для уже отмеченных чекбоксов
if (checkbox.checked) {
applyCheckboxStyles(checkbox, true);
}
// Удаляем старые обработчики, если они есть
if (checkbox._checkboxHandler) {
checkbox.removeEventListener("change", checkbox._checkboxHandler);
}
// Создаем новый обработчик
checkbox._checkboxHandler = async function (event) {
// Применяем визуальные эффекты сразу
applyCheckboxStyles(this, this.checked);
// Находим родительскую заметку
const noteContainer = this.closest("#note");
if (!noteContainer) return;
const noteId = noteContainer.dataset.noteId;
const textNoteElement = noteContainer.querySelector(".textNote");
// Получаем исходный markdown контент
let originalContent = textNoteElement.dataset.originalContent;
if (!originalContent) return;
// Находим индекс чекбокса в списке всех чекбоксов этой заметки
const allCheckboxes = textNoteElement.querySelectorAll(
"input[type='checkbox']"
);
const checkboxIndex = Array.from(allCheckboxes).indexOf(this);
// Находим все чекбоксы в markdown контенте
const checkboxRegex = /- \[([ x])\]/gi;
let matches = [];
let match;
while ((match = checkboxRegex.exec(originalContent)) !== null) {
matches.push({
index: match.index,
checked: match[1].toLowerCase() === "x",
fullMatch: match[0],
});
}
// Если нашли соответствующий чекбокс, изменяем его состояние
if (matches[checkboxIndex]) {
const targetMatch = matches[checkboxIndex];
const newState = this.checked ? "- [x]" : "- [ ]";
// Заменяем состояние чекбокса в исходном тексте
const beforeCheckbox = originalContent.substring(
0,
targetMatch.index
);
const afterCheckbox = originalContent.substring(
targetMatch.index + targetMatch.fullMatch.length
);
originalContent = beforeCheckbox + newState + afterCheckbox;
// Обновляем data-атрибут
textNoteElement.dataset.originalContent = originalContent;
// Автоматически сохраняем изменения на сервере
try {
const response = await fetch(`/api/notes/${noteId}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
content: originalContent,
}),
});
if (!response.ok) {
throw new Error("Ошибка сохранения заметки");
}
// Обновляем кэш заметок
const noteInCache = allNotes.find((n) => n.id == noteId);
if (noteInCache) {
noteInCache.content = originalContent;
}
} catch (error) {
console.error("Ошибка автосохранения чекбокса:", error);
// Если не удалось сохранить, возвращаем чекбокс в прежнее состояние
this.checked = !this.checked;
alert("Ошибка сохранения изменений");
}
}
};
checkbox.addEventListener("change", checkbox._checkboxHandler);
});
}
// Функция сохранения заметки (вынесена отдельно для повторного использования)
async function saveNote() {
if (noteInput.value.trim() !== "" || selectedImages.length > 0) {
@ -1409,6 +1848,114 @@ noteInput.addEventListener("keydown", function (event) {
if (event.altKey && event.key === "Enter") {
event.preventDefault(); // Предотвращаем стандартное поведение
saveNote();
} else if (event.key === "Enter") {
// Автоматическое продолжение списков
const textarea = event.target;
const start = textarea.selectionStart;
const text = textarea.value;
const lines = text.split("\n");
// Определяем текущую строку
let currentLineIndex = 0;
let currentLineStart = 0;
let currentLine = "";
for (let i = 0; i < lines.length; i++) {
const lineLength = lines[i].length;
if (currentLineStart + lineLength >= start) {
currentLineIndex = i;
currentLine = lines[i];
break;
}
currentLineStart += lineLength + 1; // +1 для символа новой строки
}
// Проверяем, является ли текущая строка списком
const listPatterns = [
/^(\s*)- \[ \] /, // Чекбокс (не отмечен): - [ ]
/^(\s*)- \[x\] /i, // Чекбокс (отмечен): - [x]
/^(\s*)- /, // Неупорядоченный список: -
/^(\s*)\* /, // Неупорядоченный список: *
/^(\s*)\+ /, // Неупорядоченный список: +
/^(\s*)(\d+)\. /, // Упорядоченный список: 1. 2. 3.
/^(\s*)(\w+)\. /, // Буквенный список: a. b. c.
];
let listMatch = null;
let listType = null;
for (const pattern of listPatterns) {
const match = currentLine.match(pattern);
if (match) {
listMatch = match;
if (pattern === listPatterns[0] || pattern === listPatterns[1]) {
listType = "checkbox";
} else if (
pattern === listPatterns[2] ||
pattern === listPatterns[3] ||
pattern === listPatterns[4]
) {
listType = "unordered";
} else {
listType = "ordered";
}
break;
}
}
if (listMatch) {
event.preventDefault();
const indent = listMatch[1] || ""; // Отступы перед маркером
const marker = listMatch[0].slice(indent.length); // Маркер списка без отступов
// Получаем текст после маркера
const afterMarker = currentLine.slice(listMatch[0].length);
if (afterMarker.trim() === "") {
// Если строка пустая после маркера, выходим из списка
const beforeCursor = text.substring(0, start);
const afterCursor = text.substring(start);
// Удаляем маркер и отступы текущей строки
const newBefore = beforeCursor.replace(
/\n\s*- \[ \] \s*$|\n\s*- \[x\] \s*$|\n\s*[-*+]\s*$|\n\s*\d+\.\s*$|\n\s*\w+\.\s*$/i,
"\n"
);
textarea.value = newBefore + afterCursor;
// Устанавливаем курсор после удаленного маркера
const newCursorPos = newBefore.length;
textarea.setSelectionRange(newCursorPos, newCursorPos);
} else {
// Продолжаем список
const beforeCursor = text.substring(0, start);
const afterCursor = text.substring(start);
let newMarker = "";
if (listType === "checkbox") {
// Для чекбоксов всегда создаем новый пустой чекбокс
newMarker = indent + "- [ ] ";
} else if (listType === "unordered") {
newMarker = indent + marker;
} else if (listType === "ordered") {
// Для упорядоченных списков увеличиваем номер
const number = parseInt(listMatch[2]);
const nextNumber = number + 1;
const numberStr = listMatch[2].replace(/\d+/, nextNumber.toString());
newMarker = indent + numberStr + ". ";
}
textarea.value = beforeCursor + "\n" + newMarker + afterCursor;
// Устанавливаем курсор после нового маркера
const newCursorPos = start + 1 + newMarker.length;
textarea.setSelectionRange(newCursorPos, newCursorPos);
}
// Обновляем высоту textarea
autoExpandTextarea(textarea);
}
}
});

View File

@ -258,6 +258,9 @@
<button class="btnMarkdown" id="italicBtn" title="Курсив">
<span class="iconify" data-icon="mdi:format-italic"></span>
</button>
<button class="btnMarkdown" id="colorBtn" title="Цвет текста">
<span class="iconify" data-icon="mdi:palette"></span>
</button>
<button class="btnMarkdown" id="headerBtn" title="Заголовок">
<span class="iconify" data-icon="mdi:format-header-1"></span>
</button>
@ -273,6 +276,12 @@
<button class="btnMarkdown" id="linkBtn" title="Ссылка">
<span class="iconify" data-icon="mdi:link"></span>
</button>
<button class="btnMarkdown" id="checkboxBtn" title="To-Do список">
<span
class="iconify"
data-icon="mdi:checkbox-marked-outline"
></span>
</button>
<button
class="btnMarkdown"
id="imageBtn"

View File

@ -106,6 +106,10 @@ header .iconify[data-icon="mdi:account-plus"] {
color: #757575;
}
.btnMarkdown .iconify[data-icon="mdi:palette"] {
color: #e91e63;
}
.btnMarkdown .iconify[data-icon="mdi:format-header-1"] {
color: #1976d2;
}
@ -372,6 +376,11 @@ textarea:focus {
.date {
font-size: 11px;
color: grey;
white-space: pre-line;
display: flex;
align-items: flex-start;
gap: 10px;
flex-wrap: wrap;
}
.notesHeaderBtn {
@ -379,7 +388,7 @@ textarea:focus {
cursor: pointer;
color: black;
font-weight: bold;
margin-left: 10px;
white-space: nowrap;
}
.textNote {
@ -458,12 +467,14 @@ textarea:focus {
padding-left: 20px;
word-wrap: break-word;
overflow-wrap: break-word;
line-height: 0.5;
}
/* Убираем маргины у элементов списка */
.textNote li {
margin: 0;
padding: 0;
line-height: 0.5;
word-wrap: break-word;
overflow-wrap: break-word;
}
@ -520,6 +531,86 @@ textarea:focus {
word-break: break-word;
}
/* Стили для чекбоксов (to-do списков) */
.textNote input[type="checkbox"] {
cursor: pointer;
margin-right: 8px;
width: 18px;
height: 18px;
accent-color: var(--accent-color, #007bff);
vertical-align: middle;
position: relative;
top: -1px;
}
/* Стили для элементов списка с чекбоксами (task-list-item создается marked.js) */
.textNote .task-list-item {
list-style-type: none;
margin-left: -20px;
padding: 0;
line-height: 0.5;
transition: all 0.3s ease;
}
/* Перечеркивание текста для выполненных задач */
.textNote .task-list-item:has(input[type="checkbox"]:checked) {
opacity: 0.65;
}
.textNote .task-list-item input[type="checkbox"]:checked ~ * {
text-decoration: line-through;
color: #999;
}
/* Альтернативный вариант для перечеркивания всего текста в элементе */
.textNote input[type="checkbox"]:checked {
text-decoration: none;
}
.textNote input[type="checkbox"]:checked + * {
text-decoration: line-through;
color: #999;
}
/* Hover эффект для чекбоксов */
.textNote .task-list-item:hover {
background-color: rgba(0, 123, 255, 0.05);
border-radius: 4px;
padding-left: 4px;
margin-left: -24px;
}
/* Если marked.js не добавляет класс task-list-item, используем :has селектор */
.textNote li:has(input[type="checkbox"]) {
list-style-type: none;
margin-left: -20px;
padding: 0;
line-height: 0.5;
transition: all 0.3s ease;
}
.textNote li:has(input[type="checkbox"]:checked) {
opacity: 0.65;
}
.textNote li:has(input[type="checkbox"]) input[type="checkbox"]:checked {
text-decoration: none;
}
.textNote li:has(input[type="checkbox"]:checked) label,
.textNote li:has(input[type="checkbox"]:checked) span,
.textNote li:has(input[type="checkbox"]:checked) p {
text-decoration: line-through;
color: #999;
}
.textNote li:has(input[type="checkbox"]):hover {
background-color: rgba(0, 123, 255, 0.05);
border-radius: 4px;
padding-left: 4px;
margin-left: -24px;
}
.notes-container {
width: 100%;
display: flex;

View File

@ -246,7 +246,6 @@ function createTables() {
console.error("Ошибка создания таблицы пользователей:", err.message);
} else {
console.log("Таблица пользователей готова");
createIndexes();
runMigrations();
}
});
@ -257,6 +256,7 @@ function createIndexes() {
const indexes = [
"CREATE INDEX IF NOT EXISTS idx_notes_user_id ON notes(user_id)",
"CREATE INDEX IF NOT EXISTS idx_notes_created_at ON notes(created_at)",
"CREATE INDEX IF NOT EXISTS idx_notes_updated_at ON notes(updated_at)",
"CREATE INDEX IF NOT EXISTS idx_notes_date ON notes(date)",
"CREATE INDEX IF NOT EXISTS idx_note_images_note_id ON note_images(note_id)",
"CREATE INDEX IF NOT EXISTS idx_users_username ON users(username)",
@ -299,6 +299,46 @@ function runMigrations() {
);
}
});
// Проверяем существование колонки updated_at в таблице notes и добавляем её если нужно
db.all("PRAGMA table_info(notes)", (err, columns) => {
if (err) {
console.error("Ошибка проверки структуры таблицы notes:", err.message);
return;
}
const hasUpdatedAt = columns.some((col) => col.name === "updated_at");
if (!hasUpdatedAt) {
db.run("ALTER TABLE notes ADD COLUMN updated_at DATETIME", (err) => {
if (err) {
console.error("Ошибка добавления колонки updated_at:", err.message);
} else {
console.log("Колонка updated_at добавлена в таблицу notes");
// Устанавливаем updated_at = created_at для существующих записей
db.run(
"UPDATE notes SET updated_at = created_at WHERE updated_at IS NULL",
(err) => {
if (err) {
console.error(
"Ошибка обновления updated_at для существующих записей:",
err.message
);
} else {
console.log(
"Колонка updated_at обновлена для существующих записей"
);
// Создаем индексы после добавления колонки
createIndexes();
}
}
);
}
});
} else {
// Если колонка уже существует, просто создаем индексы
createIndexes();
}
});
}
// Middleware для аутентификации
@ -592,7 +632,7 @@ app.get("/api/notes", requireAuth, (req, res) => {
LEFT JOIN note_images ni ON n.id = ni.note_id
WHERE n.user_id = ?
GROUP BY n.id
ORDER BY n.created_at ASC
ORDER BY n.created_at DESC
`;
db.all(sql, [req.session.userId], (err, rows) => {
@ -620,7 +660,7 @@ app.post("/api/notes", requireAuth, (req, res) => {
}
const sql =
"INSERT INTO notes (user_id, content, date, time) VALUES (?, ?, ?, ?)";
"INSERT INTO notes (user_id, content, date, time, created_at, updated_at) VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)";
const params = [req.session.userId, content, date, time];
db.run(sql, params, function (err) {
@ -634,15 +674,15 @@ app.post("/api/notes", requireAuth, (req, res) => {
// API для обновления заметки
app.put("/api/notes/:id", requireAuth, (req, res) => {
const { content, date, time } = req.body;
const { content } = req.body;
const { id } = req.params;
if (!content || !date || !time) {
return res.status(400).json({ error: "Не все поля заполнены" });
if (!content) {
return res.status(400).json({ error: "Содержание заметки обязательно" });
}
// Проверяем, что заметка принадлежит текущему пользователю
const checkSql = "SELECT user_id FROM notes WHERE id = ?";
const checkSql = "SELECT user_id, date, time FROM notes WHERE id = ?";
db.get(checkSql, [id], (err, row) => {
if (err) {
console.error("Ошибка проверки доступа:", err.message);
@ -658,8 +698,8 @@ app.put("/api/notes/:id", requireAuth, (req, res) => {
}
const updateSql =
"UPDATE notes SET content = ?, date = ?, time = ? WHERE id = ?";
const params = [content, date, time, id];
"UPDATE notes SET content = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?";
const params = [content, id];
db.run(updateSql, params, function (err) {
if (err) {
@ -669,7 +709,7 @@ app.put("/api/notes/:id", requireAuth, (req, res) => {
if (this.changes === 0) {
return res.status(404).json({ error: "Заметка не найдена" });
}
res.json({ id, content, date, time });
res.json({ id, content, date: row.date, time: row.time });
});
});
});