ドキュメントビューでフィルターをA-Z順に組み合わせる?

フィルターを使用して、ドキュメントビューとA-Z(アルファベット順)でカテゴリを表示できるかどうか疑問に思っています。

用語集はトピックビューで並べ替えて表示されるべきです:slight_smile:

現在、フィルタービューはドキュメントとは別になっています。

最終的に、これ用のカスタムTheme Componentを使用することになりました:

// A-Z にはカテゴリ全体が必要です。コアではトピックリストをタイトル順にソートできないため
// (TopicQuery::SORTABLE_MAPPING に "title" がありません)、ソートする前に
// ページネーション付きリストを使い切ります。上限は設定されています —
// ドキュメントのカテゴリは小さいため、それ以上の場合は読み込まれたもののみをソートし、
// 以下の MutationObserver が最新の状態を維持します。
// ponytail: コアがサーバーサイドのタイトル順ソートをリリースしたら、このループ全体を削除してください。
const MAX_DOC_PAGES = 12; // 1ページ30件で約360件
let docLoadRun = 0;

async function loadAllDocTopics(api) {
  const run = ++docLoadRun;

  // `controller:discovery/topics` は現在の Discourse では非推奨です
  // (非推奨 ID: discourse.discovery-topics-controller)。Discovery は現在、
  // フィルターごとに専用のコントローラーを使用しています。ドキュメントのカテゴリは常に
  // 最新のトピックリストであるため、そのコントローラーを直接参照します。
  // ここで失敗しても最初のページをソートするだけなので、すべてのステップはオプションチェーンを使用しています。
  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();

    // 読み込み中にページを離れたり、リストの増加が止まったりした場合、いずれの場合も停止します。
    if (run !== docLoadRun || (list.topics?.length ?? 0) === before) {
      return;
    }
  }
}

// ドキュメントのトピックリストを A-Z でソートし、ページの <html lang> に合わせて照合します。
let docSortObservers = new Map();

function docSortLocale() {
  const lang = document.documentElement.lang;
  if (!lang) {
    return undefined;
  }

  try {
    Intl.getCanonicalLocales(lang); // 不正なタグでは RangeError をスローします
    return lang;
  } catch {
    return undefined; // ブラウザのデフォルトロケールにフォールバックします
  }
}

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; // すでにソート済み — オブザーバーが自身を再帰的に呼び出すのを防ぎます
  }

  // 行の後に続くものの前に挿入し、load-more センチネルを最後に保ちます。
  const anchor = rows[rows.length - 1].nextSibling;

  // 最初の表示可能な行を固定し、その上への挿入がスクロールをジャンプさせないようにします。
  const pinned = rows.find((row) => row.getBoundingClientRect().bottom > 0);
  const pinnedTop = pinned?.getBoundingClientRect().top;

  // 自身の移動が関数を再帰的に呼び出さないように、オブザーバーを一時停止します。
  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();

  // flex/grid コンテナではないため、並べ替えには DOM ノードを移動する必要があり、CSS の `order` は無視されます。
  document.querySelectorAll(".topic-list.doc-simple-mode .topic-list-body").forEach((body) => {
    // ソートで例外がスローされてもリストが常に表示されるように try/finally を使用します。
    try {
      sortDocTopicList(body, locale);
    } finally {
      body.classList.add("docs-sorted");
    }

    // 読み込みが静まるまでソートしない — 読み込み中の並べ替えはローダーを停止させていました。
    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);
  });
}

このコードは大幅に改善できる可能性が高いと思います。提案や PR を喜んで受け入れます。