From a77bdd3e7bf4cb665b04af02371e260a7f5b88d1 Mon Sep 17 00:00:00 2001 From: Fovway Date: Wed, 22 Oct 2025 08:04:41 +0700 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20=D0=94=D0=BE=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D1=8B=20=D1=84=D1=83=D0=BD=D0=BA=D1=86=D0=B8?= =?UTF-8?q?=D0=B8=20=D0=B4=D0=BB=D1=8F=20=D1=80=D0=B0=D0=B1=D0=BE=D1=82?= =?UTF-8?q?=D1=8B=20=D1=81=20=D1=86=D0=B2=D0=B5=D1=82=D0=BE=D0=BC=20=D1=82?= =?UTF-8?q?=D0=B5=D0=BA=D1=81=D1=82=D0=B0=20=D0=B8=20=D1=87=D0=B5=D0=BA?= =?UTF-8?q?=D0=B1=D0=BE=D0=BA=D1=81=D0=B0=D0=BC=D0=B8=20=D0=B2=20=D0=B7?= =?UTF-8?q?=D0=B0=D0=BC=D0=B5=D1=82=D0=BA=D0=B0=D1=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Реализована возможность вставки цветового тега в текст заметок с помощью диалога выбора цвета. - Добавлены функции для работы с чекбоксами, включая автоматическое продолжение списков и визуальные эффекты для отмеченных задач. - Обновлены стили для чекбоксов и элементов списка, улучшено отображение дат создания и изменения заметок. - Обновлены обработчики событий для поддержки новых функций в интерфейсе редактирования заметок. --- public/app.js | 573 ++++++++++++++++++++++++++++++++++++++++++++-- public/notes.html | 9 + public/style.css | 93 +++++++- server.js | 68 ++++-- 4 files changed, 715 insertions(+), 28 deletions(-) diff --git a/public/app.js b/public/app.js index de7cf7e..3fecf47 100644 --- a/public/app.js +++ b/public/app.js @@ -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 = `Текст`; + } else { + // Если текст выделен, оборачиваем его в цветовой тег + replacement = `${selected}`; + } + + 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 = `Текст`; + } else { + // Если текст выделен, оборачиваем его в цветовой тег + replacement = `${selected}`; + } + + 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, '$1'); } +// Настройка 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(/]*disabled[^>]*>/gi, "").trim(); + // Создаем чекбокс БЕЗ disabled атрибута + return `
  • ${cleanText}
  • \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 += ""; } + // Форматируем дату создания и дату изменения + 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 + )}
    Изменено: ${formatDate(updatedDate)}`; + } + const noteHtml = `
    - ${note.date} ${note.time} + ${dateDisplay}
    Редактировать
    @@ -853,7 +1032,7 @@ async function renderNotes(notes) { ${imagesHtml}
    `; - 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); + } } }); diff --git a/public/notes.html b/public/notes.html index 89353b7..52c9335 100644 --- a/public/notes.html +++ b/public/notes.html @@ -258,6 +258,9 @@ + @@ -273,6 +276,12 @@ +