✨ Добавлены функции для работы с цветом текста и чекбоксами в заметках
- Реализована возможность вставки цветового тега в текст заметок с помощью диалога выбора цвета. - Добавлены функции для работы с чекбоксами, включая автоматическое продолжение списков и визуальные эффекты для отмеченных задач. - Обновлены стили для чекбоксов и элементов списка, улучшено отображение дат создания и изменения заметок. - Обновлены обработчики событий для поддержки новых функций в интерфейсе редактирования заметок.
This commit is contained in:
parent
b831dcc52c
commit
a77bdd3e7b
573
public/app.js
573
public/app.js
@ -6,11 +6,13 @@ const notesList = document.getElementById("notes-container");
|
|||||||
// Получаем кнопки markdown
|
// Получаем кнопки markdown
|
||||||
const boldBtn = document.getElementById("boldBtn");
|
const boldBtn = document.getElementById("boldBtn");
|
||||||
const italicBtn = document.getElementById("italicBtn");
|
const italicBtn = document.getElementById("italicBtn");
|
||||||
|
const colorBtn = document.getElementById("colorBtn");
|
||||||
const headerBtn = document.getElementById("headerBtn");
|
const headerBtn = document.getElementById("headerBtn");
|
||||||
const listBtn = document.getElementById("listBtn");
|
const listBtn = document.getElementById("listBtn");
|
||||||
const quoteBtn = document.getElementById("quoteBtn");
|
const quoteBtn = document.getElementById("quoteBtn");
|
||||||
const codeBtn = document.getElementById("codeBtn");
|
const codeBtn = document.getElementById("codeBtn");
|
||||||
const linkBtn = document.getElementById("linkBtn");
|
const linkBtn = document.getElementById("linkBtn");
|
||||||
|
const checkboxBtn = document.getElementById("checkboxBtn");
|
||||||
const imageBtn = document.getElementById("imageBtn");
|
const imageBtn = document.getElementById("imageBtn");
|
||||||
|
|
||||||
// Элементы для загрузки изображений
|
// Элементы для загрузки изображений
|
||||||
@ -224,6 +226,58 @@ noteInput.addEventListener("input", function () {
|
|||||||
// Изначально запускаем для установки правильной высоты
|
// Изначально запускаем для установки правильной высоты
|
||||||
autoExpandTextarea(noteInput);
|
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
|
// Функция для вставки markdown
|
||||||
function insertMarkdown(tag) {
|
function insertMarkdown(tag) {
|
||||||
const start = noteInput.selectionStart;
|
const start = noteInput.selectionStart;
|
||||||
@ -248,8 +302,13 @@ function insertMarkdown(tag) {
|
|||||||
noteInput.value = `${before}[Текст ссылки](URL)${after}`;
|
noteInput.value = `${before}[Текст ссылки](URL)${after}`;
|
||||||
const cursorPosition = start + 1; // Помещаем курсор внутрь текста ссылки
|
const cursorPosition = start + 1; // Помещаем курсор внутрь текста ссылки
|
||||||
noteInput.setSelectionRange(cursorPosition, cursorPosition + 12);
|
noteInput.setSelectionRange(cursorPosition, cursorPosition + 12);
|
||||||
} else if (tag === "- " || tag === "> " || tag === "# ") {
|
} else if (
|
||||||
// Для списка, цитаты и заголовка помещаем курсор после `- `, `> ` или `# `
|
tag === "- " ||
|
||||||
|
tag === "> " ||
|
||||||
|
tag === "# " ||
|
||||||
|
tag === "- [ ] "
|
||||||
|
) {
|
||||||
|
// Для списка, цитаты, заголовка и чекбокса помещаем курсор после тега
|
||||||
noteInput.value = `${before}${tag}${after}`;
|
noteInput.value = `${before}${tag}${after}`;
|
||||||
const cursorPosition = start + tag.length;
|
const cursorPosition = start + tag.length;
|
||||||
noteInput.setSelectionRange(cursorPosition, cursorPosition);
|
noteInput.setSelectionRange(cursorPosition, cursorPosition);
|
||||||
@ -266,8 +325,13 @@ function insertMarkdown(tag) {
|
|||||||
noteInput.value = `${before}[${selected}](URL)${after}`;
|
noteInput.value = `${before}[${selected}](URL)${after}`;
|
||||||
const cursorPosition = start + selected.length + 3; // Помещаем курсор в URL
|
const cursorPosition = start + selected.length + 3; // Помещаем курсор в URL
|
||||||
noteInput.setSelectionRange(cursorPosition, cursorPosition + 3);
|
noteInput.setSelectionRange(cursorPosition, cursorPosition + 3);
|
||||||
} else if (tag === "- " || tag === "> " || tag === "# ") {
|
} else if (
|
||||||
// Для списка, цитаты и заголовка добавляем `- `, `> ` или `# ` перед выделенным текстом
|
tag === "- " ||
|
||||||
|
tag === "> " ||
|
||||||
|
tag === "# " ||
|
||||||
|
tag === "- [ ] "
|
||||||
|
) {
|
||||||
|
// Для списка, цитаты, заголовка и чекбокса добавляем тег перед выделенным текстом
|
||||||
noteInput.value = `${before}${tag}${selected}${after}`;
|
noteInput.value = `${before}${tag}${selected}${after}`;
|
||||||
const cursorPosition = start + tag.length + selected.length;
|
const cursorPosition = start + tag.length + selected.length;
|
||||||
noteInput.setSelectionRange(cursorPosition, cursorPosition);
|
noteInput.setSelectionRange(cursorPosition, cursorPosition);
|
||||||
@ -282,6 +346,58 @@ function insertMarkdown(tag) {
|
|||||||
noteInput.focus();
|
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 в режиме редактирования
|
// Функция для вставки markdown в режиме редактирования
|
||||||
function insertMarkdownForEdit(textarea, tag) {
|
function insertMarkdownForEdit(textarea, tag) {
|
||||||
const start = textarea.selectionStart;
|
const start = textarea.selectionStart;
|
||||||
@ -306,8 +422,13 @@ function insertMarkdownForEdit(textarea, tag) {
|
|||||||
textarea.value = `${before}[Текст ссылки](URL)${after}`;
|
textarea.value = `${before}[Текст ссылки](URL)${after}`;
|
||||||
const cursorPosition = start + 1; // Помещаем курсор внутрь текста ссылки
|
const cursorPosition = start + 1; // Помещаем курсор внутрь текста ссылки
|
||||||
textarea.setSelectionRange(cursorPosition, cursorPosition + 12);
|
textarea.setSelectionRange(cursorPosition, cursorPosition + 12);
|
||||||
} else if (tag === "- " || tag === "> " || tag === "# ") {
|
} else if (
|
||||||
// Для списка, цитаты и заголовка помещаем курсор после `- `, `> ` или `# `
|
tag === "- " ||
|
||||||
|
tag === "> " ||
|
||||||
|
tag === "# " ||
|
||||||
|
tag === "- [ ] "
|
||||||
|
) {
|
||||||
|
// Для списка, цитаты, заголовка и чекбокса помещаем курсор после тега
|
||||||
textarea.value = `${before}${tag}${after}`;
|
textarea.value = `${before}${tag}${after}`;
|
||||||
const cursorPosition = start + tag.length;
|
const cursorPosition = start + tag.length;
|
||||||
textarea.setSelectionRange(cursorPosition, cursorPosition);
|
textarea.setSelectionRange(cursorPosition, cursorPosition);
|
||||||
@ -324,8 +445,13 @@ function insertMarkdownForEdit(textarea, tag) {
|
|||||||
textarea.value = `${before}[${selected}](URL)${after}`;
|
textarea.value = `${before}[${selected}](URL)${after}`;
|
||||||
const cursorPosition = start + selected.length + 3; // Помещаем курсор в URL
|
const cursorPosition = start + selected.length + 3; // Помещаем курсор в URL
|
||||||
textarea.setSelectionRange(cursorPosition, cursorPosition + 3);
|
textarea.setSelectionRange(cursorPosition, cursorPosition + 3);
|
||||||
} else if (tag === "- " || tag === "> " || tag === "# ") {
|
} else if (
|
||||||
// Для списка, цитаты и заголовка добавляем `- `, `> ` или `# ` перед выделенным текстом
|
tag === "- " ||
|
||||||
|
tag === "> " ||
|
||||||
|
tag === "# " ||
|
||||||
|
tag === "- [ ] "
|
||||||
|
) {
|
||||||
|
// Для списка, цитаты, заголовка и чекбокса добавляем тег перед выделенным текстом
|
||||||
textarea.value = `${before}${tag}${selected}${after}`;
|
textarea.value = `${before}${tag}${selected}${after}`;
|
||||||
const cursorPosition = start + tag.length + selected.length;
|
const cursorPosition = start + tag.length + selected.length;
|
||||||
textarea.setSelectionRange(cursorPosition, cursorPosition);
|
textarea.setSelectionRange(cursorPosition, cursorPosition);
|
||||||
@ -349,6 +475,10 @@ italicBtn.addEventListener("click", function () {
|
|||||||
insertMarkdown("*");
|
insertMarkdown("*");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
colorBtn.addEventListener("click", function () {
|
||||||
|
insertColorTag();
|
||||||
|
});
|
||||||
|
|
||||||
headerBtn.addEventListener("click", function () {
|
headerBtn.addEventListener("click", function () {
|
||||||
insertMarkdown("# ");
|
insertMarkdown("# ");
|
||||||
});
|
});
|
||||||
@ -369,6 +499,10 @@ linkBtn.addEventListener("click", function () {
|
|||||||
insertMarkdown("[Текст ссылки](URL)");
|
insertMarkdown("[Текст ссылки](URL)");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
checkboxBtn.addEventListener("click", function () {
|
||||||
|
insertMarkdown("- [ ] ");
|
||||||
|
});
|
||||||
|
|
||||||
// Обработчик для кнопки загрузки изображений
|
// Обработчик для кнопки загрузки изображений
|
||||||
imageBtn.addEventListener("click", function (event) {
|
imageBtn.addEventListener("click", function (event) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
@ -768,6 +902,30 @@ function highlightSearchText(content, query) {
|
|||||||
return content.replace(regex, '<span class="search-highlight">$1</span>');
|
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) {
|
async function renderNotes(notes) {
|
||||||
notesList.innerHTML = "";
|
notesList.innerHTML = "";
|
||||||
@ -835,10 +993,31 @@ async function renderNotes(notes) {
|
|||||||
imagesHtml += "</div>";
|
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 = `
|
const noteHtml = `
|
||||||
<div id="note" class="container" data-note-id="${note.id}">
|
<div id="note" class="container" data-note-id="${note.id}">
|
||||||
<div class="date">
|
<div class="date">
|
||||||
${note.date} ${note.time}
|
${dateDisplay}
|
||||||
<div id="editBtn" class="notesHeaderBtn" data-id="${
|
<div id="editBtn" class="notesHeaderBtn" data-id="${
|
||||||
note.id
|
note.id
|
||||||
}">Редактировать</div>
|
}">Редактировать</div>
|
||||||
@ -853,7 +1032,7 @@ async function renderNotes(notes) {
|
|||||||
${imagesHtml}
|
${imagesHtml}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
notesList.insertAdjacentHTML("afterbegin", noteHtml);
|
notesList.insertAdjacentHTML("beforeend", noteHtml);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Добавляем обработчики событий для кнопок редактирования и удаления
|
// Добавляем обработчики событий для кнопок редактирования и удаления
|
||||||
@ -865,6 +1044,9 @@ async function renderNotes(notes) {
|
|||||||
// Добавляем обработчики для изображений в заметках
|
// Добавляем обработчики для изображений в заметках
|
||||||
addImageEventListeners();
|
addImageEventListeners();
|
||||||
|
|
||||||
|
// Добавляем обработчики для чекбоксов в заметках
|
||||||
|
addCheckboxEventListeners();
|
||||||
|
|
||||||
// Обрабатываем длинные заметки
|
// Обрабатываем длинные заметки
|
||||||
handleLongNotes();
|
handleLongNotes();
|
||||||
|
|
||||||
@ -966,11 +1148,17 @@ function addNoteEventListeners() {
|
|||||||
const markdownButtons = [
|
const markdownButtons = [
|
||||||
{ id: "editBoldBtn", icon: "mdi:format-bold", tag: "**" },
|
{ id: "editBoldBtn", icon: "mdi:format-bold", tag: "**" },
|
||||||
{ id: "editItalicBtn", icon: "mdi:format-italic", tag: "*" },
|
{ id: "editItalicBtn", icon: "mdi:format-italic", tag: "*" },
|
||||||
|
{ id: "editColorBtn", icon: "mdi:palette", tag: "color" },
|
||||||
{ id: "editHeaderBtn", icon: "mdi:format-header-1", tag: "# " },
|
{ id: "editHeaderBtn", icon: "mdi:format-header-1", tag: "# " },
|
||||||
{ id: "editListBtn", icon: "mdi:format-list-bulleted", tag: "- " },
|
{ id: "editListBtn", icon: "mdi:format-list-bulleted", tag: "- " },
|
||||||
{ id: "editQuoteBtn", icon: "mdi:format-quote-close", tag: "> " },
|
{ id: "editQuoteBtn", icon: "mdi:format-quote-close", tag: "> " },
|
||||||
{ id: "editCodeBtn", icon: "mdi:code-tags", tag: "`" },
|
{ id: "editCodeBtn", icon: "mdi:code-tags", tag: "`" },
|
||||||
{ id: "editLinkBtn", icon: "mdi:link", tag: "[Текст ссылки](URL)" },
|
{ id: "editLinkBtn", icon: "mdi:link", tag: "[Текст ссылки](URL)" },
|
||||||
|
{
|
||||||
|
id: "editCheckboxBtn",
|
||||||
|
icon: "mdi:checkbox-marked-outline",
|
||||||
|
tag: "- [ ] ",
|
||||||
|
},
|
||||||
{ id: "editImageBtn", icon: "mdi:image-plus", tag: "image" },
|
{ id: "editImageBtn", icon: "mdi:image-plus", tag: "image" },
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -994,6 +1182,121 @@ function addNoteEventListeners() {
|
|||||||
autoExpandTextarea(textarea);
|
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");
|
const editImageInput = document.createElement("input");
|
||||||
editImageInput.type = "file";
|
editImageInput.type = "file";
|
||||||
@ -1109,7 +1412,6 @@ function addNoteEventListeners() {
|
|||||||
const saveEditNote = async function () {
|
const saveEditNote = async function () {
|
||||||
if (textarea.value.trim() !== "" || editSelectedImages.length > 0) {
|
if (textarea.value.trim() !== "" || editSelectedImages.length > 0) {
|
||||||
try {
|
try {
|
||||||
const { date, time } = getFormattedDateTime();
|
|
||||||
const response = await fetch(`/api/notes/${noteId}`, {
|
const response = await fetch(`/api/notes/${noteId}`, {
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
headers: {
|
headers: {
|
||||||
@ -1117,8 +1419,6 @@ function addNoteEventListeners() {
|
|||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
content: textarea.value || " ", // Минимальный контент, если только изображения
|
content: textarea.value || " ", // Минимальный контент, если только изображения
|
||||||
date: date,
|
|
||||||
time: time,
|
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -1190,6 +1490,9 @@ function addNoteEventListeners() {
|
|||||||
if (button.tag === "image") {
|
if (button.tag === "image") {
|
||||||
// Для кнопки изображения открываем диалог выбора файлов
|
// Для кнопки изображения открываем диалог выбора файлов
|
||||||
editImageInput.click();
|
editImageInput.click();
|
||||||
|
} else if (button.tag === "color") {
|
||||||
|
// Для кнопки цвета открываем диалог выбора цвета
|
||||||
|
insertColorTagForEdit(textarea);
|
||||||
} else {
|
} else {
|
||||||
insertMarkdownForEdit(textarea, button.tag);
|
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() {
|
async function saveNote() {
|
||||||
if (noteInput.value.trim() !== "" || selectedImages.length > 0) {
|
if (noteInput.value.trim() !== "" || selectedImages.length > 0) {
|
||||||
@ -1409,6 +1848,114 @@ noteInput.addEventListener("keydown", function (event) {
|
|||||||
if (event.altKey && event.key === "Enter") {
|
if (event.altKey && event.key === "Enter") {
|
||||||
event.preventDefault(); // Предотвращаем стандартное поведение
|
event.preventDefault(); // Предотвращаем стандартное поведение
|
||||||
saveNote();
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -258,6 +258,9 @@
|
|||||||
<button class="btnMarkdown" id="italicBtn" title="Курсив">
|
<button class="btnMarkdown" id="italicBtn" title="Курсив">
|
||||||
<span class="iconify" data-icon="mdi:format-italic"></span>
|
<span class="iconify" data-icon="mdi:format-italic"></span>
|
||||||
</button>
|
</button>
|
||||||
|
<button class="btnMarkdown" id="colorBtn" title="Цвет текста">
|
||||||
|
<span class="iconify" data-icon="mdi:palette"></span>
|
||||||
|
</button>
|
||||||
<button class="btnMarkdown" id="headerBtn" title="Заголовок">
|
<button class="btnMarkdown" id="headerBtn" title="Заголовок">
|
||||||
<span class="iconify" data-icon="mdi:format-header-1"></span>
|
<span class="iconify" data-icon="mdi:format-header-1"></span>
|
||||||
</button>
|
</button>
|
||||||
@ -273,6 +276,12 @@
|
|||||||
<button class="btnMarkdown" id="linkBtn" title="Ссылка">
|
<button class="btnMarkdown" id="linkBtn" title="Ссылка">
|
||||||
<span class="iconify" data-icon="mdi:link"></span>
|
<span class="iconify" data-icon="mdi:link"></span>
|
||||||
</button>
|
</button>
|
||||||
|
<button class="btnMarkdown" id="checkboxBtn" title="To-Do список">
|
||||||
|
<span
|
||||||
|
class="iconify"
|
||||||
|
data-icon="mdi:checkbox-marked-outline"
|
||||||
|
></span>
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
class="btnMarkdown"
|
class="btnMarkdown"
|
||||||
id="imageBtn"
|
id="imageBtn"
|
||||||
|
|||||||
@ -106,6 +106,10 @@ header .iconify[data-icon="mdi:account-plus"] {
|
|||||||
color: #757575;
|
color: #757575;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.btnMarkdown .iconify[data-icon="mdi:palette"] {
|
||||||
|
color: #e91e63;
|
||||||
|
}
|
||||||
|
|
||||||
.btnMarkdown .iconify[data-icon="mdi:format-header-1"] {
|
.btnMarkdown .iconify[data-icon="mdi:format-header-1"] {
|
||||||
color: #1976d2;
|
color: #1976d2;
|
||||||
}
|
}
|
||||||
@ -372,6 +376,11 @@ textarea:focus {
|
|||||||
.date {
|
.date {
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
color: grey;
|
color: grey;
|
||||||
|
white-space: pre-line;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 10px;
|
||||||
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.notesHeaderBtn {
|
.notesHeaderBtn {
|
||||||
@ -379,7 +388,7 @@ textarea:focus {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
color: black;
|
color: black;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
margin-left: 10px;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.textNote {
|
.textNote {
|
||||||
@ -458,12 +467,14 @@ textarea:focus {
|
|||||||
padding-left: 20px;
|
padding-left: 20px;
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
overflow-wrap: break-word;
|
overflow-wrap: break-word;
|
||||||
|
line-height: 0.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Убираем маргины у элементов списка */
|
/* Убираем маргины у элементов списка */
|
||||||
.textNote li {
|
.textNote li {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
line-height: 0.5;
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
overflow-wrap: break-word;
|
overflow-wrap: break-word;
|
||||||
}
|
}
|
||||||
@ -520,6 +531,86 @@ textarea:focus {
|
|||||||
word-break: break-word;
|
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 {
|
.notes-container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
68
server.js
68
server.js
@ -246,7 +246,6 @@ function createTables() {
|
|||||||
console.error("Ошибка создания таблицы пользователей:", err.message);
|
console.error("Ошибка создания таблицы пользователей:", err.message);
|
||||||
} else {
|
} else {
|
||||||
console.log("Таблица пользователей готова");
|
console.log("Таблица пользователей готова");
|
||||||
createIndexes();
|
|
||||||
runMigrations();
|
runMigrations();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -257,6 +256,7 @@ function createIndexes() {
|
|||||||
const indexes = [
|
const indexes = [
|
||||||
"CREATE INDEX IF NOT EXISTS idx_notes_user_id ON notes(user_id)",
|
"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_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_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_note_images_note_id ON note_images(note_id)",
|
||||||
"CREATE INDEX IF NOT EXISTS idx_users_username ON users(username)",
|
"CREATE INDEX IF NOT EXISTS idx_users_username ON users(username)",
|
||||||
@ -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 для аутентификации
|
// Middleware для аутентификации
|
||||||
@ -530,9 +570,9 @@ app.get("/api/notes/search", requireAuth, (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const sql = `
|
const sql = `
|
||||||
SELECT
|
SELECT
|
||||||
n.*,
|
n.*,
|
||||||
CASE
|
CASE
|
||||||
WHEN COUNT(ni.id) = 0 THEN '[]'
|
WHEN COUNT(ni.id) = 0 THEN '[]'
|
||||||
ELSE json_group_array(
|
ELSE json_group_array(
|
||||||
json_object(
|
json_object(
|
||||||
@ -572,9 +612,9 @@ app.get("/api/notes/search", requireAuth, (req, res) => {
|
|||||||
// API для получения всех заметок с изображениями
|
// API для получения всех заметок с изображениями
|
||||||
app.get("/api/notes", requireAuth, (req, res) => {
|
app.get("/api/notes", requireAuth, (req, res) => {
|
||||||
const sql = `
|
const sql = `
|
||||||
SELECT
|
SELECT
|
||||||
n.*,
|
n.*,
|
||||||
CASE
|
CASE
|
||||||
WHEN COUNT(ni.id) = 0 THEN '[]'
|
WHEN COUNT(ni.id) = 0 THEN '[]'
|
||||||
ELSE json_group_array(
|
ELSE json_group_array(
|
||||||
json_object(
|
json_object(
|
||||||
@ -592,7 +632,7 @@ app.get("/api/notes", requireAuth, (req, res) => {
|
|||||||
LEFT JOIN note_images ni ON n.id = ni.note_id
|
LEFT JOIN note_images ni ON n.id = ni.note_id
|
||||||
WHERE n.user_id = ?
|
WHERE n.user_id = ?
|
||||||
GROUP BY n.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) => {
|
db.all(sql, [req.session.userId], (err, rows) => {
|
||||||
@ -620,7 +660,7 @@ app.post("/api/notes", requireAuth, (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const sql =
|
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];
|
const params = [req.session.userId, content, date, time];
|
||||||
|
|
||||||
db.run(sql, params, function (err) {
|
db.run(sql, params, function (err) {
|
||||||
@ -634,15 +674,15 @@ app.post("/api/notes", requireAuth, (req, res) => {
|
|||||||
|
|
||||||
// API для обновления заметки
|
// API для обновления заметки
|
||||||
app.put("/api/notes/:id", requireAuth, (req, res) => {
|
app.put("/api/notes/:id", requireAuth, (req, res) => {
|
||||||
const { content, date, time } = req.body;
|
const { content } = req.body;
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
|
|
||||||
if (!content || !date || !time) {
|
if (!content) {
|
||||||
return res.status(400).json({ error: "Не все поля заполнены" });
|
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) => {
|
db.get(checkSql, [id], (err, row) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
console.error("Ошибка проверки доступа:", err.message);
|
console.error("Ошибка проверки доступа:", err.message);
|
||||||
@ -658,8 +698,8 @@ app.put("/api/notes/:id", requireAuth, (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const updateSql =
|
const updateSql =
|
||||||
"UPDATE notes SET content = ?, date = ?, time = ? WHERE id = ?";
|
"UPDATE notes SET content = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?";
|
||||||
const params = [content, date, time, id];
|
const params = [content, id];
|
||||||
|
|
||||||
db.run(updateSql, params, function (err) {
|
db.run(updateSql, params, function (err) {
|
||||||
if (err) {
|
if (err) {
|
||||||
@ -669,7 +709,7 @@ app.put("/api/notes/:id", requireAuth, (req, res) => {
|
|||||||
if (this.changes === 0) {
|
if (this.changes === 0) {
|
||||||
return res.status(404).json({ error: "Заметка не найдена" });
|
return res.status(404).json({ error: "Заметка не найдена" });
|
||||||
}
|
}
|
||||||
res.json({ id, content, date, time });
|
res.json({ id, content, date: row.date, time: row.time });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user