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.