improve task list handling
This commit is contained in:
+38
-10
@@ -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
@@ -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);
|
||||
});
|
||||
}());
|
||||
|
||||
@@ -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,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
@@ -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);
|
||||
|
||||
+11
-3
@@ -273,12 +273,20 @@ main {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
a.heading-anchor {
|
||||
color: var(--text-muted);
|
||||
.dropdown.heading-anchor {
|
||||
margin-right: 0.4em;
|
||||
font-weight: normal;
|
||||
}
|
||||
a.heading-anchor:hover {
|
||||
.heading-anchor .dropdown-toggle {
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
color: var(--text-muted);
|
||||
font: inherit;
|
||||
font-weight: normal;
|
||||
}
|
||||
.heading-anchor .dropdown-toggle:hover {
|
||||
color: var(--primary-hover);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user