diff --git a/config.php b/config.php index 2581132..25797bc 100644 --- a/config.php +++ b/config.php @@ -1,7 +1,7 @@
-
+
- +
diff --git a/public/js/modules/board.js b/public/js/modules/board.js index 913a684..c38c6ca 100644 --- a/public/js/modules/board.js +++ b/public/js/modules/board.js @@ -5,7 +5,8 @@ import { state, sortedCards, parseTags, tagChipHtml, getChildren, getRecent, get import { stripMd, getTodos, firstImage, leadingImage } from './markdown.js'; import { pushReorder, pushCardEdit, pushBatch, snapshotCard } from './undo.js'; import { getCalConfig } from './caldav.js'; -import { renderMd, addCodeCopyButtons } from './markdown.js'; +import { renderMd, addCodeCopyButtons, formatTodoItems } from './markdown.js'; +import { bindCheckboxes } from './detail.js'; const board = $('board'); const boardPreview = $('board-preview'); @@ -113,6 +114,8 @@ function showBoardPreview(cardId) { if (boardPreviewBody) { boardPreviewBody.innerHTML = renderMd(card.body); addCodeCopyButtons(boardPreviewBody); + formatTodoItems(boardPreviewBody); + bindCheckboxes(boardPreviewBody, card); } const tags = parseTags(card); if (boardPreviewTags) { @@ -283,7 +286,7 @@ export function renderBoard() { cards = cards.filter(c => favs.includes(String(c.id))); } else if (state.activeBucket === 'journal') { const today = new Date().toISOString().substring(0, 10); - cards = cards.filter(c => /^\d{4}-\d{2}-\d{2}/.test(c.title) && c.title.substring(0, 10) <= today); + cards = cards.filter(c => /^\d{4}-\d{2}-\d{2}/.test(c.title) && c.title.substring(0, 10) <= today && !(c.tags || '').split(',').includes('geburtstag')); cards.sort((a, b) => b.title.localeCompare(a.title)); } else if (state.activeBucket === 'none') { cards = cards.filter(c => !c.bucket_id && !/^\d{4}-\d{2}-\d{2}/.test(c.title)); diff --git a/public/js/modules/detail.js b/public/js/modules/detail.js index fd920e0..d2ef88b 100644 --- a/public/js/modules/detail.js +++ b/public/js/modules/detail.js @@ -2,7 +2,7 @@ import { $, esc, api, fuzzyMatch } from './util.js'; import { state, parseTags, tagChipHtml, getChildren, getGroupCards, getRecent, markRecent } from './state.js'; -import { renderMd, stripMd, firstImage, addCodeCopyButtons } from './markdown.js'; +import { renderMd, stripMd, firstImage, addCodeCopyButtons, formatTodoItems } from './markdown.js'; import { pushCardDelete, pushCardEdit, snapshotCard } from './undo.js'; import { showDialog } from './dialog.js'; @@ -449,11 +449,16 @@ function showContent(card) { titleEl.textContent = card.title; bodyEl.innerHTML = renderMd(card.body); addCodeCopyButtons(bodyEl); + formatTodoItems(bodyEl); applyReadingResizeHandles(bodyEl); bindDetailImageFullscreen(bodyEl); // Interactive checkboxes - bodyEl.querySelectorAll('input[type="checkbox"]').forEach((cb, idx) => { + bindCheckboxes(bodyEl, card); +} + +export function bindCheckboxes(container, card) { + container.querySelectorAll('input[type="checkbox"]').forEach((cb, idx) => { cb.disabled = false; cb.addEventListener('change', async () => { let n = 0; @@ -886,7 +891,9 @@ function loadSplitCard(id) { splitContent.querySelectorAll('.card-link[data-card-id]').forEach(link => { link.addEventListener('click', () => loadSplitCard(link.dataset.cardId)); }); + bindCheckboxes(splitContent, card); addCodeCopyButtons(splitContent); + formatTodoItems(splitContent); } function promoteSplitCard(id) { diff --git a/public/js/modules/editor.js b/public/js/modules/editor.js index b004c6e..ad91fc6 100644 --- a/public/js/modules/editor.js +++ b/public/js/modules/editor.js @@ -2,7 +2,7 @@ import { $, api, escRe, getCsrf } from './util.js'; import { state, getChildren, CARD_COLORS } from './state.js'; -import { renderMd, addCodeCopyButtons } from './markdown.js'; +import { renderMd, addCodeCopyButtons, formatTodoItems } from './markdown.js'; import { pushCardEdit, snapshotCard } from './undo.js'; import { fuzzyMatch } from './util.js'; import { esc } from './util.js'; @@ -313,6 +313,16 @@ function updatePreview() { const title = inpTitle.value.trim(); previewContent.innerHTML = (title ? `

${esc(title)}

` : '') + renderMd(body.value); addCodeCopyButtons(previewContent); + formatTodoItems(previewContent); + // Interactive checkboxes in editor preview — toggle in textarea + previewContent.querySelectorAll('input[type="checkbox"]').forEach((cb, idx) => { + cb.disabled = false; + cb.addEventListener('change', () => { + let n = 0; + body.value = body.value.replace(/- \[([ x])\]/g, (m, s) => n++ === idx ? `- [${s === 'x' ? ' ' : 'x'}]` : m); + updatePreview(); + }); + }); } async function doSave(close, { animate = true } = {}) { @@ -381,7 +391,32 @@ $('md-toolbar')?.addEventListener('click', e => { code: () => insertMd('`', '`', 'code'), codeblock: () => insertMd('```\n', '\n```', 'code'), ul: () => insertMd('- ', '', 'item'), ol: () => insertMd('1. ', '', 'item'), tree: () => insertMd('```tree\n- ', '\n - child\n', 'parent'), - quote: () => insertMd('> ', '', 'quote'), todo: () => insertMd('- [ ] ', '', 'task'), + quote: () => insertMd('> ', '', 'quote'), + todo: () => { + const s = body.selectionStart, e = body.selectionEnd; + const sel = body.value.substring(s, e); + if (sel && sel.includes('\n') || (sel && /^[\s]*[-*+]\s/m.test(sel))) { + // Multi-line or bullet list: convert each line + const lines = sel.split('\n'); + const hasTodos = lines.some(l => /^\s*- \[[ x]\]/.test(l)); + const converted = lines.map(l => { + const trimmed = l.trim(); + if (!trimmed) return l; + if (hasTodos) { + // Toggle back: remove checkbox + return l.replace(/^(\s*)- \[[ x]\] /, '$1- '); + } + // Add checkbox: strip existing bullet if present + return l.replace(/^(\s*)(?:[-*+]|\d+\.)\s/, '$1').replace(/^(\s*)(.+)$/, '$1- [ ] $2'); + }).join('\n'); + body.setRangeText(converted, s, e, 'select'); + body.selectionStart = s; + body.selectionEnd = s + converted.length; + updatePreview(); + } else { + insertMd('- [ ] ', '', 'task'); + } + }, link: () => insertMd('[', '](url)', 'text'), cardlink: () => { insertMd('[[', ']]', ''); showLinkAc(''); }, extract: async () => { @@ -828,6 +863,33 @@ body.addEventListener('keydown', e => { return; } if (e.key === 'Tab') { e.preventDefault(); insertMd(' '); } + // Enter after todo/bullet/numbered list: continue the prefix + if (e.key === 'Enter' && !e.shiftKey) { + const pos = body.selectionStart; + const before = body.value.substring(0, pos); + const lineStart = before.lastIndexOf('\n') + 1; + const line = before.substring(lineStart); + const m = line.match(/^(\s*)(- \[[ x]\] |[-*+] |\d+\. )/); + if (m) { + e.preventDefault(); + const indent = m[1]; + let prefix = m[2]; + // If current line is empty (just prefix), clear it instead + if (line.trim() === prefix.trim()) { + body.setRangeText('', lineStart, pos, 'end'); + body.selectionStart = body.selectionEnd = lineStart; + updatePreview(); + return; + } + // Increment number for ordered lists + const numMatch = prefix.match(/^(\d+)\. $/); + if (numMatch) prefix = (parseInt(numMatch[1]) + 1) + '. '; + body.setRangeText('\n' + indent + prefix, pos, pos, 'end'); + updatePreview(); + if (linesVisible) updateGutter(); + return; + } + } }); body.addEventListener('blur', () => { setTimeout(hideLinkAc, 150); setTimeout(hideTagAc, 150); setTimeout(hideSnippetAc, 150); }); diff --git a/public/js/modules/markdown.js b/public/js/modules/markdown.js index ce473dd..2b9c64f 100644 --- a/public/js/modules/markdown.js +++ b/public/js/modules/markdown.js @@ -98,6 +98,22 @@ export function addCodeCopyButtons(container) { }); } +export function formatTodoItems(container) { + if (!container) return; + container.querySelectorAll('li').forEach(li => { + const cb = li.querySelector('input[type="checkbox"]'); + if (!cb) return; + if (li.classList.contains('todo-item')) return; + li.classList.add('todo-item'); + if (cb.checked) li.classList.add('todo-done'); + // Wrap remaining text nodes in a span + const span = document.createElement('span'); + span.className = 'todo-text'; + while (cb.nextSibling) span.appendChild(cb.nextSibling); + li.appendChild(span); + }); +} + export function stripMd(src) { return (src || '') .replace(/]*>/gi, '')