From 08be8c68fe627b2f3b9d6f5778ea78892848b73c Mon Sep 17 00:00:00 2001 From: luxick Date: Fri, 3 Jul 2026 12:24:50 +0200 Subject: [PATCH] Add quick actions toolbar for todo --- assets/style.css | 9 +++++ assets/todo-rail.js | 84 ++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 89 insertions(+), 4 deletions(-) diff --git a/assets/style.css b/assets/style.css index 0b83b7b..f603ed7 100644 --- a/assets/style.css +++ b/assets/style.css @@ -774,6 +774,15 @@ aside.sidebar:empty { display: none; } margin-bottom: var(--space-2); padding-right: var(--space-2); } +/* Compact action toolbar in the widget header. Buttons are the app's standard + bracketed .btn/.btn-tool; the row just holds them together and lets them wrap + in the narrow rail rather than overflow. */ +.todo-toolbar { flex-wrap: wrap; } +/* Roomier tap targets when the widget is surfaced full-screen on mobile. */ +.overlay-body .todo-toolbar .btn { + font-size: var(--font-sm); + padding: var(--space-1) var(--space-2); +} .todo-unsaved { color: var(--primary-hover); font-size: var(--font-xs); } /* The scrolling children run edge-to-edge (the aside has no right padding) so their scrollbars are flush with the divider; padding-right keeps their text diff --git a/assets/todo-rail.js b/assets/todo-rail.js index bb0746e..e35c8d7 100644 --- a/assets/todo-rail.js +++ b/assets/todo-rail.js @@ -32,6 +32,7 @@ var loadingBundle = false; var wantFocus = false; // user reached for the editor before it mounted var knownETag = null; // ETag of the content currently in the buffer + var pendingAction = null; // toolbar action clicked before the editor mounted fetch(TODO_URL, { credentials: 'same-origin', cache: 'no-store' }).then(function (r) { if (r.status === 404) return null; // no todo.txt — no widget @@ -47,14 +48,22 @@ var head = document.createElement('div'); head.className = 'row space-between todo-rail-head'; - var label = document.createElement('span'); - label.className = 'muted'; - label.textContent = 'todo.txt'; + + // Toolbar replaces the old static "todo.txt" label. Each button acts on + // the current line of the editor via runAction; the labels render + // bracketed ([done] [pri] …) to match the app's button aesthetic. + var toolbar = document.createElement('div'); + toolbar.className = 'row gap-1 todo-toolbar'; + toolbar.appendChild(toolButton('done', 'Toggle done (x) on this line', actions.done)); + toolbar.appendChild(toolButton('pri', 'Cycle priority (A → B → C → none)', actions.priority)); + toolbar.appendChild(toolButton('date', "Insert today's date at the cursor", actions.date)); + toolbar.appendChild(toolButton('del', 'Delete this line', actions.del, 'danger')); + marker = document.createElement('span'); marker.className = 'todo-unsaved'; marker.textContent = 'unsaved'; marker.hidden = true; - head.appendChild(label); + head.appendChild(toolbar); head.appendChild(marker); bodyEl = document.createElement('div'); @@ -131,9 +140,12 @@ // Only steal focus if the user actually reached for the editor — the // passive idle mount must not grab focus or scroll the page. if (wantFocus) view.focus(); + // A toolbar button clicked during the mount runs now that the view exists. + if (pendingAction) { var p = pendingAction; pendingAction = null; p(view); } } function mountFallback() { + pendingAction = null; // the plain textarea can't run CM-based actions fallback = document.createElement('textarea'); fallback.className = 'input todo-fallback'; fallback.value = readView.textContent; @@ -144,6 +156,70 @@ if (wantFocus) fallback.focus(); } + // --- Toolbar --------------------------------------------------------- + + function pad2(n) { return (n < 10 ? '0' : '') + n; } + + // Local ISO date (YYYY-MM-DD), matching the wiki's date convention. + function todayISO() { + var d = new Date(); + return d.getFullYear() + '-' + pad2(d.getMonth() + 1) + '-' + pad2(d.getDate()); + } + + function mainLine(v) { return v.state.doc.lineAt(v.state.selection.main.head); } + + // Each action operates on the current line of the CM view. Kept deliberately + // simple (string edits at the line start / cursor) — no task parsing, per + // the todo.txt widget's highlighting-only remit. + var actions = { + // Toggle the `x ` done prefix (with completion date). Stripping also + // removes the optional date that followed it. + done: function (v) { + var line = mainLine(v); + var m = /^x (?:\d{4}-\d{2}-\d{2} )?/.exec(line.text); + if (m) { + v.dispatch({ changes: { from: line.from, to: line.from + m[0].length } }); + } else { + v.dispatch({ changes: { from: line.from, insert: 'x ' + todayISO() + ' ' } }); + } + }, + // Cycle the leading priority: none → (A) → (B) → (C) → none. + priority: function (v) { + var line = mainLine(v); + var m = /^\(([A-Z])\) /.exec(line.text); + var next = !m ? '(A) ' : m[1] === 'A' ? '(B) ' : m[1] === 'B' ? '(C) ' : ''; + v.dispatch({ changes: { from: line.from, to: line.from + (m ? m[0].length : 0), insert: next } }); + }, + // Insert today's date at the cursor. + date: function (v) { + v.dispatch(v.state.replaceSelection(todayISO() + ' ')); + }, + del: function (v) { CM.deleteLine(v); }, + }; + + function runAction(fn) { + if (view) { fn(view); view.focus(); return; } + if (fallback) return; // unstyled fallback has no CM view to act on + // Editor still mounting — remember the action and mount it now, focused. + pendingAction = fn; + wantFocus = true; + upgrade(); + } + + function toolButton(label, title, fn, extraClass) { + var b = document.createElement('button'); + b.type = 'button'; + b.className = 'btn btn-tool' + (extraClass ? ' ' + extraClass : ''); + b.textContent = label; + b.title = title; + // Keep the editor focused (and the mobile keyboard up) when tapping the + // toolbar, mirroring the page editor — preventDefault blocks the focus + // shift on mousedown; the click still fires. + b.addEventListener('mousedown', function (e) { e.preventDefault(); }); + b.addEventListener('click', function () { runAction(fn); }); + return b; + } + function currentText() { if (view) return view.state.doc.toString(); if (fallback) return fallback.value;