Learn: icon formatting toolbar; fix editor shown when hidden

This commit is contained in:
2026-09-17 18:38:19 +03:00
parent 7a341fb871
commit c4dceacc9f
3 changed files with 97 additions and 36 deletions

View File

@@ -44,6 +44,9 @@ body {
a { color: var(--c-text-link); } a { color: var(--c-text-link); }
a:hover { color: var(--c-text-link-hover); } a:hover { color: var(--c-text-link-hover); }
/* Атрибут hidden должен скрывать элемент даже если у него display из класса */
[hidden] { display: none !important; }
/* Layout */ /* Layout */
.page { display: flex; flex-direction: column; min-height: 100vh; } .page { display: flex; flex-direction: column; min-height: 100vh; }
.page-header { position: sticky; top: 0; z-index: 10; border-bottom: 1px solid var(--c-border); background: var(--c-surface); } .page-header { position: sticky; top: 0; z-index: 10; border-bottom: 1px solid var(--c-border); background: var(--c-surface); }
@@ -387,16 +390,33 @@ th.th-filterable { padding-bottom: 0.35rem; }
} }
/* Панель форматирования markdown и встроенный редактор */ /* Панель форматирования markdown и встроенный редактор */
.md-toolbar { display: flex; flex-wrap: wrap; gap: 0.25rem; margin-bottom: 0.4rem; } .md-toolbar {
.md-btn { display: flex; flex-wrap: wrap; align-items: center; gap: 0.1rem;
font: inherit; font-size: 0.78rem; line-height: 1.4; cursor: pointer; padding: 0.3rem; margin-bottom: 0.5rem;
padding: 0.2rem 0.5rem; border: 1px solid var(--c-border); border-radius: 0.35rem; border: 1px solid var(--c-border); border-radius: 0.5rem; background: var(--c-bg);
background: var(--c-surface); color: var(--c-text);
} }
.md-btn:hover { background: var(--c-bg); } .md-sprite { display: none; }
.md-btn {
display: inline-flex; align-items: center; justify-content: center;
width: 2rem; height: 2rem; padding: 0; cursor: pointer;
border: 1px solid transparent; border-radius: 0.375rem;
background: transparent; color: var(--c-text);
}
.md-btn:hover { background: var(--c-surface); border-color: var(--c-border); }
.md-btn:active { transform: translateY(1px); } .md-btn:active { transform: translateY(1px); }
.md-btn:focus-visible { outline: 2px solid var(--c-text-link); outline-offset: 1px; }
.md-ico {
width: 1.15rem; height: 1.15rem; fill: none; stroke: currentColor;
stroke-width: 2; stroke-linecap: round; stroke-linejoin: round;
}
.md-sep { width: 1px; align-self: stretch; margin: 0.2rem 0.3rem; background: var(--c-border); }
.md-editor-head {
display: flex; align-items: center; justify-content: space-between; gap: 0.75rem;
padding: 0.75rem 1.25rem; border-bottom: 1px solid var(--c-border);
}
.md-editor-title { font-weight: 600; }
.md-editor-body { .md-editor-body {
width: 100%; min-height: 20rem; font-family: "DejaVu Sans Mono", Consolas, monospace; width: 100%; min-height: 22rem; font-family: "DejaVu Sans Mono", Consolas, monospace;
font-size: 0.9rem; line-height: 1.5; white-space: pre; overflow-wrap: normal; font-size: 0.9rem; line-height: 1.5; white-space: pre; overflow-wrap: normal;
} }
.md-figure { margin: 1rem 0; text-align: center; } .md-figure { margin: 1rem 0; text-align: center; }

View File

@@ -2,20 +2,56 @@
<html lang="ru" xmlns:th="http://www.thymeleaf.org"> <html lang="ru" xmlns:th="http://www.thymeleaf.org">
<body> <body>
<!-- Панель форматирования markdown. target — CSS-селектор textarea --> <!-- Панель форматирования markdown. target — CSS-селектор textarea -->
<div th:fragment="toolbar(target)" class="md-toolbar" th:attr="data-md-target=${target}"> <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> role="toolbar" aria-label="Форматирование текста">
<button type="button" class="md-btn" data-md="italic" title="Курсив: *текст*"><i>Курсив</i></button> <button type="button" class="md-btn" data-md="bold" title="Полужирный" aria-label="Полужирный">
<button type="button" class="md-btn" data-md="h2" title="Заголовок раздела">Заголовок</button> <svg class="md-ico" viewBox="0 0 24 24"><path d="M7 5h5.2a3.4 3.4 0 0 1 0 6.8H7z"/><path d="M7 11.8h6a3.6 3.6 0 0 1 0 7.2H7z"/></svg>
<button type="button" class="md-btn" data-md="ul" title="Маркированный список">Список</button> </button>
<button type="button" class="md-btn" data-md="ol" title="Нумерованный список">Нумерация</button> <button type="button" class="md-btn" data-md="italic" title="Курсив" aria-label="Курсив">
<button type="button" class="md-btn" data-md="quote" title="Цитата">Цитата</button> <svg class="md-ico" viewBox="0 0 24 24"><path d="M11 5h6M7 19h6M15 5l-4 14"/></svg>
<button type="button" class="md-btn" data-md="link" title="Ссылка">Ссылка</button> </button>
<button type="button" class="md-btn" data-md="code" title="Код в строке">Код</button>
<button type="button" class="md-btn" data-md="codeblock" title="Блок кода на C">Блок кода</button> <span class="md-sep"></span>
<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="h2" title="Заголовок раздела" aria-label="Заголовок раздела">
<button type="button" class="md-btn" data-md="video" title="Видео (файл в assets/)">Видео</button> <svg class="md-ico" viewBox="0 0 24 24"><path d="M4 6v12M11 6v12M4 12h7"/><text x="14.5" y="18.2" font-size="10" font-weight="700" fill="currentColor" stroke="none" font-family="system-ui, sans-serif">2</text></svg>
<button type="button" class="md-btn" data-md="spoiler" title="Спойлер: текст скрыт, открывается по клику">Спойлер</button> </button>
<button type="button" class="md-btn" data-md="ul" title="Маркированный список" aria-label="Маркированный список">
<svg class="md-ico" viewBox="0 0 24 24"><path d="M9 6h11M9 12h11M9 18h11"/><circle cx="4.6" cy="6" r="1.4" fill="currentColor" stroke="none"/><circle cx="4.6" cy="12" r="1.4" fill="currentColor" stroke="none"/><circle cx="4.6" cy="18" r="1.4" fill="currentColor" stroke="none"/></svg>
</button>
<button type="button" class="md-btn" data-md="ol" title="Нумерованный список" aria-label="Нумерованный список">
<svg class="md-ico" viewBox="0 0 24 24"><path d="M11 6h9M11 12h9M11 18h9"/><text x="3" y="8.4" font-size="7.5" fill="currentColor" stroke="none" font-family="system-ui, sans-serif">1</text><text x="3" y="14.4" font-size="7.5" fill="currentColor" stroke="none" font-family="system-ui, sans-serif">2</text><text x="3" y="20.4" font-size="7.5" fill="currentColor" stroke="none" font-family="system-ui, sans-serif">3</text></svg>
</button>
<button type="button" class="md-btn" data-md="quote" title="Цитата" aria-label="Цитата">
<svg class="md-ico" viewBox="0 0 24 24"><path d="M5 6v12" stroke-width="3"/><path d="M10 8h9M10 12h9M10 16h6"/></svg>
</button>
<span class="md-sep"></span>
<button type="button" class="md-btn" data-md="link" title="Ссылка" aria-label="Ссылка">
<svg class="md-ico" viewBox="0 0 24 24"><path d="M10.5 13.5a4 4 0 0 1 0-5.66l2.12-2.12a4 4 0 0 1 5.66 5.66l-1.2 1.2"/><path d="M13.5 10.5a4 4 0 0 1 0 5.66l-2.12 2.12a4 4 0 0 1-5.66-5.66l1.2-1.2"/></svg>
</button>
<button type="button" class="md-btn" data-md="code" title="Код в строке" aria-label="Код в строке">
<svg class="md-ico" viewBox="0 0 24 24"><path d="M9 8l-4 4 4 4M15 8l4 4-4 4"/></svg>
</button>
<button type="button" class="md-btn" data-md="codeblock" title="Блок кода" aria-label="Блок кода">
<svg class="md-ico" viewBox="0 0 24 24"><rect x="3" y="5" width="18" height="14" rx="2"/><path d="M9.5 10l-2 2 2 2M14.5 10l2 2-2 2"/></svg>
</button>
<span class="md-sep"></span>
<button type="button" class="md-btn" data-md="table" title="Таблица" aria-label="Таблица">
<svg class="md-ico" viewBox="0 0 24 24"><rect x="3" y="5" width="18" height="14" rx="2"/><path d="M3 9.7h18M9 9.7V19M15 9.7V19M3 14.3h18"/></svg>
</button>
<button type="button" class="md-btn" data-md="figure" title="Картинка с подписью" aria-label="Картинка с подписью">
<svg class="md-ico" viewBox="0 0 24 24"><rect x="3" y="5" width="18" height="14" rx="2"/><circle cx="8.5" cy="10" r="1.5"/><path d="M4 17l4.5-4.5 3 3 3-3L20 17"/></svg>
</button>
<button type="button" class="md-btn" data-md="video" title="Видео" aria-label="Видео">
<svg class="md-ico" viewBox="0 0 24 24"><rect x="3" y="5" width="18" height="14" rx="2"/><path d="M10 9.5l5 2.5-5 2.5z" fill="currentColor" stroke="none"/></svg>
</button>
<button type="button" class="md-btn" data-md="spoiler" title="Спойлер (скрытый текст)" aria-label="Спойлер">
<svg class="md-ico" viewBox="0 0 24 24"><path d="M3 3l18 18"/><path d="M10.6 6.2A9.7 9.7 0 0 1 12 6c5 0 9 6 9 6a15.4 15.4 0 0 1-3.5 4M6.4 8.1A15.3 15.3 0 0 0 3 12s4 6 9 6a9.6 9.6 0 0 0 3.1-.5"/><path d="M9.9 9.9a3 3 0 0 0 4.2 4.2"/></svg>
</button>
</div> </div>
</body> </body>
</html> </html>

View File

@@ -82,26 +82,31 @@
<div id="article-view" 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 <form id="article-edit" class="card" hidden
th:if="${currentUser != null and currentUser.admin}" th:if="${currentUser != null and currentUser.admin}"
th:action="@{/admin/content/articles/{id}/save(id=${article.id})}" method="post"> th:action="@{/admin/content/articles/{id}/save(id=${article.id})}" method="post">
<input type="hidden" name="_csrf" th:value="${csrfToken}"> <input type="hidden" name="_csrf" th:value="${csrfToken}">
<input type="hidden" name="back" th:value="${back}"> <input type="hidden" name="back" th:value="${back}">
<div><label class="label" for="article-title">Заголовок</label> <div class="md-editor-head">
<input class="input" id="article-title" name="title" th:value="${article.title}" required></div> <span class="md-editor-title">Редактирование статьи</span>
<div> <div class="flex gap-2">
<label class="label" for="article-body">Текст статьи (markdown)</label> <button class="btn btn-secondary btn-sm" type="button" onclick="cancelArticleEdit()">Отмена</button>
<div th:replace="~{fragments/md-toolbar :: toolbar('#article-body')}"></div> <button class="btn btn-primary btn-sm" type="submit">Сохранить</button>
<textarea class="input md-editor-body" id="article-body" name="bodyMd" rows="24" </div>
th:text="${article.bodyMd}"></textarea>
<p class="hint">
Кнопки вставляют разметку в текст. Картинки и видео берутся из папки <code class="code">assets/</code>
архива материалов (имена файлов — латиницей). Заголовок статьи — отдельное поле выше.
</p>
</div> </div>
<div class="flex gap-2"> <div class="card-body form-stack">
<button class="btn btn-primary" type="submit">Сохранить</button> <div><label class="label" for="article-title">Заголовок</label>
<button class="btn btn-secondary" type="button" onclick="cancelArticleEdit()">Отмена</button> <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> </div>
</form> </form>