将过滤器(顺序 A-Z)与文档视图混合使用?

我想知道我们是否可以使用过滤器来使用文档视图和 A-Z(按字母顺序)显示一个类别。

我们有一个应该按主题视图排序的词汇表 :slight_smile:

目前,过滤器视图与文档是分开的:

我最终为它使用了一个自定义的 Theme Component

// A-Z 排序需要获取整个类别:核心功能无法按标题对主题列表进行排序
// (TopicQuery::SORTABLE_MAPPING 中没有 "title"),因此我们需要在排序前
// 耗尽分页列表。这里设置了上限——文档类别通常较小;如果更大,则回退到
// 对已加载内容进行排序,下方的 MutationObserver 会保持其更新。
// ponytail: 如果核心功能将来实现了服务器端标题排序,请删除整个循环。
const MAX_DOC_PAGES = 12; // 每页 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; // 已排序——防止观察者自我触发
  }

  // 插入到行后面的元素之前,确保任何“加载更多”哨兵保持在最后。
  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。