diff --git a/docs/TZ.md b/docs/TZ.md index d2a97fb..37093f2 100644 --- a/docs/TZ.md +++ b/docs/TZ.md @@ -4,7 +4,7 @@ | | | |---|---| -| Версия ТЗ | 0.25 | +| Версия ТЗ | 0.26 | | Дата | 2026-09-21 | | Статус | На обсуждении | @@ -72,6 +72,8 @@ - Описание задачи — **Markdown** (редактирование с форматированием). - Вложения — изображения, в том числе **вставка из буфера обмена** (Ctrl+V прямо в редактор). - Отображение Markdown в описании (просмотр и редактирование). +- **Чекбоксы списков задач** (`- [ ]` / `- [x]`) в отображаемом описании кликабельны: + клик переключает пункт и сохраняет статус в markdown описания. - **Инлайн-редактирование** на странице задачи: каждое редактируемое поле правится прямо на месте (клик по значению → редактор на его месте, ✓/✗ или Esc); пустые поля тоже показываются («— не указано») и редактируются кликом. То же — на diff --git a/frontend/src/taskui.ts b/frontend/src/taskui.ts index 7e4b8ae..9377cf8 100644 --- a/frontend/src/taskui.ts +++ b/frontend/src/taskui.ts @@ -111,6 +111,23 @@ return DOMPurify.sanitize(marked.parse(text, { async: false })) } +// Переключить чекбокс №index в markdown-списке задач ([ ] ↔ [x]). +// Возвращает null, если чекбокса с таким номером нет (DOM разошёлся с текстом — +// вслепую не правим). Понимает маркеры - * + и нумерованные 1. / 1). +export function toggleMarkdownCheckbox(md: string, index: number): string | null { + const re = /^([ \t]*(?:[-*+]|\d+[.)])[ \t]+\[)([ xX])(\])/gm + let i = 0 + let replaced = false + const out = md.replace(re, (line, pre, mark, post) => { + if (i++ === index) { + replaced = true + return pre + (mark === ' ' ? 'x' : ' ') + post + } + return line + }) + return replaced ? out : null +} + // «90» → «1 ч 30 м»; для чипов оценок времени export function formatMinutes(minutes: number | null): string { if (minutes === null) return '' diff --git a/frontend/src/views/TaskView.vue b/frontend/src/views/TaskView.vue index 1403409..80ade65 100644 --- a/frontend/src/views/TaskView.vue +++ b/frontend/src/views/TaskView.vue @@ -17,6 +17,7 @@ deadlinePeriodOptions, recurrenceLabel, renderMarkdown, + toggleMarkdownCheckbox, setPageTitle, statusOptions, statusLabel, @@ -54,8 +55,13 @@ const editing = ref(false) const notFound = ref(false) +// Чекбоксы списков задач в отображаемом описании кликабельны: marked рендерит +// их с disabled, а клики по disabled-инпутам браузер не шлёт вовсе — атрибут +// снимаем, статус сохраняет onDescClick (PATCH сырого markdown) const descriptionHtml = computed(() => - task.value ? renderMarkdown(task.value.description) : '', + task.value + ? renderMarkdown(task.value.description).replace(/ disabled="" type="checkbox"/g, ' type="checkbox"') + : '', ) // Тайтл вкладки — заголовок задачи (после загрузки уточняет meta маршрута) @@ -185,7 +191,22 @@ if (target.tagName === 'IMG') { const img = target as HTMLImageElement descImage.value = { url: img.getAttribute('src') ?? '', name: img.alt } + return } + // Клик по чекбоксу списка задач: переключаем его в сыром markdown и сохраняем + const task0 = task.value + if (!task0 || !(target instanceof HTMLInputElement) || target.type !== 'checkbox') return + const boxes = Array.from( + (e.currentTarget as HTMLElement).querySelectorAll('input[type=checkbox]'), + ) + const index = boxes.indexOf(target) + if (index === -1) return + const updated = toggleMarkdownCheckbox(task0.description, index) + if (updated === null) return + target.disabled = true // второй клик, пока PATCH летит; при ошибке вернём + patchTask({ description: updated }).catch(() => { + target.disabled = false + }) } async function savePriority() { @@ -1234,6 +1255,10 @@ max-width: 100%; height: auto; } +/* чекбоксы списков задач — кликабельны (статус сохраняется в markdown) */ +.md-view :deep(input[type='checkbox']) { + cursor: pointer; +} .side-card { margin-bottom: 0; }