Compare commits
2 Commits
083ac11ab1
...
1172edf31e
| Author | SHA1 | Date | |
|---|---|---|---|
| 1172edf31e | |||
| 05706a7e35 |
167
public/app.js
167
public/app.js
@ -95,6 +95,29 @@ function getFormattedDateTime() {
|
||||
};
|
||||
}
|
||||
|
||||
// Вспомогательные функции для корректной работы с временем (UTC -> локаль)
|
||||
function parseSQLiteUtc(ts) {
|
||||
return new Date(ts.replace(" ", "T") + "Z");
|
||||
}
|
||||
|
||||
function formatLocalDateTime(date) {
|
||||
return new Intl.DateTimeFormat("ru-RU", {
|
||||
day: "2-digit",
|
||||
month: "2-digit",
|
||||
year: "numeric",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
}).format(date);
|
||||
}
|
||||
|
||||
function formatLocalDateOnly(date) {
|
||||
return new Intl.DateTimeFormat("ru-RU", {
|
||||
day: "2-digit",
|
||||
month: "2-digit",
|
||||
year: "numeric",
|
||||
}).format(date);
|
||||
}
|
||||
|
||||
// Функция для авторасширения текстового поля
|
||||
function autoExpandTextarea(textarea) {
|
||||
textarea.style.height = "auto";
|
||||
@ -108,6 +131,45 @@ function extractTags(content) {
|
||||
let match;
|
||||
|
||||
while ((match = tagRegex.exec(content)) !== null) {
|
||||
const matchIndex = match.index;
|
||||
|
||||
// Проверяем, не находится ли # внутри HTML-атрибута
|
||||
const beforeContext = content.substring(
|
||||
Math.max(0, matchIndex - 100),
|
||||
matchIndex
|
||||
);
|
||||
const afterContext = content.substring(
|
||||
matchIndex + match[0].length,
|
||||
Math.min(content.length, matchIndex + match[0].length + 100)
|
||||
);
|
||||
|
||||
// Проверяем, есть ли признаки HTML-атрибута (например, style="color: #...)
|
||||
const lastOpenTag = beforeContext.lastIndexOf("<");
|
||||
const lastCloseTag = beforeContext.lastIndexOf(">");
|
||||
|
||||
// Если внутри HTML-тега, пропускаем
|
||||
if (lastOpenTag > lastCloseTag) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Проверяем наличие = и кавычки перед #
|
||||
const lastQuote = Math.max(
|
||||
beforeContext.lastIndexOf('"'),
|
||||
beforeContext.lastIndexOf("'")
|
||||
);
|
||||
const lastEquals = beforeContext.lastIndexOf("=");
|
||||
|
||||
// Если перед # есть = и кавычка, и после есть закрывающая кавычка
|
||||
if (lastEquals > -1 && lastQuote > lastEquals) {
|
||||
const nextQuote = Math.min(
|
||||
afterContext.indexOf('"') !== -1 ? afterContext.indexOf('"') : Infinity,
|
||||
afterContext.indexOf("'") !== -1 ? afterContext.indexOf("'") : Infinity
|
||||
);
|
||||
if (nextQuote !== Infinity) {
|
||||
continue; // Пропускаем, это часть HTML-атрибута
|
||||
}
|
||||
}
|
||||
|
||||
const tag = match[1].toLowerCase();
|
||||
if (!tags.includes(tag)) {
|
||||
tags.push(tag);
|
||||
@ -149,6 +211,31 @@ function makeTagsClickable(content) {
|
||||
continue; // Пропускаем этот тег
|
||||
}
|
||||
|
||||
// Дополнительная проверка: не находится ли # внутри HTML-атрибута (например, style="color: #ff0000")
|
||||
// Ищем последний символ кавычки перед текущей позицией
|
||||
const beforeContext = beforeTag.substring(Math.max(0, match.index - 100));
|
||||
const lastQuote = Math.max(
|
||||
beforeContext.lastIndexOf('"'),
|
||||
beforeContext.lastIndexOf("'")
|
||||
);
|
||||
const lastEquals = beforeContext.lastIndexOf("=");
|
||||
|
||||
// Если перед # есть = и кавычка, и нет закрывающей кавычки после =, то # находится в атрибуте
|
||||
if (lastEquals > -1 && lastQuote > lastEquals) {
|
||||
const afterContext = afterTag.substring(
|
||||
0,
|
||||
Math.min(100, afterTag.length)
|
||||
);
|
||||
const nextQuote = Math.min(
|
||||
afterContext.indexOf('"') !== -1 ? afterContext.indexOf('"') : Infinity,
|
||||
afterContext.indexOf("'") !== -1 ? afterContext.indexOf("'") : Infinity
|
||||
);
|
||||
// Если нашли закрывающую кавычку, значит # внутри атрибута
|
||||
if (nextQuote !== Infinity) {
|
||||
continue; // Пропускаем этот тег
|
||||
}
|
||||
}
|
||||
|
||||
// Заменяем тег на кликабельный элемент
|
||||
const replacement = `<span class="tag-in-note" data-tag="${match.tag}">${match.fullMatch}</span>`;
|
||||
result = beforeTag + replacement + afterTag;
|
||||
@ -1136,24 +1223,21 @@ 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
|
||||
)} • Изменено: ${formatDate(updatedDate)}`;
|
||||
// Форматируем дату создания и изменения по локали устройства
|
||||
let dateDisplay;
|
||||
if (note.created_at) {
|
||||
const created = parseSQLiteUtc(note.created_at);
|
||||
if (note.updated_at && note.created_at !== note.updated_at) {
|
||||
const updated = parseSQLiteUtc(note.updated_at);
|
||||
dateDisplay = `Создано: ${formatLocalDateTime(
|
||||
created
|
||||
)} • Изменено: ${formatLocalDateTime(updated)}`;
|
||||
} else {
|
||||
dateDisplay = formatLocalDateTime(created);
|
||||
}
|
||||
} else {
|
||||
// Фолбэк для старых записей
|
||||
dateDisplay = `${note.date} ${note.time}`;
|
||||
}
|
||||
|
||||
const noteHtml = `
|
||||
@ -1489,12 +1573,19 @@ function addNoteEventListeners() {
|
||||
saveEditBtn.textContent = "Сохранить";
|
||||
saveEditBtn.classList.add("btnSave");
|
||||
|
||||
// Кнопка отмены
|
||||
const cancelEditBtn = document.createElement("button");
|
||||
cancelEditBtn.textContent = "Отмена";
|
||||
cancelEditBtn.classList.add("btnSave");
|
||||
cancelEditBtn.style.marginLeft = "8px";
|
||||
|
||||
// Подсказка о горячей клавише
|
||||
const saveHint = document.createElement("span");
|
||||
saveHint.classList.add("save-hint");
|
||||
saveHint.textContent = "или нажмите Alt + Enter";
|
||||
|
||||
saveButtonContainer.appendChild(saveEditBtn);
|
||||
saveButtonContainer.appendChild(cancelEditBtn);
|
||||
saveButtonContainer.appendChild(saveHint);
|
||||
|
||||
// Функция обновления превью изображений для режима редактирования
|
||||
@ -1593,11 +1684,42 @@ function addNoteEventListeners() {
|
||||
}
|
||||
};
|
||||
|
||||
// Функция отмены редактирования
|
||||
const cancelEditNote = async function () {
|
||||
const originalMarkdown = noteContent.dataset.originalContent || "";
|
||||
const hasTextChanges = textarea.value !== originalMarkdown;
|
||||
const hasNewImages = editSelectedImages.length > 0;
|
||||
|
||||
if (hasTextChanges || hasNewImages) {
|
||||
const ok = confirm("Отменить изменения?");
|
||||
if (!ok) return;
|
||||
}
|
||||
|
||||
// Очистить выбранные новые изображения и превью
|
||||
editSelectedImages.length = 0;
|
||||
if (editImagePreviewContainer) {
|
||||
editImagePreviewContainer.style.display = "none";
|
||||
}
|
||||
if (editImagePreviewList) {
|
||||
editImagePreviewList.innerHTML = "";
|
||||
}
|
||||
if (editImageInput) {
|
||||
editImageInput.value = "";
|
||||
}
|
||||
|
||||
// Перерисовать заметки, вернув исходное состояние
|
||||
await loadNotes(true);
|
||||
};
|
||||
|
||||
// Обработчик горячей клавиши Alt+Enter для сохранения редактирования
|
||||
textarea.addEventListener("keydown", function (event) {
|
||||
if (event.altKey && event.key === "Enter") {
|
||||
event.preventDefault();
|
||||
saveEditNote();
|
||||
} else if (event.key === "Escape") {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
cancelEditNote();
|
||||
}
|
||||
});
|
||||
|
||||
@ -1654,6 +1776,9 @@ function addNoteEventListeners() {
|
||||
|
||||
// Обработчик сохранения редактирования
|
||||
saveEditBtn.addEventListener("click", saveEditNote);
|
||||
|
||||
// Обработчик отмены редактирования
|
||||
cancelEditBtn.addEventListener("click", cancelEditNote);
|
||||
});
|
||||
});
|
||||
}
|
||||
@ -2230,11 +2355,7 @@ let currentDate = new Date();
|
||||
|
||||
// Функция для преобразования timestamp в формат dd.mm.yyyy
|
||||
function formatDateFromTimestamp(timestamp) {
|
||||
const date = new Date(timestamp);
|
||||
const day = String(date.getDate()).padStart(2, "0");
|
||||
const month = String(date.getMonth() + 1).padStart(2, "0");
|
||||
const year = date.getFullYear();
|
||||
return `${day}.${month}.${year}`;
|
||||
return formatLocalDateOnly(parseSQLiteUtc(timestamp));
|
||||
}
|
||||
|
||||
// Функция для отображения календаря
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user