Добавлена поддержка внешних ссылок в рендерере marked.js

- Реализована функция для определения внешних ссылок и переопределен рендеринг ссылок, чтобы открывать их в новом окне.
- Добавлены обработчики для внешних ссылок, которые учитывают режим PWA.
- Обновлены стили для внешних ссылок, добавлен значок для визуального обозначения.
This commit is contained in:
Fovway 2025-10-26 14:52:22 +07:00
parent e249638e7f
commit ce803b4c20
4 changed files with 310 additions and 1 deletions

View File

@ -1585,6 +1585,33 @@ function highlightSearchText(content, query) {
// Настройка marked.js для поддержки чекбоксов и strikethrough
const renderer = new marked.Renderer();
// Функция для определения внешних ссылок
function isExternalLink(href) {
try {
const url = new URL(href);
return url.origin !== window.location.origin;
} catch (e) {
// Если URL невалидный, считаем его внутренним
return false;
}
}
// Переопределяем рендеринг ссылок для открытия внешних ссылок в браузере
const originalLink = renderer.link.bind(renderer);
renderer.link = function (href, title, text) {
const isExternal = isExternalLink(href);
if (isExternal) {
// Внешние ссылки открываем в браузере
return `<a href="${href}" title="${
title || ""
}" target="_blank" rel="noopener noreferrer" class="external-link">${text}</a>`;
} else {
// Внутренние ссылки обрабатываем как обычно
return originalLink(href, title, text);
}
};
// Переопределяем рендеринг списков, чтобы чекбоксы были кликабельными (без disabled)
const originalListItem = renderer.listitem.bind(renderer);
renderer.listitem = function (text, task, checked) {
@ -1743,6 +1770,9 @@ async function renderNotes(notes) {
// Добавляем обработчики для изображений в заметках
addImageEventListeners();
// Добавляем обработчики для внешних ссылок
addExternalLinkListeners();
// Добавляем обработчики для чекбоксов в заметках
addCheckboxEventListeners();
@ -2676,6 +2706,32 @@ function addImageEventListeners() {
// Обработчики для кнопок удаления изображений удалены - кнопки удаления только в режиме редактирования
}
// Функция для добавления обработчиков внешних ссылок
function addExternalLinkListeners() {
// Обработчики для внешних ссылок
document.querySelectorAll(".external-link").forEach((linkElement) => {
// Проверяем, не добавлен ли уже обработчик
if (linkElement._externalClickHandler) {
return; // Пропускаем, если обработчик уже добавлен
}
// Создаем новый обработчик
linkElement._externalClickHandler = function (event) {
// Для PWA приложений открываем ссылку в браузере
if (
window.matchMedia("(display-mode: standalone)").matches ||
window.navigator.standalone === true
) {
event.preventDefault();
window.open(this.href, "_blank", "noopener,noreferrer");
}
// В обычном браузере оставляем стандартное поведение (target="_blank")
};
linkElement.addEventListener("click", linkElement._externalClickHandler);
});
}
// Функция для применения визуальных стилей к чекбоксу
function applyCheckboxStyles(checkbox, checked) {
const parentLi = checkbox.closest("li");

View File

@ -1,3 +1,70 @@
// Настройка marked.js для поддержки внешних ссылок
function setupMarkedRenderer() {
// Функция для определения внешних ссылок
function isExternalLink(href) {
try {
const url = new URL(href);
return url.origin !== window.location.origin;
} catch (e) {
// Если URL невалидный, считаем его внутренним
return false;
}
}
// Создаем renderer для marked
const renderer = new marked.Renderer();
// Переопределяем рендеринг ссылок для открытия внешних ссылок в браузере
const originalLink = renderer.link.bind(renderer);
renderer.link = function (href, title, text) {
const isExternal = isExternalLink(href);
if (isExternal) {
// Внешние ссылки открываем в браузере
return `<a href="${href}" title="${
title || ""
}" target="_blank" rel="noopener noreferrer" class="external-link">${text}</a>`;
} else {
// Внутренние ссылки обрабатываем как обычно
return originalLink(href, title, text);
}
};
// Настраиваем marked
marked.setOptions({
gfm: true,
breaks: true,
renderer: renderer,
html: true,
});
}
// Функция для добавления обработчиков внешних ссылок
function addExternalLinkListeners() {
// Обработчики для внешних ссылок
document.querySelectorAll(".external-link").forEach((linkElement) => {
// Проверяем, не добавлен ли уже обработчик
if (linkElement._externalClickHandler) {
return; // Пропускаем, если обработчик уже добавлен
}
// Создаем новый обработчик
linkElement._externalClickHandler = function (event) {
// Для PWA приложений открываем ссылку в браузере
if (
window.matchMedia("(display-mode: standalone)").matches ||
window.navigator.standalone === true
) {
event.preventDefault();
window.open(this.href, "_blank", "noopener,noreferrer");
}
// В обычном браузере оставляем стандартное поведение (target="_blank")
};
linkElement.addEventListener("click", linkElement._externalClickHandler);
});
}
// Логика переключения темы
function initThemeToggle() {
const themeToggleBtn = document.getElementById("theme-toggle-btn");
@ -58,7 +125,10 @@ function applyTheme(theme) {
}
// Инициализируем переключатель темы при загрузке страницы
document.addEventListener("DOMContentLoaded", initThemeToggle);
document.addEventListener("DOMContentLoaded", function () {
initThemeToggle();
setupMarkedRenderer();
});
// Переменные для пагинации логов
let logsOffset = 0;
@ -439,6 +509,9 @@ async function loadArchivedNotes() {
// Добавление обработчиков для архивных заметок
function addArchivedNotesEventListeners() {
// Добавляем обработчики для внешних ссылок
addExternalLinkListeners();
// Восстановление
document.querySelectorAll(".btn-restore").forEach((btn) => {
btn.addEventListener("click", async (e) => {

View File

@ -800,6 +800,26 @@ textarea:focus {
overflow-wrap: break-word;
}
/* Стили для внешних ссылок */
.textNote a.external-link {
position: relative;
padding-right: 16px;
}
.textNote a.external-link::after {
content: "↗";
position: absolute;
right: 0;
top: 0;
font-size: 0.8em;
opacity: 0.7;
transition: opacity 0.3s ease;
}
.textNote a.external-link:hover::after {
opacity: 1;
}
.textNote a:hover {
text-decoration: underline;
}

View File

@ -0,0 +1,160 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Тест внешних ссылок - NoteJS</title>
<link rel="stylesheet" href="style.css" />
<link rel="manifest" href="manifest.json" />
<meta name="theme-color" content="#007bff" />
</head>
<body>
<div class="container">
<h1>Тест внешних ссылок в PWA</h1>
<div class="textNote">
<h2>Примеры ссылок:</h2>
<h3>Внутренние ссылки (открываются в PWA):</h3>
<p><a href="/notes">Перейти к заметкам</a></p>
<p><a href="/profile">Перейти к профилю</a></p>
<h3>Внешние ссылки (открываются в браузере):</h3>
<p>
<a
href="https://www.google.com"
target="_blank"
rel="noopener noreferrer"
class="external-link"
>Google</a
>
</p>
<p>
<a
href="https://github.com"
target="_blank"
rel="noopener noreferrer"
class="external-link"
>GitHub</a
>
</p>
<p>
<a
href="https://www.wikipedia.org"
target="_blank"
rel="noopener noreferrer"
class="external-link"
>Wikipedia</a
>
</p>
<h3>Markdown ссылки:</h3>
<p>Внутренняя ссылка: [Заметки](/notes)</p>
<p>Внешняя ссылка: [Google](https://www.google.com)</p>
<p>Внешняя ссылка: [GitHub](https://github.com)</p>
</div>
<div class="test-results">
<h3>Ожидаемое поведение:</h3>
<ul>
<li>Внутренние ссылки должны открываться в том же PWA окне</li>
<li>Внешние ссылки должны открываться в браузере (новой вкладке)</li>
<li>Внешние ссылки должны иметь иконку ↗</li>
<li>
В PWA режиме внешние ссылки должны принудительно открываться в
браузере
</li>
</ul>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<script>
// Настройка marked.js для поддержки внешних ссылок
function setupMarkedRenderer() {
// Функция для определения внешних ссылок
function isExternalLink(href) {
try {
const url = new URL(href);
return url.origin !== window.location.origin;
} catch (e) {
// Если URL невалидный, считаем его внутренним
return false;
}
}
// Создаем renderer для marked
const renderer = new marked.Renderer();
// Переопределяем рендеринг ссылок для открытия внешних ссылок в браузере
const originalLink = renderer.link.bind(renderer);
renderer.link = function (href, title, text) {
const isExternal = isExternalLink(href);
if (isExternal) {
// Внешние ссылки открываем в браузере
return `<a href="${href}" title="${
title || ""
}" target="_blank" rel="noopener noreferrer" class="external-link">${text}</a>`;
} else {
// Внутренние ссылки обрабатываем как обычно
return originalLink(href, title, text);
}
};
// Настраиваем marked
marked.setOptions({
gfm: true,
breaks: true,
renderer: renderer,
html: true,
});
}
// Функция для добавления обработчиков внешних ссылок
function addExternalLinkListeners() {
// Обработчики для внешних ссылок
document.querySelectorAll(".external-link").forEach((linkElement) => {
// Проверяем, не добавлен ли уже обработчик
if (linkElement._externalClickHandler) {
return; // Пропускаем, если обработчик уже добавлен
}
// Создаем новый обработчик
linkElement._externalClickHandler = function (event) {
// Для PWA приложений открываем ссылку в браузере
if (
window.matchMedia("(display-mode: standalone)").matches ||
window.navigator.standalone === true
) {
event.preventDefault();
window.open(this.href, "_blank", "noopener,noreferrer");
}
// В обычном браузере оставляем стандартное поведение (target="_blank")
};
linkElement.addEventListener(
"click",
linkElement._externalClickHandler
);
});
}
// Инициализация
document.addEventListener("DOMContentLoaded", function () {
setupMarkedRenderer();
// Обрабатываем markdown ссылки
const markdownLinks = document.querySelectorAll(".textNote p");
markdownLinks.forEach((p) => {
if (p.textContent.includes("[") && p.textContent.includes("](")) {
const htmlContent = marked.parse(p.textContent);
p.innerHTML = htmlContent;
}
});
addExternalLinkListeners();
});
</script>
</body>
</html>