// A-Z precisa da categoria inteira: o core não consegue ordenar uma lista de tópicos por título
// (TopicQuery::SORTABLE_MAPPING não tem "title"), então esgotamos a lista paginada
// antes de ordenar. Limitado — as categorias de documentação são pequenas; qualquer coisa maior
// cai no fallback de ordenar o que já foi carregado, o que o MutationObserver abaixo mantém atualizado.
// ponytail: remova este loop inteiro se o core um dia implementar ordenação de título no servidor.
const MAX_DOC_PAGES = 12; // ~360 tópicos com 30 por página
let docLoadRun = 0;
async function loadAllDocTopics(api) {
const run = ++docLoadRun;
// `controller:discovery/topics` está obsoleto no Discourse atual
// (id de deprecação: discourse.discovery-topics-controller). O Discovery agora usa um
// controlador dedicado por filtro; as categorias de documentação são sempre a lista de tópicos
// mais recentes, então buscamos esse controlador diretamente. Falhar aqui apenas significa que
// ordenamos a primeira página, então cada passo usa optional chaining.
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();
// Navegou para fora durante o carregamento, ou a lista parou de crescer — em qualquer caso, para.
if (run !== docLoadRun || (list.topics?.length ?? 0) === before) {
return;
}
}
}
// Ordena a lista de tópicos da documentação de A a Z, colacionando com o próprio <html lang> da página.
let docSortObservers = new Map();
function docSortLocale() {
const lang = document.documentElement.lang;
if (!lang) {
return undefined;
}
try {
Intl.getCanonicalLocales(lang); // lança RangeError em uma tag malformada
return lang;
} catch {
return undefined; // cai no locale padrão do navegador
}
}
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; // já está ordenado — impede que o observer se alimente a si mesmo
}
// Insere antes do que segue as linhas, mantendo qualquer sentinela de "carregar mais" no final.
const anchor = rows[rows.length - 1].nextSibling;
// Fixa a primeira linha visível para que inserções acima dela não causem salto no scroll.
const pinned = rows.find((row) => row.getBoundingClientRect().bottom > 0);
const pinnedTop = pinned?.getBoundingClientRect().top;
// Observer pausado para que nossos próprios movimentos não reentrem nesta função.
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();
// Não é um container flex/grid, então a reordenação deve mover nós do DOM — CSS `order` é ignorado.
document.querySelectorAll(".topic-list.doc-simple-mode .topic-list-body").forEach((body) => {
// try/finally para que a lista seja sempre revelada mesmo se a ordenação lançar um erro.
try {
sortDocTopicList(body, locale);
} finally {
body.classList.add("docs-sorted");
}
// Ordena apenas uma vez que o carregamento acalme — reordenação durante o carregamento travava o 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);
});
}
Acho que o código provavelmente pode ser melhorado. Aceito sugestões e/ou PRs com prazer.
Ah, desculpe por isso. Eu atualizei a URL, ela é interna e estou ainda trabalhando nela, então por favor, considere-a experimental.
Estou atento a qualquer feedback, sugestão ou erro que você encontrar ao analisar o código ou testá-lo! Sei que não é a solução mais elegante, mas funciona para nosso caso de uso.