improve task list handling

This commit is contained in:
2026-05-23 08:44:19 +02:00
parent a25d5434ac
commit bf16f2ec3c
9 changed files with 392 additions and 25 deletions
+38 -10
View File
@@ -3,6 +3,36 @@ function encodePickedPath(p) {
return '/' + p.replace(/^\/+/, '').split('/').map(encodeURIComponent).join('/');
}
// postReplace POSTs to action with the optional form body, then loads target
// into the current history entry — so the action and its result occupy one
// entry instead of two, and back-navigation skips past the stale pre-mutation
// snapshot in bfcache. body may be null for empty POSTs.
//
// We can't just call window.location.replace(target): when target differs from
// the current URL only by fragment, the browser updates the URL bar without
// re-fetching, so a server-side mutation wouldn't be reflected. Instead,
// rewrite the current entry's URL via history.replaceState, then reload — the
// reload always re-fetches and preserves the (new) URL including its fragment.
function postReplace(action, body, target) {
var init = { method: 'POST', redirect: 'manual' };
if (body) {
init.headers = { 'Content-Type': 'application/x-www-form-urlencoded' };
init.body = body;
}
fetch(action, init).then(function (res) {
if (res.type === 'opaqueredirect' || res.ok) {
window.history.replaceState(null, '', target);
window.location.reload();
return;
}
return res.text().then(function (msg) {
alert(msg || ('Request failed (' + res.status + ')'));
});
}).catch(function () {
alert('Network error');
});
}
function promptPageName(title, initial, confirmLabel, onName) {
var input = document.createElement('input');
input.type = 'text';
@@ -89,11 +119,9 @@ function movePage() {
var action = window.location.pathname + '?move=' +
encodeURIComponent(dest);
if (linksCheckbox.checked) action += '&links=1';
var form = document.createElement('form');
form.method = 'POST';
form.action = action;
document.body.appendChild(form);
form.submit();
var target = encodePickedPath(dest) + '/';
closeModal();
postReplace(action, null, target);
}
}
});
@@ -112,11 +140,11 @@ function deletePage() {
danger: true,
enterConfirms: false,
onConfirm: function () {
var form = document.createElement('form');
form.method = 'POST';
form.action = window.location.pathname + '?delete=1';
document.body.appendChild(form);
form.submit();
var p = window.location.pathname.replace(/\/+$/, '');
var idx = p.lastIndexOf('/');
var parent = idx > 0 ? p.substring(0, idx + 1) : '/';
closeModal();
postReplace(window.location.pathname + '?delete=1', null, parent);
}
},
cancel: { autofocus: true },
+108 -11
View File
@@ -1,17 +1,114 @@
(function () {
var content = document.querySelector("main");
var content = document.querySelector('.content');
if (!content) return;
var headings = content.querySelectorAll("h2, h3, h4");
if (!headings) return
var allHeadings = content.querySelectorAll('h1, h2, h3, h4, h5, h6');
if (!allHeadings.length) return;
headings.forEach(function (h) {
if (!h.id) return;
var a = document.createElement('a');
a.href = '#' + h.id;
a.className = 'heading-anchor';
a.setAttribute('aria-label', 'Link to this section');
a.textContent = '#';
h.insertBefore(a, h.firstChild);
function copyAnchor(id, item, label, menu) {
var url = window.location.origin + window.location.pathname + '#' + id;
function flash() {
item.textContent = 'Copied!';
setTimeout(function () {
item.textContent = label;
menu.classList.remove('is-open');
}, 1200);
}
function fallback() {
var ta = document.createElement('textarea');
ta.value = url;
ta.setAttribute('readonly', '');
ta.style.position = 'fixed';
ta.style.opacity = '0';
document.body.appendChild(ta);
ta.select();
var ok = false;
try { ok = document.execCommand('copy'); } catch (e) { ok = false; }
document.body.removeChild(ta);
if (ok) flash();
else openModal({
title: 'Copy anchor link',
body: 'Could not copy automatically. URL:\n' + url,
confirm: { label: 'OK', onConfirm: function () { closeModal(); } }
});
}
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(url).then(flash, fallback);
} else {
fallback();
}
}
function addTask(sectionIndex, headingId) {
var input = document.createElement('input');
input.type = 'text';
input.className = 'modal-input';
input.placeholder = 'Task description';
var ctrl = openModal({
title: 'Add task',
body: input,
confirm: {
label: 'ADD',
initiallyDisabled: true,
onConfirm: function () {
var text = input.value.trim();
if (!text) return;
var action = window.location.pathname + '?addtask=' + sectionIndex;
var target = window.location.pathname + '#' + headingId;
closeModal();
postReplace(action, 'text=' + encodeURIComponent(text), target);
}
}
});
input.addEventListener('input', function () {
ctrl.setConfirmDisabled(input.value.trim() === '');
});
}
allHeadings.forEach(function (h, i) {
if (!h.id) return;
var tag = h.tagName.toLowerCase();
if (tag !== 'h2' && tag !== 'h3' && tag !== 'h4') return;
var sectionIndex = i + 1;
var wrap = document.createElement('span');
wrap.className = 'dropdown heading-anchor';
var trigger = document.createElement('button');
trigger.type = 'button';
trigger.className = 'dropdown-toggle';
trigger.setAttribute('aria-haspopup', 'menu');
trigger.setAttribute('aria-label', 'Section actions');
trigger.textContent = '#';
var menu = document.createElement('div');
menu.className = 'dropdown-menu';
var copyLabel = 'Copy anchor link';
var copyBtn = document.createElement('button');
copyBtn.type = 'button';
copyBtn.className = 'btn btn-tool btn-block';
copyBtn.dataset.action = 'copy-anchor';
copyBtn.textContent = copyLabel;
copyBtn.addEventListener('click', function (e) {
e.stopPropagation();
copyAnchor(h.id, copyBtn, copyLabel, menu);
});
var addBtn = document.createElement('button');
addBtn.type = 'button';
addBtn.className = 'btn btn-tool btn-block';
addBtn.dataset.action = 'add-task';
addBtn.textContent = 'Add task';
addBtn.addEventListener('click', function () { addTask(sectionIndex, h.id); });
menu.appendChild(copyBtn);
menu.appendChild(addBtn);
wrap.appendChild(trigger);
wrap.appendChild(menu);
h.insertBefore(wrap, h.firstChild);
wireDropdown(trigger);
});
}());
+3
View File
@@ -42,6 +42,9 @@
<button class="btn btn-block" data-companion-reveal hidden title="Reveal in file manager">REVEAL ON CLIENT</button>
{{if not .IsRoot}}
<button class="btn btn-block" onclick="movePage()" title="Move page (M)">MOVE PAGE</button>
{{end}}
<button class="btn btn-block" data-action="clean-tasks" onclick="cleanUpTasks()" title="Clean up finished tasks" hidden>CLEAN UP TASKS</button>
{{if not .IsRoot}}
<button class="btn btn-block danger" onclick="deletePage()" title="Delete page">DELETE PAGE</button>
{{end}}
</nav>{{end}}{{if .SidebarWidget}}{{.SidebarWidget}}{{end}}{{end}}
+21
View File
@@ -21,4 +21,25 @@
});
});
});
var hasChecked = !!document.querySelector('input.task-checkbox:checked');
if (hasChecked) {
var btn = document.querySelector('[data-action="clean-tasks"]');
if (btn) btn.hidden = false;
}
})();
function cleanUpTasks() {
openModal({
title: 'Clean up tasks',
body: 'Remove all completed tasks from this page?',
confirm: {
label: 'CLEAN UP',
danger: true,
onConfirm: function () {
closeModal();
postReplace(window.location.pathname + '?cleantasks=1', null, window.location.pathname);
}
}
});
}
+1 -1
View File
@@ -21,7 +21,7 @@ document.addEventListener("DOMContentLoaded", function () {
var a = document.createElement("a");
a.href = "#" + h.id;
var clone = h.cloneNode(true);
clone.querySelectorAll(".btn, .muted, .heading-anchor").forEach(function (el) { el.remove(); });
clone.querySelectorAll(".btn, .muted, .heading-anchor, .dropdown").forEach(function (el) { el.remove(); });
a.textContent = clone.textContent.trim();
li.appendChild(a);
list.appendChild(li);