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, '')