Bonanza dei pulsanti del compositore

Ho creato un clone di questo TC nel mio repository GitHub all’indirizzo GitHub - denvergeeks/DiscourseComposerButtonBonanza e ho applicato correzioni per questi errori [perché l’accesso al servizio Codeberg.org sembrava non funzionare per il forking.]

Ecco il log dei commit delle mie modifiche:

Se hai riscontrato errori con DCBB e/o hai delle correzioni per tali errori, sarebbe fantastico se potessi aprire una issue upstream per segnalare tutto ciò, in modo che io possa correggerli — invece di limitarti a clonare il repository altrove e dire alle persone di usare la tua copia. (Se per qualche motivo non puoi forcare il repository su Codeberg, sarebbe comunque bello che mi venisse comunicato direttamente, invece che incidentalmente tramite un argomento di discussione di cui non ricevo sempre gli aggiornamenti via email.)

Oh, ehi… proprio mentre sto per premere Rispondi, è apparso un PR! Grazie!

Questo PR affronta alcuni dei problemi che si verificano con <div> rispetto a <span> sotto WYSIWYG-Composer (ciò che ha afflitto @shauny) — o affronta qualche altro errore/disfunzione, o affronta solo gli avvisi di deprecazione (cioè cose che si romperanno da un giorno all’altro, ma non sono ancora rotte)?

Vorrei capire cosa è cambiato dietro le quinte. Qualsiasi riferimento a discussioni o alla documentazione di Discourse sarebbe molto apprezzato.

Ho lavorato con ChatGPT per creare una correzione che potrei semplicemente inserire nel mio file JS.

Questo fa sì che intercetti il pulsante “Spoiler” e attivi invece il pulsante predefinito.

Mi piacerebbe vedere questo come parte del plugin, se non ci sono altri modi per farlo (idealmente invece di richiamare il menu e forzare un clic sul pulsante, attiverebbe la stessa cosa che attiva il pulsante).

Ecco il codice che ho aggiunto al file JS del mio tema:

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

export default apiInitializer((api) => {
  /**
   * === Correzione Spoiler RTE per Composer Button Bonanza ===
   *
   * Desktop: funziona cliccando su Opzioni, poi cliccando su "Sfoca spoiler".
   * Mobile: il markup del menu a discesa è diverso, quindi dobbiamo individuare l'elemento tramite testo, non selettori rigidi.
   *
   */

  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() {
    // Su Discourse, i menu a discesa utilizzano in genere uno di questi contenitori.
    // Scegliamo l'ultimo visibile (aperto più di recente).
    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;

    // Prima, prova la struttura più simile al desktop (percorso veloce)
    const exact =
      menuRoot.querySelector('button[title="Blur spoiler"]') ||
      menuRoot.querySelector('button[aria-label="Blur spoiler"]');
    if (exact) return exact;

    // Altrimenti, individua tramite il contenuto testuale ovunque all'interno degli elementi del menu a discesa
    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")) {
        // Clicca sul pulsante effettivo più vicino
        const btn = node.closest("button") || node.querySelector?.("button");
        if (btn) return btn;
        // Se è già un elemento simile a un pulsante, restituiscilo
        if (node.tagName === "BUTTON") return node;
      }
    }

    // Ultima risorsa: scansiona tutti i pulsanti nel menu tramite il testo dell'etichetta
    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;

    // Alcuni menu a discesa mobili necessitano di un tick per terminare il posizionamento/l'associazione degli handler
    requestAnimationFrame(() => {
      requestAnimationFrame(() => btn.click());
    });

    return true;
  }

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

    // Riprova finché il menu e l'elemento non esistono
    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;

      // Sovrascrivi solo nel contesto RTE; in modalità markdown lascia Bonanza indisturbato.
      if (!isRichTextComposerContext(bonanzaBtn)) return;

      // Impedisci l'inserimento di BBCode di Bonanza in RTE
      e.preventDefault();
      e.stopImmediatePropagation();

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

È una soluzione grezza, ma funziona. Testato solo su iOS/macOS Safari e macOS Chrome, non ancora su Android.