I wonder if we can use the filters in order to display a category using docs view and A-Z (alphabetically).
We have a glossary that should display the topic view ordered ![]()
Currently the filter view is separated from docs:
I ended up using a custom Theme Component for it:
// A-Z needs the whole category: core cannot order a topic list by title
// (TopicQuery::SORTABLE_MAPPING has no "title"), so we exhaust the paginated list
// before sorting. Capped — docs categories are small; anything bigger falls back to
// sorting whatever is loaded, which the MutationObserver below keeps up to date.
// ponytail: drop this whole loop if core ever ships server-side title ordering.
const MAX_DOC_PAGES = 12; // ~360 topics at 30 per page
let docLoadRun = 0;
async function loadAllDocTopics(api) {
const run = ++docLoadRun;
// `controller:discovery/topics` is deprecated in current Discourse
// (deprecation id: discourse.discovery-topics-controller). Discovery now uses a
// dedicated controller per filter; docs categories are always the latest topic
// list, so we look that controller up directly. Failing here just means we sort
// the first page, so every step is optional-chained.
const list = api.container.lookup("controller:discovery/latest")?.model;
if (!list?.loadMore) {
return;
}
for (let page = 0; page < MAX_DOC_PAGES; page++) {
if (!list.more_topics_url) {
return;
}
const before = list.topics?.length ?? 0;
await list.loadMore();
// Navigated away mid-load, or the list stopped growing — either way, stop.
if (run !== docLoadRun || (list.topics?.length ?? 0) === before) {
return;
}
}
}
// Sorts the Docs topic list A-Z, collating with the page's own <html lang>.
let docSortObservers = new Map();
function docSortLocale() {
const lang = document.documentElement.lang;
if (!lang) {
return undefined;
}
try {
Intl.getCanonicalLocales(lang); // throws RangeError on a malformed tag
return lang;
} catch {
return undefined; // falls back to the browser's default locale
}
}
function sortDocTopicList(body, locale) {
const rows = [...body.querySelectorAll(":scope > .topic-list-item")];
if (rows.length < 2) {
return;
}
const sorted = [...rows].sort((a, b) => {
const titleA = a.querySelector(".title")?.textContent.trim() ?? "";
const titleB = b.querySelector(".title")?.textContent.trim() ?? "";
return titleA.localeCompare(titleB, locale);
});
if (sorted.every((row, i) => rows[i] === row)) {
return; // already sorted — stops the observer from feeding itself
}
// Insert before whatever follows the rows, keeping any load-more sentinel last.
const anchor = rows[rows.length - 1].nextSibling;
// Pin the first visible row so inserts above it don't jump the scroll.
const pinned = rows.find((row) => row.getBoundingClientRect().bottom > 0);
const pinnedTop = pinned?.getBoundingClientRect().top;
// Observer paused so our own moves don't re-enter this function.
const observer = docSortObservers.get(body);
observer?.disconnect();
sorted.forEach((row) => body.insertBefore(row, anchor));
observer?.observe(body, { childList: true });
if (pinned) {
window.scrollBy(0, pinned.getBoundingClientRect().top - pinnedTop);
}
}
function sortDocCategoryTopicLists() {
docSortObservers.forEach((observer) => observer.disconnect());
docSortObservers = new Map();
const locale = docSortLocale();
// Not a flex/grid container, so reordering must move DOM nodes — CSS `order` is ignored.
document.querySelectorAll(".topic-list.doc-simple-mode .topic-list-body").forEach((body) => {
// try/finally so the list is always revealed even if sorting throws.
try {
sortDocTopicList(body, locale);
} finally {
body.classList.add("docs-sorted");
}
// Sort only once loading goes quiet — mid-load reordering stalled the loader.
let settleTimer = null;
const observer = new MutationObserver(() => {
window.clearTimeout(settleTimer);
settleTimer = window.setTimeout(() => {
sortDocTopicList(body, locale);
}, 400);
});
observer.observe(body, { childList: true });
docSortObservers.set(body, observer);
});
}
I think the code can very likely be improved, I gladly accept suggestions and/or PRs.