breadcrumps, todos, etc.
This commit is contained in:
@@ -273,6 +273,48 @@ body {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
|
||||
.toc-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.toc-row .toc-link {
|
||||
min-width: 0;
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.toc-edit-link {
|
||||
flex: 0 0 auto;
|
||||
padding: 2px 5px;
|
||||
color: #777;
|
||||
text-decoration: none;
|
||||
font-size: .78rem;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.toc-edit-link:hover {
|
||||
color: var(--wiki-link);
|
||||
}
|
||||
|
||||
.editor-template-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: .86rem;
|
||||
}
|
||||
|
||||
.editor-template-row label {
|
||||
color: var(--wiki-muted);
|
||||
}
|
||||
|
||||
#template-select {
|
||||
min-width: 180px;
|
||||
max-width: 360px;
|
||||
padding: 4px 6px;
|
||||
}
|
||||
|
||||
.pages-section {
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid #d7d7d7;
|
||||
@@ -1048,3 +1090,63 @@ body.editor-mode .editor-metadata-row {
|
||||
.wiki-graph-node-focus text {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
|
||||
/* Recent changes and bookmark overview pages. */
|
||||
.special-page-list,
|
||||
.bookmarks-list {
|
||||
max-width: 920px;
|
||||
}
|
||||
|
||||
.special-page-row,
|
||||
.bookmark-row {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
gap: 1rem;
|
||||
padding: .45rem 0;
|
||||
border-bottom: 1px solid var(--wiki-border);
|
||||
}
|
||||
|
||||
.special-page-row a,
|
||||
.bookmark-row a,
|
||||
.bookmark-section-actions a {
|
||||
color: var(--wiki-link);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.special-page-row a:hover,
|
||||
.bookmark-row a:hover,
|
||||
.bookmark-section-actions a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.special-page-meta,
|
||||
.bookmark-section-actions {
|
||||
color: var(--wiki-muted);
|
||||
font-size: .86rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.bookmark-section {
|
||||
margin: 1.4rem 0 1.8rem;
|
||||
}
|
||||
|
||||
.bookmark-section h2 {
|
||||
margin: 0 0 .35rem;
|
||||
font-size: 1.15rem;
|
||||
}
|
||||
|
||||
/* Linked todo markers --------------------------------------------------- */
|
||||
.wiki-todo {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.wiki-todo:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.todo-item-target {
|
||||
background: #fff3a3;
|
||||
outline: 2px solid #e1c75a;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
<div id="app">
|
||||
<aside id="sidebar">
|
||||
<a id="wiki-brand" class="brand" href="#">Racket Wiki</a>
|
||||
<a id="recent-link" class="sidebar-primary-link" href="#">* <span data-tr="recent">Recent</span></a>
|
||||
<a id="bookmarks-link" class="sidebar-primary-link" href="#">* <span data-tr="bookmarks">Bookmarks</span></a>
|
||||
<a id="todo-link" class="sidebar-primary-link" href="#">* <span data-tr="todo-list">Todo list</span></a>
|
||||
<a id="graph-link" class="sidebar-primary-link" href="#">* <span data-tr="graph">Graph</span></a>
|
||||
<form id="search-form" class="wiki-search" role="search">
|
||||
@@ -45,6 +47,7 @@
|
||||
<div class="navigation-row">
|
||||
<nav id="breadcrumbs" class="breadcrumbs hidden" aria-label="Breadcrumb"></nav>
|
||||
<div id="page-action-links" class="page-action-links hidden">
|
||||
<a id="bookmark-page" href="#" data-tr="bookmark">Bookmark</a>
|
||||
<a id="edit-page" class="editor-only hidden" href="#" data-tr="edit">Edit</a>
|
||||
<a id="history-page" href="#" data-tr="history">History</a>
|
||||
<a id="delete-page" class="editor-only danger hidden" href="#" data-tr="delete">Delete</a>
|
||||
@@ -76,6 +79,12 @@
|
||||
<div class="editor-heading">
|
||||
<input id="editor-title" class="title-input" placeholder="Page title" data-tr-placeholder="page-title">
|
||||
<div id="editor-slug-info" class="muted"></div>
|
||||
<div class="editor-template-row">
|
||||
<label for="template-select" data-tr="template">Template</label>
|
||||
<select id="template-select">
|
||||
<option value="" data-tr="no-template">No template</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="toolbar">
|
||||
<button id="cancel-edit" data-tr="cancel">Cancel</button>
|
||||
@@ -101,6 +110,21 @@
|
||||
<div id="search-results" class="search-results"></div>
|
||||
</section>
|
||||
|
||||
|
||||
<section id="recent-view" class="hidden">
|
||||
<header class="page-header">
|
||||
<h1 data-tr="recent-changes">Recent changes</h1>
|
||||
</header>
|
||||
<div id="recent-list" class="special-page-list"></div>
|
||||
</section>
|
||||
|
||||
<section id="bookmarks-view" class="hidden">
|
||||
<header class="page-header">
|
||||
<h1 data-tr="bookmarks">Bookmarks</h1>
|
||||
</header>
|
||||
<div id="bookmarks-list" class="bookmarks-list"></div>
|
||||
</section>
|
||||
|
||||
<section id="todo-view" class="hidden">
|
||||
<header class="page-header">
|
||||
<h1 data-tr="todo-items">Todo items</h1>
|
||||
|
||||
+294
-19
@@ -11,7 +11,8 @@
|
||||
translations: {},
|
||||
translationPage: "wiki-translations",
|
||||
translationTemplate: "",
|
||||
siteTitle: "Racket Wiki"
|
||||
siteTitle: "Racket Wiki",
|
||||
bookmarks: []
|
||||
};
|
||||
|
||||
let easyMDE = null;
|
||||
@@ -19,7 +20,7 @@
|
||||
const $ = (id) => document.getElementById(id);
|
||||
|
||||
function show(viewId) {
|
||||
for (const id of ["page-view", "not-found-view", "editor-view", "search-view", "todo-view", "graph-view", "history-view", "admin-view", "user-admin-view"]) {
|
||||
for (const id of ["page-view", "not-found-view", "editor-view", "search-view", "recent-view", "bookmarks-view", "todo-view", "graph-view", "history-view", "admin-view", "user-admin-view"]) {
|
||||
$(id).classList.toggle("hidden", id !== viewId);
|
||||
}
|
||||
$("page-action-links").classList.toggle("hidden", viewId !== "page-view");
|
||||
@@ -50,16 +51,25 @@
|
||||
.replaceAll('"', """);
|
||||
}
|
||||
|
||||
function expandTodoMarkup(markdown) {
|
||||
function expandTodoMarkup(markdown, pageSlug = null) {
|
||||
let inFence = false;
|
||||
let todoNumber = 0;
|
||||
return (markdown || "").split("\n").map((line) => {
|
||||
if (/^\s*(```|~~~)/.test(line)) {
|
||||
inFence = !inFence;
|
||||
return line;
|
||||
}
|
||||
if (inFence) return line;
|
||||
return line.replace(/todo\(([^()\r\n]+)\)/g, (_match, text) =>
|
||||
`<span class="wiki-todo"><strong>TODO</strong> ${escapeHtml(text.trim())}</span>`);
|
||||
return line.replace(/todo\(([^()\r\n]+)\)/gi, (_match, text) => {
|
||||
todoNumber += 1;
|
||||
const label = escapeHtml(tr("todo", "Todo"));
|
||||
const todoText = escapeHtml(text.trim());
|
||||
if (pageSlug) {
|
||||
const href = `#todo/${encodeURIComponent(pageSlug)}/${todoNumber}`;
|
||||
return `<a class="wiki-todo" href="${href}"><strong>${label}:</strong> ${todoText}</a>`;
|
||||
}
|
||||
return `<span class="wiki-todo"><strong>${label}:</strong> ${todoText}</span>`;
|
||||
});
|
||||
}).join("\n");
|
||||
}
|
||||
|
||||
@@ -138,8 +148,8 @@
|
||||
});
|
||||
}
|
||||
|
||||
function renderMarkdown(markdown) {
|
||||
const html = easyMDE.markdown(expandTodoMarkup(markdown || ""));
|
||||
function renderMarkdown(markdown, pageSlug = null) {
|
||||
const html = easyMDE.markdown(expandTodoMarkup(markdown || "", pageSlug));
|
||||
return DOMPurify.sanitize(applyImageWidthMarkup(html));
|
||||
}
|
||||
|
||||
@@ -302,7 +312,7 @@
|
||||
.then(onSuccess)
|
||||
.catch((error) => onError(error.message));
|
||||
},
|
||||
previewRender: (plainText) => easyMDE ? renderMarkdown(plainText) : "",
|
||||
previewRender: (plainText) => easyMDE ? renderMarkdown(plainText, state.currentPage?.slug || state.newPageSlug) : "",
|
||||
renderingConfig: {
|
||||
codeSyntaxHighlighting: true,
|
||||
hljs: window.hljs,
|
||||
@@ -419,7 +429,7 @@
|
||||
return headings;
|
||||
}
|
||||
|
||||
function renderToc(entries, onSelect) {
|
||||
function renderToc(entries, onSelect, onEdit = null) {
|
||||
const toc = $("toc-list");
|
||||
toc.replaceChildren();
|
||||
|
||||
@@ -432,6 +442,9 @@
|
||||
}
|
||||
|
||||
for (const entry of entries) {
|
||||
const row = document.createElement("div");
|
||||
row.className = "toc-row";
|
||||
|
||||
const link = document.createElement("a");
|
||||
link.href = entry.href || "#";
|
||||
link.className = `toc-link toc-level-${Math.min(entry.level, 4)}`;
|
||||
@@ -440,14 +453,32 @@
|
||||
event.preventDefault();
|
||||
onSelect(entry);
|
||||
});
|
||||
toc.append(link);
|
||||
row.append(link);
|
||||
|
||||
if (onEdit) {
|
||||
const edit = document.createElement("a");
|
||||
edit.href = "#";
|
||||
edit.className = "toc-edit-link";
|
||||
edit.textContent = "✎";
|
||||
edit.title = tr("edit-section", "Edit section");
|
||||
edit.setAttribute("aria-label", `${tr("edit-section", "Edit section")}: ${entry.text}`);
|
||||
edit.addEventListener("click", (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
onEdit(entry);
|
||||
});
|
||||
row.append(edit);
|
||||
}
|
||||
|
||||
toc.append(row);
|
||||
}
|
||||
}
|
||||
|
||||
function renderPageToc() {
|
||||
const article = $("markdown-preview");
|
||||
const usedIds = new Set();
|
||||
const entries = Array.from(article.querySelectorAll("h1, h2, h3, h4, h5, h6")).map((heading) => {
|
||||
const sourceHeadings = markdownHeadings(state.currentPage?.markdown || "");
|
||||
const entries = Array.from(article.querySelectorAll("h1, h2, h3, h4, h5, h6")).map((heading, index) => {
|
||||
const text = heading.textContent.trim();
|
||||
const id = heading.id || headingId(text, usedIds);
|
||||
heading.id = id;
|
||||
@@ -455,14 +486,21 @@
|
||||
level: Number(heading.tagName.slice(1)),
|
||||
text,
|
||||
href: `#${id}`,
|
||||
element: heading
|
||||
element: heading,
|
||||
line: sourceHeadings[index]?.line ?? null
|
||||
};
|
||||
});
|
||||
|
||||
renderToc(entries, (entry) => {
|
||||
entry.element.scrollIntoView({ behavior: "smooth", block: "start" });
|
||||
history.replaceState(null, "", `#/${encodeURIComponent(state.currentPage.slug)}`);
|
||||
});
|
||||
renderToc(
|
||||
entries,
|
||||
(entry) => {
|
||||
entry.element.scrollIntoView({ behavior: "smooth", block: "start" });
|
||||
history.replaceState(null, "", `#/${encodeURIComponent(state.currentPage.slug)}`);
|
||||
},
|
||||
can("editor")
|
||||
? (entry) => beginEditPageAtLine(entry.line)
|
||||
: null
|
||||
);
|
||||
}
|
||||
|
||||
function renderEditorToc() {
|
||||
@@ -475,6 +513,20 @@
|
||||
});
|
||||
}
|
||||
|
||||
function focusEditorLine(line) {
|
||||
if (!easyMDE || line === null || line === undefined) return;
|
||||
easyMDE.codemirror.setCursor({ line, ch: 0 });
|
||||
easyMDE.codemirror.scrollIntoView({ line, ch: 0 }, 160);
|
||||
easyMDE.codemirror.focus();
|
||||
}
|
||||
|
||||
function beginEditPageAtLine(line) {
|
||||
beginEditPage();
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(() => focusEditorLine(line));
|
||||
});
|
||||
}
|
||||
|
||||
function renderBreadcrumbs(items) {
|
||||
const breadcrumbs = $("breadcrumbs");
|
||||
breadcrumbs.replaceChildren();
|
||||
@@ -584,6 +636,73 @@
|
||||
state.pages = result.pages;
|
||||
updateWikiIdentity();
|
||||
renderPageList();
|
||||
updateTemplateSelect();
|
||||
}
|
||||
|
||||
|
||||
function templatePages() {
|
||||
return state.pages.filter((page) => page.slug.toLocaleLowerCase().startsWith("template-"));
|
||||
}
|
||||
|
||||
function updateTemplateSelect() {
|
||||
const select = $("template-select");
|
||||
const selected = select.value;
|
||||
select.replaceChildren();
|
||||
|
||||
const none = document.createElement("option");
|
||||
none.value = "";
|
||||
none.textContent = tr("no-template", "No template");
|
||||
select.append(none);
|
||||
|
||||
for (const page of templatePages()) {
|
||||
const option = document.createElement("option");
|
||||
option.value = page.slug;
|
||||
option.textContent = page.title;
|
||||
select.append(option);
|
||||
}
|
||||
|
||||
select.value = Array.from(select.options).some((option) => option.value === selected) ? selected : "";
|
||||
}
|
||||
|
||||
async function applyTemplate(slug) {
|
||||
if (!slug) return;
|
||||
const template = await api(`/api/pages/${encodeURIComponent(slug)}`);
|
||||
const current = easyMDE.value();
|
||||
|
||||
if (current.trim() !== "") {
|
||||
const message = tr("replace-with-template", "Replace the current page content with template {template}?")
|
||||
.replace("{template}", template.title);
|
||||
if (!window.confirm(message)) {
|
||||
$("template-select").value = "";
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
easyMDE.value(template.markdown || "");
|
||||
$("template-select").value = "";
|
||||
renderEditorToc();
|
||||
easyMDE.codemirror.focus();
|
||||
}
|
||||
|
||||
function bookmarkForSlug(slug) {
|
||||
return state.bookmarks.find((bookmark) => bookmark.slug === slug) || null;
|
||||
}
|
||||
|
||||
function updateBookmarkAction() {
|
||||
const link = $("bookmark-page");
|
||||
if (!state.currentPage) {
|
||||
link.classList.add("hidden");
|
||||
return;
|
||||
}
|
||||
link.classList.remove("hidden");
|
||||
const bookmark = bookmarkForSlug(state.currentPage.slug);
|
||||
link.textContent = bookmark ? tr("bookmarked", "Bookmarked") : tr("bookmark", "Bookmark");
|
||||
}
|
||||
|
||||
async function loadBookmarks() {
|
||||
const result = await api("/api/bookmarks");
|
||||
state.bookmarks = result.bookmarks || [];
|
||||
updateBookmarkAction();
|
||||
}
|
||||
|
||||
async function openPage(slug) {
|
||||
@@ -593,13 +712,14 @@
|
||||
state.newPageSlug = null;
|
||||
$("page-title").textContent = page.title;
|
||||
$("page-meta").textContent = "";
|
||||
$("markdown-preview").innerHTML = renderMarkdown(page.markdown);
|
||||
$("markdown-preview").innerHTML = renderMarkdown(page.markdown, page.slug);
|
||||
renderPageDetails(page);
|
||||
pageBreadcrumbs(page);
|
||||
show("page-view");
|
||||
setPageActionVisibility(true);
|
||||
renderPageToc();
|
||||
renderPageList();
|
||||
updateBookmarkAction();
|
||||
}
|
||||
|
||||
function updateEditorSlugInfo() {
|
||||
@@ -628,6 +748,7 @@
|
||||
}
|
||||
$("edit-summary").value = "";
|
||||
$("save-status").textContent = "";
|
||||
$("template-select").value = "";
|
||||
updateEditorSlugInfo();
|
||||
activateEditor();
|
||||
$("editor-title").focus();
|
||||
@@ -647,6 +768,7 @@
|
||||
easyMDE.value(state.currentPage.markdown);
|
||||
$("edit-summary").value = "";
|
||||
$("save-status").textContent = "";
|
||||
$("template-select").value = "";
|
||||
updateEditorSlugInfo();
|
||||
activateEditor();
|
||||
}
|
||||
@@ -951,7 +1073,7 @@
|
||||
view.textContent = tr("view", "View");
|
||||
view.addEventListener("click", async () => {
|
||||
const full = await api(`/api/pages/${encodeURIComponent(state.currentPage.slug)}/versions/${encodeURIComponent(version.version)}`);
|
||||
$("diff-target").innerHTML = `<article class="markdown-body">${renderMarkdown(full.markdown)}</article>`;
|
||||
$("diff-target").innerHTML = `<article class="markdown-body">${renderMarkdown(full.markdown, state.currentPage?.slug)}</article>`;
|
||||
});
|
||||
const compare = document.createElement("button");
|
||||
compare.textContent = index + 1 < result.versions.length ? tr("compare-previous", "Compare previous") : "";
|
||||
@@ -1081,6 +1203,12 @@
|
||||
}
|
||||
|
||||
async function route() {
|
||||
const todoMatch = location.hash.match(/^#todo\/([^/]+)\/(\d+)$/);
|
||||
if (todoMatch) {
|
||||
await showTodos(decodeURIComponent(todoMatch[1]), Number(todoMatch[2]));
|
||||
return;
|
||||
}
|
||||
|
||||
const match = location.hash.match(/^#\/([^/]+)$/);
|
||||
if (match) {
|
||||
const slug = decodeURIComponent(match[1]);
|
||||
@@ -1116,7 +1244,130 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function showTodos() {
|
||||
|
||||
function formatTimestamp(value) {
|
||||
const date = new Date(Number(value) * 1000);
|
||||
return date.toLocaleString(state.language || undefined);
|
||||
}
|
||||
|
||||
async function showRecent() {
|
||||
state.previousView = state.currentPage ? "page-view" : "recent-view";
|
||||
renderBreadcrumbs([
|
||||
{ label: state.siteTitle, href: "/" },
|
||||
{ label: tr("recent", "Recent") }
|
||||
]);
|
||||
setPageActionVisibility(false);
|
||||
show("recent-view");
|
||||
|
||||
const result = await api("/api/recent");
|
||||
const list = $("recent-list");
|
||||
list.replaceChildren();
|
||||
const pages = result.pages || [];
|
||||
if (pages.length === 0) {
|
||||
const empty = document.createElement("p");
|
||||
empty.className = "muted";
|
||||
empty.textContent = tr("no-recent-pages", "No recent pages.");
|
||||
list.append(empty);
|
||||
return;
|
||||
}
|
||||
|
||||
for (const page of pages) {
|
||||
const row = document.createElement("div");
|
||||
row.className = "special-page-row";
|
||||
const link = document.createElement("a");
|
||||
link.href = `#/${encodeURIComponent(page.slug)}`;
|
||||
link.textContent = page.title;
|
||||
const meta = document.createElement("span");
|
||||
meta.className = "special-page-meta";
|
||||
meta.textContent = `${formatTimestamp(page.updatedAt)} · ${tr("changed-by", "changed by")} ${page.updatedBy}`;
|
||||
row.append(link, meta);
|
||||
list.append(row);
|
||||
}
|
||||
}
|
||||
|
||||
async function saveBookmark(slug, existingSection = "") {
|
||||
const section = window.prompt(tr("bookmark-section", "Bookmark section"), existingSection);
|
||||
if (section === null) return;
|
||||
await api("/api/bookmarks", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ slug, section: section.trim() })
|
||||
});
|
||||
await loadBookmarks();
|
||||
}
|
||||
|
||||
async function removeBookmark(slug) {
|
||||
await api(`/api/bookmarks/${encodeURIComponent(slug)}`, { method: "DELETE" });
|
||||
await loadBookmarks();
|
||||
}
|
||||
|
||||
async function showBookmarks() {
|
||||
state.previousView = state.currentPage ? "page-view" : "bookmarks-view";
|
||||
renderBreadcrumbs([
|
||||
{ label: state.siteTitle, href: "/" },
|
||||
{ label: tr("bookmarks", "Bookmarks") }
|
||||
]);
|
||||
setPageActionVisibility(false);
|
||||
show("bookmarks-view");
|
||||
await loadBookmarks();
|
||||
|
||||
const target = $("bookmarks-list");
|
||||
target.replaceChildren();
|
||||
if (state.bookmarks.length === 0) {
|
||||
const empty = document.createElement("p");
|
||||
empty.className = "muted";
|
||||
empty.textContent = tr("no-bookmarks", "No bookmarks yet.");
|
||||
target.append(empty);
|
||||
return;
|
||||
}
|
||||
|
||||
const groups = new Map();
|
||||
for (const bookmark of state.bookmarks) {
|
||||
const section = bookmark.section || "";
|
||||
if (!groups.has(section)) groups.set(section, []);
|
||||
groups.get(section).push(bookmark);
|
||||
}
|
||||
|
||||
for (const [section, bookmarks] of groups.entries()) {
|
||||
const sectionElement = document.createElement("section");
|
||||
sectionElement.className = "bookmark-section";
|
||||
const heading = document.createElement("h2");
|
||||
heading.textContent = section || tr("bookmarks", "Bookmarks");
|
||||
sectionElement.append(heading);
|
||||
|
||||
for (const bookmark of bookmarks) {
|
||||
const row = document.createElement("div");
|
||||
row.className = "bookmark-row";
|
||||
const link = document.createElement("a");
|
||||
link.href = `#/${encodeURIComponent(bookmark.slug)}`;
|
||||
link.textContent = bookmark.title;
|
||||
const actions = document.createElement("span");
|
||||
actions.className = "bookmark-section-actions";
|
||||
const move = document.createElement("a");
|
||||
move.href = "#";
|
||||
move.textContent = tr("move", "Move");
|
||||
move.addEventListener("click", async (event) => {
|
||||
event.preventDefault();
|
||||
await saveBookmark(bookmark.slug, bookmark.section || "");
|
||||
await showBookmarks();
|
||||
});
|
||||
const separator = document.createTextNode(" · ");
|
||||
const remove = document.createElement("a");
|
||||
remove.href = "#";
|
||||
remove.textContent = tr("remove", "Remove");
|
||||
remove.addEventListener("click", async (event) => {
|
||||
event.preventDefault();
|
||||
await removeBookmark(bookmark.slug);
|
||||
await showBookmarks();
|
||||
});
|
||||
actions.append(move, separator, remove);
|
||||
row.append(link, actions);
|
||||
sectionElement.append(row);
|
||||
}
|
||||
target.append(sectionElement);
|
||||
}
|
||||
}
|
||||
|
||||
async function showTodos(targetSlug = null, targetNumber = null) {
|
||||
state.previousView = state.currentPage ? "page-view" : "todo-view";
|
||||
renderBreadcrumbs([
|
||||
{ label: state.siteTitle, href: "/" },
|
||||
@@ -1137,6 +1388,7 @@
|
||||
for (const item of result.items) {
|
||||
const row = document.createElement("article");
|
||||
row.className = "todo-item";
|
||||
row.id = `todo-${item.slug}-${item.number}`;
|
||||
const link = document.createElement("a");
|
||||
link.href = `#/${encodeURIComponent(item.slug)}`;
|
||||
link.textContent = item.title;
|
||||
@@ -1149,6 +1401,14 @@
|
||||
row.append(link, text, location);
|
||||
list.append(row);
|
||||
}
|
||||
|
||||
if (targetSlug && targetNumber) {
|
||||
const target = document.getElementById(`todo-${targetSlug}-${targetNumber}`);
|
||||
if (target) {
|
||||
target.classList.add("todo-item-target");
|
||||
target.scrollIntoView({ block: "center" });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function pageLinks(markdown) {
|
||||
@@ -1348,6 +1608,7 @@
|
||||
$("account-role").textContent = tr(`role-${state.session.user.role}`, state.session.user.role);
|
||||
updateRoleUi();
|
||||
await loadPages();
|
||||
await loadBookmarks();
|
||||
await route();
|
||||
startKeepAlive();
|
||||
}
|
||||
@@ -1359,6 +1620,12 @@
|
||||
searchWiki($("search-input").value).catch((error) => console.error(error));
|
||||
});
|
||||
|
||||
$("bookmark-page").addEventListener("click", (event) => {
|
||||
event.preventDefault();
|
||||
if (!state.currentPage) return;
|
||||
const existing = bookmarkForSlug(state.currentPage.slug);
|
||||
saveBookmark(state.currentPage.slug, existing?.section || "").catch((error) => console.error(error));
|
||||
});
|
||||
$("edit-page").addEventListener("click", (event) => { event.preventDefault(); beginEditPage(); });
|
||||
$("delete-page").addEventListener("click", (event) => { event.preventDefault(); deleteCurrentPage(); });
|
||||
$("history-page").addEventListener("click", (event) => { event.preventDefault(); showHistory(); });
|
||||
@@ -1372,6 +1639,8 @@
|
||||
event.preventDefault();
|
||||
goHome().catch((error) => console.error(error));
|
||||
});
|
||||
$("recent-link").addEventListener("click", (event) => { event.preventDefault(); showRecent().catch((error) => console.error(error)); });
|
||||
$("bookmarks-link").addEventListener("click", (event) => { event.preventDefault(); showBookmarks().catch((error) => console.error(error)); });
|
||||
$("todo-link").addEventListener("click", (event) => { event.preventDefault(); showTodos().catch((error) => console.error(error)); });
|
||||
$("graph-link").addEventListener("click", (event) => { event.preventDefault(); showGraph().catch((error) => console.error(error)); });
|
||||
$("context-link").addEventListener("click", (event) => { event.preventDefault(); showContextGraph().catch((error) => console.error(error)); });
|
||||
@@ -1410,6 +1679,12 @@
|
||||
route();
|
||||
});
|
||||
$("save-page").addEventListener("click", savePage);
|
||||
$("template-select").addEventListener("change", (event) => {
|
||||
applyTemplate(event.target.value).catch((error) => {
|
||||
$("save-status").textContent = error.message;
|
||||
event.target.value = "";
|
||||
});
|
||||
});
|
||||
$("file-input").addEventListener("change", (event) => {
|
||||
uploadFiles(event.target.files).catch(() => {});
|
||||
event.target.value = "";
|
||||
|
||||
Reference in New Issue
Block a user