作曲家按钮盛宴

我在我的 GitHub 仓库 GitHub - denvergeeks/DiscourseComposerButtonBonanza 中克隆了这个 TC,并应用了针对这些错误的修复 [因为似乎无法访问 Codeberg.org 服务进行分叉]。

这是我的更改的提交日志:

如果您在使用 DCBB 时遇到错误,或者即使您有修复这些错误的方案,如果您能打开一个上游问题来报告所有这些情况,那就太好了,这样我就可以修复它们——而不是仅仅在别处克隆仓库,然后告诉人们使用您的克隆版本。(如果您因为某些原因无法在 Codeberg 上分叉(fork)该仓库,也请直接告知我,而不是通过我并不总能收到邮件更新的讨论主题间接告知。)

哦,嘿……就在我准备点击 回复 时,一个 PR 出现了!谢谢!

这个 PR 是否解决了 WYSIWYG-Composer 下 <div><span> 之间出现的问题(困扰了 @shauny 的问题)——或者它是否解决了其他错误/功能失常,或者仅仅是解决了弃用警告(即那些随时可能中断但尚未中断的事情)?

我想了解幕后发生了哪些变化。任何指向相关主题或 Discourse 文档的指引都将非常感谢。

我与 ChatGPT 合作创建了一个可以简单地放入我的 JS 文件中的修复程序。

它的作用是拦截“Spoiler”(剧透)按钮,然后触发默认按钮。

我很希望看到这成为插件的一部分,如果没有其他方法可以实现(理想情况下,不是调用菜单并强制点击按钮,而是只触发按钮触发的相同操作)。

这是我添加到主题 JS 文件中的代码:

import { apiInitializer } from "discourse/lib/api";

export default apiInitializer((api) => {
  /**
   * === RTE 剧透按钮混乱修复 ===
   *
   * 桌面端:通过点击“Options”(选项),然后点击“Blur spoiler”(模糊剧透)来工作。
   * 移动端:下拉菜单的标记不同,因此我们必须通过文本而不是严格的选择器来定位项目。
   *
   */

  const BONANZA_SPOILER_SELECTOR = "button.ComposerButtonBonanza-btn-spoiler";

  const OPTIONS_TRIGGER_SELECTORS = [
    'button[aria-label="Options"]',
    'button[title="Options"]',
    'button[aria-label*="options" i]',
    'button[title*="options" i]',
  ].join(",");

  function isRichTextComposerContext(el) {
    const composer = el.closest(".d-editor, .composer, .composer-controls");
    if (!composer) return false;

    const hasProseMirror = !!composer.querySelector(".ProseMirror");
    const hasTextarea = !!composer.querySelector("textarea");
    return hasProseMirror && !hasTextarea;
  }

  function getComposerRoot(el) {
    return el.closest(".d-editor, .composer, .composer-controls") || document;
  }

  function normalizeText(s) {
    return (s || "").trim().replace(/\s+/g, " ");
  }

  function isVisible(el) {
    if (!el) return false;
    const style = window.getComputedStyle(el);
    if (!style) return false;
    if (style.display === "none" || style.visibility === "hidden") return false;
    const r = el.getBoundingClientRect?.();
    return !!r && r.width > 0 && r.height > 0;
  }

  function openOptionsMenu(composerRoot) {
    const btn = composerRoot.querySelector(OPTIONS_TRIGGER_SELECTORS);
    if (!btn) return false;
    btn.click();
    return true;
  }

  function findOpenDropdownMenuRoot() {
    // 在 Discourse 上,下拉菜单通常使用这些容器之一。
    // 我们选择最后一个可见的(最近打开的)。
    const candidates = [
      ...document.querySelectorAll(".dropdown-menu, .dropdown-menu__items, .dropdown-menu__content"),
    ].filter(isVisible);

    return candidates.length ? candidates[candidates.length - 1] : null;
  }

  function findBlurSpoilerButton(menuRoot) {
    if (!menuRoot) return null;

    // 首先,尝试桌面式的结构(快速路径)
    const exact =
      menuRoot.querySelector('button[title="Blur spoiler"]') ||
      menuRoot.querySelector('button[aria-label="Blur spoiler"]');
    if (exact) return exact;

    // 否则,通过下拉菜单项中任何位置的文本内容定位
    const itemRoots = Array.from(
      menuRoot.querySelectorAll(".dropdown-menu__item, li, div, button")
    );

    for (const node of itemRoots) {
      const text = normalizeText(node.textContent).toLowerCase();
      if (text === "blur spoiler" || text.includes("blur spoiler")) {
        // 点击最接近的实际按钮
        const btn = node.closest("button") || node.querySelector?.("button");
        if (btn) return btn;
        // 如果它已经是按钮状元素,则返回它
        if (node.tagName === "BUTTON") return node;
      }
    }

    // 最后手段:按标签文本扫描菜单中的所有按钮
    const buttons = Array.from(menuRoot.querySelectorAll("button"));
    for (const btn of buttons) {
      const t = normalizeText(btn.textContent).toLowerCase();
      const title = normalizeText(btn.getAttribute("title") || "").toLowerCase();
      const aria = normalizeText(btn.getAttribute("aria-label") || "").toLowerCase();

      if (t.includes("blur spoiler") || title === "blur spoiler" || aria === "blur spoiler") {
        return btn;
      }
    }

    return null;
  }

  function clickBlurSpoilerFromOpenMenu() {
    const menuRoot = findOpenDropdownMenuRoot();
    const btn = findBlurSpoilerButton(menuRoot);
    if (!btn) return false;

    // 一些移动端下拉菜单需要一个“tick”来完成定位/附加处理程序
    requestAnimationFrame(() => {
      requestAnimationFrame(() => btn.click());
    });

    return true;
  }

  function openOptionsAndClickBlurSpoiler(composerRoot) {
    if (!openOptionsMenu(composerRoot)) return false;

    // 重试直到菜单和项目存在
    let tries = 0;
    const maxTries = 30;

    const attempt = () => {
      tries++;
      if (clickBlurSpoilerFromOpenMenu()) return;
      if (tries < maxTries) setTimeout(attempt, 35);
    };

    setTimeout(attempt, 0);
    return true;
  }

  document.addEventListener(
    "click",
    (e) => {
      const bonanzaBtn = e.target.closest(BONANZA_SPOILER_SELECTOR);
      if (!bonanzaBtn) return;

      // 仅在富文本(RTE)中覆盖;将 Markdown 模式留给 Bonanza 处理。
      if (!isRichTextComposerContext(bonanzaBtn)) return;

      // 阻止 Bonanza 在 RTE 中插入 BBCode
      e.preventDefault();
      e.stopImmediatePropagation();

      const composerRoot = getComposerRoot(bonanzaBtn);
      openOptionsAndClickBlurSpoiler(composerRoot);
    },
    true
  );
});

它很粗糙,但有效。仅在 iOS/macOS Safari 和 macOS Chrome 上进行了测试,尚未在 Android 上测试。

我刚刚发布了 Composer Button Bonanza 的 2.0.0 版本。唯一的改动是修复了对 site.desktopView 的弃用用法。有关详细信息,请参阅此源代码提交

我更新了该警告。经过调查这些问题后,我认为实际上是反过来的:富文本编辑器与此主题组件不兼容,因为其 ProsemirrorTextManipulationTextManipulation 接口的实现不完整和/或不正确。

特别是:

  • ProsemirrorTextManipulation.applyList() 的实现没有完全使用调用者提供的 head 参数。相反,它会查看调用者提供的_示例文本_的键以猜测调用者试图做什么,并且它是硬编码的,只能理解内置的无序列表、有序列表和引用按钮。
  • ProsemirrorTextManipulation.applySurround() 的实现与原始 TextareaTextManipulation.applySurround() 的行为不匹配,负责不加区分地使用 <div>,即使它应该使用 <span>。Prosemirror 实现还会忽略 applySurround()opts 参数。(并且,使用与 applyList() 相同的技巧,它会硬编码示例文本键以检测斜体、粗体和预格式化文本的按钮。)

@renato,这些问题有人关注吗?修复它们有时间表吗?

3 个赞

您能分享一下您到底需要它们用于什么吗?

一些API是在只有文本区域编辑器时构建的,它们的设计初衷并不是要在富文本编辑器上实现完全对等的功能,我们的目的不是将ProseMirror的所有功能都带到一个中间抽象层。

如果可能和有必要,我们可以改进那些地方,但一般来说,当我们只需要复杂操作时,我们通常会通过在注册的富文本编辑器扩展上设置一个 commands 键,直接调用ProseMirror的依赖。例如:

在这个例子中,applySurround 只是盲目地将剧透BBCode应用于任何选中的文本,而 toggleSpoiler 则拥有来自ProseMirror的所有功能,可以决定它是否已经在剧透节点内部,它是一个行内剧透还是一个块级剧透,等等。

1 个赞

如果这两种方法能更忠实地实现它们所属的接口,我认为 Composer Button Bonanza 在新的 RTE(至少和在 Markdown 模式下一样好)中会“开箱即用”。我惊讶地发现还没有其他主题组件或插件作者提出任何相关问题。(尽管,也许他们提出了;我还没有尝试搜索类似的抱怨。)

我不知道“ProseMirror 的所有功能”包含什么,但我怀疑那不是必需的。applyList()applySurround() 接口并不复杂——尽管它们的内容比目前已实现的内容要多。

(到目前为止实现的内容与其说是原则性的“将列表标记应用于选区”或“将文本标记应用于选区周围”,不如说是“仅仅将来自已知内置工具栏按钮的调用分派给特定的 prosemirror 函数”。)

1 个赞