Festa di pulsanti Composer

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.

Ho appena rilasciato la versione 2.0.0 di Composer Button Bonanza. L’unica modifica è la correzione dell’uso deprecato di site.desktopView. Vedi il commit sorgente per i dettagli.

Ho aggiornato quell’avviso. Avendo ora indagato sui problemi, penso che sia in realtà il contrario: l’editor di testo arricchito non è compatibile con questo componente tema, perché la sua implementazione ProsemirrorTextManipulation dell’interfaccia TextManipulation è incompleta e/o errata.

In particolare:

  • L’implementazione di ProsemirrorTextManipulation.applyList() non utilizza esattamente il parametro head fornito dal chiamante. Invece, esamina la chiave per il testo di esempio fornito dal chiamante per intuire cosa sta cercando di fare il chiamante, ed è codificata in modo fisso per comprendere solo i pulsanti integrati per elenchi puntati, elenchi numerati e citazioni.
  • L’implementazione di ProsemirrorTextManipulation.applySurround() non corrisponde al comportamento dell’implementazione originale di TextareaTextManipulation.applySurround(), ed è responsabile dell’uso indiscriminato di <div> anche quando dovrebbe usare <span>. L’implementazione Prosemirror ignora anche l’argomento opts di applySurround(). (E, usando lo stesso trucco di applyList(), codifica in modo fisso le chiavi del testo di esempio per rilevare i pulsanti per corsivo, grassetto e testo preformattato.)

@renato, questi problemi sono all’attenzione di qualcuno? C’è una tempistica per risolverli?

3 Mi Piace

Puoi condividere per cosa esattamente ne hai bisogno?

Alcune API create quando esisteva solo l’editor textarea non sono realmente pensate per avere una parità completa con l’editor avanzato, non è nostra intenzione portare tutta la potenza di ProseMirror a un’astrazione intermedia.

Possiamo migliorare quei punti se possibile e necessario, ma in generale quando abbiamo bisogno di operazioni complesse di solito ci rivolgiamo direttamente alle dipendenze di ProseMirror tramite una chiave commands su un’estensione dell’editor avanzato registrata. Per esempio:

In questo esempio, applySurround applica ciecamente il bbcode spoiler a qualsiasi testo selezionato, mentre toggleSpoiler ha tutte le funzionalità di ProseMirror per decidere se è già all’interno di un nodo spoiler, se è uno spoiler inline o uno spoiler a blocco, ecc.

1 Mi Piace

Se questi due metodi fossero implementati con maggiore fedeltà all’interfaccia a cui appartengono, penso che Composer Button Bonanza "funzionerebbe" abbastanza bene nel nuovo RTE (almeno tanto quanto funziona in modalità Markdown). Sono sorpreso che nessun altro componente del tema o autore di plugin abbia sollevato problemi correlati finora. (Anche se, forse l’hanno fatto; non ho provato a cercare lamentele simili.)

Non so cosa comporti "tutta la potenza di ProseMirror", ma dubito che sia necessario. Le interfacce applyList() e applySurround() non sono così complicate, anche se c’è più di quanto sia stato implementato finora.

(Quello che è stato implementato finora sembra essere non tanto un "applicazione del markup di lista alla selezione" o "applicazione del markup di testo che circonda la selezione" basato su principi, quanto più "invio delle chiamate dai pulsanti della barra degli strumenti noti e integrati a specifiche funzioni di prosemirror".)

1 Mi Piace

Ehi!

Questo è un componente fantastico che mi sono perso.

Stavo lavorando sulla mia versione di un componente tema simile, che permette di aggiungere scorciatoie da tastiera personalizzate per wrap personalizzati (sia di tipo [wrap] o qualsiasi contenuto HTML / markdown / specifico di Discourse consentito).

Ad esempio, posso definire Ctrl+Shift+K per inserire o avvolgere la selezione con <kbd> </kbd>, o <hr>, o [wrap=annuncio] [/wrap] (sia inline che block) o qualsiasi cosa desideriamo, e ho anche aggiunto pulsanti opzionali nel menu dell’ingranaggio.
Supporta le nuove righe con \n nel contenuto che inseriamo.

È anche compatibile con l’editor ricco. Il codice per riferimento: GitHub - Canapin/discourse-custom-shortcuts · GitHub, con un avviso: è stato scritto con l’aiuto di Claude e non ho ancora revisionato il codice. Inoltre, se avessi continuato a sviluppare la mia soluzione invece di usare la tua, probabilmente cambierei o aggiungerei funzionalità per utilizzare alcune delle tue.

Ecco come appare il mio componente:




Quindi, tornando al tuo componente, mi piace la sua versatilità, ma mancano due cose fondamentali per me:

  • Supporto per l’editor ricco
  • Un modo semplice per aggiungere contenuti personalizzati (wrap personalizzati, tag o altri)

Anche se posso scrivere il mio codice con l’aiuto dell’AI, non mi sento abbastanza a mio agio da affidarmi a proposte di pull request generate dall’AI (ad esempio per il supporto dell’editor ricco) per il tuo componente, e non sono un programmatore.

Oltre alla questione dell’editor ricco, qual è la tua opinione sulla fattibilità di aggiungere nuovi contenuti personalizzati dal tuo componente (in pratica quello che ho descritto del mio componente)? Sarebbe accettato o è fuori dal suo ambito?

Non capisco molto bene la tua domanda… Composer Button Bonanza non supporta già questa funzionalità?

Hai provato a personalizzare le impostazioni del componente, in particolare l’impostazione “Pulsanti”?

  • “Pulsanti” serve per definire i pulsanti (in realtà le funzioni).
  • “Layout” serve per specificare dove e quando appaiono nell’interfaccia utente (e anche per definire le scorciatoie).
  • “Traduzioni” serve per aggiungere traduzioni alle definizioni dei pulsanti.
1 Mi Piace

Ooooh sì, certo, mi sono completamente perso questa impostazione! Darò un’occhiata più approfondita, potrebbe soddisfare le mie esigenze :handshake:

Fatico a far funzionare la scorciatoia da tastiera.

image

Il pulsante si trova nella barra degli strumenti e funziona, ma Ctrl+Shift+k non fa nulla. Hai qualche idea?

1 Mi Piace

Ooooh, scusa! Ho commesso un errore nella documentazione per le impostazioni del layout: gli specificatori delle scorciatoie devono usare il segno più, non il trattino.

Ad esempio: shift+k (non shift-k).

(Agiornerò la documentazione della pagina delle impostazioni nei prossimi giorni.)

1 Mi Piace