Komponisten-Button-Bonanza

Ich habe einen Klon dieses TC in meinem GitHub-Repository unter GitHub - denvergeeks/DiscourseComposerButtonBonanza erstellt und dann Fehler behoben [weil der Zugriff auf den Codeberg.org-Dienst zum Forken anscheinend nicht funktionierte.]

Hier ist das Commit-Protokoll meiner Änderungen:

Wenn Sie Fehler mit DCBB hatten und/oder sogar Lösungen für diese Fehler haben, wäre es großartig, wenn Sie ein Upstream-Issue eröffnen könnten, um all dies zu melden, damit ich sie beheben kann – anstatt einfach das Repository irgendwo anders zu klonen und den Leuten zu sagen, sie sollen Ihren Klon verwenden. (Wenn Sie das Repository auf Codeberg aus irgendeinem Grund nicht forken können, wäre es auch schön, wenn Sie mir das direkt mitteilen würden, anstatt beiläufig über ein Diskussionsthema, bei dem ich nicht immer E-Mail-Benachrichtigungen erhalte.)

Oh, hey… gerade als ich auf Antworten drücken wollte, ist ein PR erschienen! Vielen Dank!

Behebt dieser PR eines der Probleme mit <div> gegenüber <span> unter dem WYSIWYG-Composer (was @shauny geplagt hat) – oder behebt er einen anderen Fehler/eine andere Fehlfunktion oder behebt er nur Deprecation Warnings (d. h. Dinge, die jeden Tag kaputtgehen werden, aber noch nicht kaputt sind)?

Ich möchte verstehen, was sich hinter den Kulissen geändert hat. Hinweise auf Themen oder die Discourse-Dokumentation wären sehr willkommen.

Ich habe mit ChatGPT eine Korrektur erstellt, die ich einfach in meine JS-Datei einfügen kann.

Dies fängt die Schaltfläche „Spoiler“ ab und löst stattdessen die Standard-Schaltfläche aus.

Ich würde dies gerne als Teil des Plugins sehen, falls es keine anderen Möglichkeiten gibt (idealerweise würde es nicht das Menü aufrufen und einen Klick auf die Schaltfläche erzwingen, sondern einfach dasselbe auslösen, was die Schaltfläche auslöst).

Hier ist der Code, den ich in die JS-Datei meines Themes eingefügt habe:

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

export default apiInitializer((api) => {
  /**
   * === RTE Spoiler Fix für Composer Button Bonanza ===
   *
   * Desktop: Funktioniert durch Klicken auf Optionen, dann auf „Spoiler verwischen“ (Blur spoiler).
   * Mobile: Das Dropdown-Markup unterscheidet sich, daher müssen wir das Element anhand des Textes und nicht anhand strenger Selektoren finden.
   *
   */

  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() {
    // Bei Discourse verwenden Dropdowns typischerweise einen dieser Container.
    // Wir wählen den letzten sichtbaren (zuletzt geöffneten).
    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;

    // Zuerst versuchen wir die Desktop-ähnliche Struktur (schneller Pfad)
    const exact =
      menuRoot.querySelector('button[title="Blur spoiler"]') ||
      menuRoot.querySelector('button[aria-label="Blur spoiler"]');
    if (exact) return exact;

    // Andernfalls nach Textinhalt irgendwo innerhalb der Dropdown-Elemente suchen
    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")) {
        // Die nächstgelegene tatsächliche Schaltfläche anklicken
        const btn = node.closest("button") || node.querySelector?.("button");
        if (btn) return btn;
        // Wenn es bereits ein button-ähnliches Element ist, gib es zurück
        if (node.tagName === "BUTTON") return node;
      }
    }

    // Letzter Ausweg: Alle Schaltflächen im Menü nach Beschriftungstext durchsuchen
    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;

    // Einige mobile Dropdowns benötigen einen Tick, um die Positionierung/Handler-Anbindung abzuschließen
    requestAnimationFrame(() => {
      requestAnimationFrame(() => btn.click());
    });

    return true;
  }

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

    // Erneut versuchen, bis das Menü und das Element vorhanden sind
    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;

      // Nur in RTE überschreiben; im Markdown-Modus Bonanza in Ruhe lassen.
      if (!isRichTextComposerContext(bonanzaBtn)) return;

      // Verhindern, dass Bonanza BBCode in RTE einfügt
      e.preventDefault();
      e.stopImmediatePropagation();

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

Es ist eine Notlösung, aber sie funktioniert. Nur unter iOS/macOS Safari und macOS Chrome getestet, noch nicht unter Android.