Learn: inline markdown editor with formatting toolbar; drop edit modals

This commit is contained in:
2026-09-17 18:31:43 +03:00
parent 114a031e36
commit 7a341fb871
7 changed files with 210 additions and 55 deletions

View File

@@ -0,0 +1,21 @@
<!DOCTYPE html>
<html lang="ru" xmlns:th="http://www.thymeleaf.org">
<body>
<!-- Панель форматирования markdown. target — CSS-селектор textarea -->
<div th:fragment="toolbar(target)" class="md-toolbar" th:attr="data-md-target=${target}">
<button type="button" class="md-btn" data-md="bold" title="Полужирный: **текст**"><b>Полужирный</b></button>
<button type="button" class="md-btn" data-md="italic" title="Курсив: *текст*"><i>Курсив</i></button>
<button type="button" class="md-btn" data-md="h2" title="Заголовок раздела">Заголовок</button>
<button type="button" class="md-btn" data-md="ul" title="Маркированный список">Список</button>
<button type="button" class="md-btn" data-md="ol" title="Нумерованный список">Нумерация</button>
<button type="button" class="md-btn" data-md="quote" title="Цитата">Цитата</button>
<button type="button" class="md-btn" data-md="link" title="Ссылка">Ссылка</button>
<button type="button" class="md-btn" data-md="code" title="Код в строке">Код</button>
<button type="button" class="md-btn" data-md="codeblock" title="Блок кода на C">Блок кода</button>
<button type="button" class="md-btn" data-md="table" title="Таблица">Таблица</button>
<button type="button" class="md-btn" data-md="figure" title="Картинка с подписью (файл в assets/)">Картинка</button>
<button type="button" class="md-btn" data-md="video" title="Видео (файл в assets/)">Видео</button>
<button type="button" class="md-btn" data-md="spoiler" title="Спойлер: текст скрыт, открывается по клику">Спойлер</button>
</div>
</body>
</html>

View File

@@ -46,7 +46,7 @@
</h1>
<div th:if="${currentUser != null and currentUser.admin}" class="flex flex-wrap gap-2 mb-4">
<button class="btn btn-primary btn-sm" type="button" onclick="openModal('modal-edit-article')">Править</button>
<button class="btn btn-primary btn-sm" type="button" onclick="startArticleEdit()">Править</button>
<form class="inline-form" th:action="@{/admin/content/articles/toggle-published}" method="post">
<input type="hidden" name="_csrf" th:value="${csrfToken}">
<input type="hidden" name="id" th:value="${article.id}">
@@ -80,7 +80,30 @@
</a>
</nav>
<div class="card card-body md-body" th:utext="${article.bodyHtml}"></div>
<div id="article-view" class="card card-body md-body" th:utext="${article.bodyHtml}"></div>
<form id="article-edit" class="card card-body form-stack" hidden
th:if="${currentUser != null and currentUser.admin}"
th:action="@{/admin/content/articles/{id}/save(id=${article.id})}" method="post">
<input type="hidden" name="_csrf" th:value="${csrfToken}">
<input type="hidden" name="back" th:value="${back}">
<div><label class="label" for="article-title">Заголовок</label>
<input class="input" id="article-title" name="title" th:value="${article.title}" required></div>
<div>
<label class="label" for="article-body">Текст статьи (markdown)</label>
<div th:replace="~{fragments/md-toolbar :: toolbar('#article-body')}"></div>
<textarea class="input md-editor-body" id="article-body" name="bodyMd" rows="24"
th:text="${article.bodyMd}"></textarea>
<p class="hint">
Кнопки вставляют разметку в текст. Картинки и видео берутся из папки <code class="code">assets/</code>
архива материалов (имена файлов — латиницей). Заголовок статьи — отдельное поле выше.
</p>
</div>
<div class="flex gap-2">
<button class="btn btn-primary" type="submit">Сохранить</button>
<button class="btn btn-secondary" type="button" onclick="cancelArticleEdit()">Отмена</button>
</div>
</form>
<nav class="article-nav mt-6" th:if="${prevArticle != null or nextArticle != null}">
<a th:if="${prevArticle != null}" class="btn btn-secondary"
@@ -97,34 +120,8 @@
</div>
<dialog id="modal-edit-article" class="modal">
<div class="modal-header">
<h2 class="modal-title">Правка статьи</h2>
<button class="modal-close" type="button" onclick="closeModal('modal-edit-article')">✕</button>
</div>
<div class="modal-body">
<form class="form-stack" th:action="@{/admin/content/articles/{id}/save(id=${article.id})}" method="post">
<input type="hidden" name="_csrf" th:value="${csrfToken}">
<input type="hidden" name="back" th:value="${back}">
<div><label class="label" for="artTitle">Заголовок</label>
<input class="input" id="artTitle" name="title" th:value="${article.title}" required></div>
<div><label class="label" for="artBody">Текст (markdown)</label>
<textarea class="input" id="artBody" name="bodyMd" rows="18" th:text="${article.bodyMd}"></textarea></div>
<p class="hint">
Картинка с подписью: <code class="code">:::figure assets/имя.svg | Рис. 1. Подпись</code>.
Видео: <code class="code">:::video assets/имя.mp4 | Подпись</code>.
Решение под спойлером: <code class="code">:::spoiler Решение … :::</code>.
</p>
<button class="btn btn-primary" type="submit">Сохранить</button>
</form>
</div>
</dialog>
<script th:if="${currentUser != null and currentUser.admin}">
if (window.location.search.indexOf('edit=1') >= 0) {
document.addEventListener('DOMContentLoaded', function () { openModal('modal-edit-article'); });
}
</script>
<script src="/js/mdeditor.js" defer></script>
</div>
</body>
</html>

View File

@@ -116,8 +116,8 @@
th:classappend="${article.published} ? 'btn-secondary' : 'btn-primary'"
th:text="${article.published} ? 'Скрыть' : 'Опубликовать'">Скрыть</button>
</form>
<button class="btn btn-secondary btn-sm" type="button"
th:data-modal="|modal-edit-article-${article.id}|" onclick="openModalData(this)">Править</button>
<a class="btn btn-secondary btn-sm"
th:href="@{/learn/{c}/{s}/{a}(c=${chapter.slug}, s=${section.slug}, a=${article.slug}, edit=1)}">Править</a>
<form class="inline-form" th:action="@{/admin/content/articles/{id}/delete(id=${article.id})}" method="post"
onsubmit="return confirm('Удалить статью?')">
<input type="hidden" name="_csrf" th:value="${csrfToken}">
@@ -244,31 +244,14 @@
<div><label class="label" for="naTitle">Заголовок статьи</label>
<input class="input" id="naTitle" name="title" required></div>
<div><label class="label" for="naBody">Текст (markdown)</label>
<textarea class="input" id="naBody" name="bodyMd" rows="10"></textarea></div>
<div th:replace="~{fragments/md-toolbar :: toolbar('#naBody')}"></div>
<textarea class="input md-editor-body" id="naBody" name="bodyMd" rows="18"></textarea></div>
<button class="btn btn-primary" type="submit">Создать</button>
</form>
</div>
</dialog>
<th:block th:if="${currentUser != null and currentUser.admin}" th:each="a : ${articles}">
<dialog th:id="|modal-edit-article-${a.id}|" class="modal">
<div class="modal-header">
<h2 class="modal-title">Правка статьи</h2>
<button class="modal-close" type="button"
th:data-modal="|modal-edit-article-${a.id}|" onclick="closeModalData(this)">✕</button>
</div>
<div class="modal-body">
<form class="form-stack" th:action="@{/admin/content/articles/{id}/save(id=${a.id})}" method="post">
<input type="hidden" name="_csrf" th:value="${csrfToken}">
<div><label class="label">Заголовок</label>
<input class="input" name="title" th:value="${a.title}" required></div>
<div><label class="label">Текст (markdown)</label>
<textarea class="input" name="bodyMd" rows="14" th:text="${a.bodyMd}"></textarea></div>
<button class="btn btn-primary" type="submit">Сохранить</button>
</form>
</div>
</dialog>
</th:block>
<script src="/js/mdeditor.js" defer></script>
<script src="/js/content-sort.js" defer></script>
</div>
</body>

View File

@@ -122,7 +122,8 @@
<div><label class="label" for="naTitle">Заголовок</label>
<input class="input" id="naTitle" name="title" required></div>
<div><label class="label" for="naBody">Текст (markdown)</label>
<textarea class="input" id="naBody" name="bodyMd" rows="14"></textarea></div>
<div th:replace="~{fragments/md-toolbar :: toolbar('#naBody')}"></div>
<textarea class="input md-editor-body" id="naBody" name="bodyMd" rows="18"></textarea></div>
<p class="hint">
Картинка: <code class="code">:::figure assets/имя.svg | Рис. 1. Подпись</code>;
видео: <code class="code">:::video assets/имя.mp4 | Подпись</code>;