decorateCookedElement per ogni cambio pagina?

Sto cercando di utilizzare la libreria Mediaelement.js per sovrascrivere il custom audio onebox e ci sono riuscito quando la pagina viene caricata/aggiornata, tuttavia, quando navigo da un argomento all’altro, non sembra ricaricarsi, mostrando invece il Discourse audio onebox nativo.

Quando carico/aggiorno:

Se poi navigo verso un altro argomento:

Sono abbastanza sicuro di star facendo qualcosa di sbagliato con il caricamento di mejs ma ho pensato che forse non fosse così e che dovessi fare qualcosa di speciale con onPageChange o qualcos’altro.

<html>

<script type="text/discourse-plugin" version="0.8.42">

let loadScript = require('discourse/lib/load-script').default;

    api.decorateCookedElement(
      element => {
        const audioplayers = element.querySelectorAll('audio');
        // console.log("player: " + audioplayers[0]);
        if (Object.entries(audioplayers).length > 0) {
            // console.log("audioplayers has length");
            loadScript(
                `https://cdnjs.cloudflare.com/ajax/libs/mediaelement/5.0.4/mediaelement.min.js`
            );
            loadScript(
                `https://cdnjs.cloudflare.com/ajax/libs/mediaelement/5.0.4/mediaelement-and-player.min.js`
            );
        }

        audioplayers.forEach(function (el) {
            el.classList.add("mejs__player");
            const controls = settings.theme_uploads.mejs-controls;
            // console.log("controls: " + controls);
            el.setAttribute("data-mejsoptions",
                `{
                    "pluginPath": "https://cdnjs.cloudflare.com/ajax/libs/mediaelement/5.0.4/",
                    "iconSprite": "https://friends.jimkleiber.com/uploads/default/original/1X/a17d9708a19654d9155dd9b79a79a05dea580067.svg",
                    "alwaysShowControls": "true",
                    "features": ["playpause", "current", "progress", "duration", "volume"]
                }`
            );
            el.setAttribute("preload", "auto");            
        });            
      },
      { id: "mediaelement-js", onlyStream: true}
    );
</script>

css

@import "https://cdnjs.cloudflare.com/ajax/libs/mediaelement/5.0.4/mediaelementplayer.min.css";

Qualcuno ha qualche suggerimento su come potrei modificarlo in modo che si ricarichi automaticamente ogni volta che viene caricata una pagina?

Questo sarà un post leggermente lungo dato che hai pubblicato in Development. Tuttavia, è molto generale e riguarda più la spiegazione dei pattern da utilizzare in Discourse, indipendentemente dalla libreria che desideri integrare o dagli elementi del post che vuoi prendere di mira. Succede solo che usa lo script che hai scelto come esempio principale.

Quindi, MediaElement offre diversi modi per inizializzare un nuovo lettore audio.

  1. Puoi aggiungere una classe CSS e alcuni attributi all’elemento, e lo script lo gestirà automaticamente per te.

  2. Puoi inizializzarlo manualmente.

Attualmente stai usando l’opzione 1, quindi esaminiamola per un po’. Quando uno script offre un’inizializzazione automatica su un elemento, di solito è un miglioramento della qualità della vita aggiunto dall’autore dello script. Nello script, di solito ascoltano l’evento di caricamento del documento e eseguono alcune operazioni sugli elementi DOM che hanno la classe o gli attributi che ti dicono di aggiungere.

Ok, allora perché non funziona? Come hai visto, funziona perfettamente al caricamento iniziale della pagina, ma non durante la navigazione successiva all’interno dell’app. Cosa succede?

La risposta breve è che Discourse è un’applicazione a pagina singola. Elementi come i tag <HTML> e <body> vengono inviati una sola volta. Quindi, in un certo senso, il documento viene caricato una sola volta. Pertanto, quando navighi dopo il caricamento iniziale della pagina, non ci sono più eventi “load” nativi che vengono inviati. Ricorda, il documento è già stato caricato alla visualizzazione iniziale della pagina. Tutto ciò che accade dopo è gestito da Discourse.

Certo, questo non significa che non ci siano eventi che si attivano durante la navigazione successiva. Tuttavia, questi sono eventi specifici di Discourse. Quindi, gli autori di script di terze parti non avrebbero modo di conoscerli in anticipo. Immagina di essere un autore di script e dover adattarti a centinaia di piattaforme diverse? Non va bene, vero?

Quindi, non possiamo utilizzare il metodo di miglioramento della qualità della vita che l’autore dello script ha aggiunto così gentilmente. Cosa facciamo ora? Bene, ricorda che possiamo ancora inizializzare manualmente lo script sugli elementi di destinazione. Quindi, proviamo a farlo.

Prima ho menzionato che c’è un solo evento di caricamento nativo (a livello di browser), ma una piattaforma come Discourse non funzionerebbe bene senza il proprio sistema di eventi. Ad esempio, l’API dei plugin ha un metodo che consente di eseguire script durante la navigazione virtuale delle pagine.

Dovresti usare quel metodo? No. Quel metodo è molto utile per cose come l’analisi statistica e così via. Non ha senso eseguire uno script che gestisce solo i tag <audio> su ogni pagina, specialmente se la pagina non ne ha nessuno.

Quindi, cosa facciamo ora? Bene, la buona notizia è che l’hai già capito. decorateCookedElement è il metodo corretto da utilizzare qui.

Ti offre un modo per… aspetta… decorare i post :tada:

Discourse garantisce che qualsiasi decoratore tu aggiunga verrà applicato a ogni post.

Bene, stai caricando lo script in un decoratore di post, quindi dovrebbe essere aggiunto e dovrebbe funzionare. Perché non funziona durante la navigazione successiva?

Per questo, devi capire come funziona loadScript(). Il tuo codice controlla già se ci sono elementi di destinazione validi prima di caricare lo script, quindi :+1:

Tuttavia, immagina una situazione in cui hai 20-30 post consecutivi in cui tutti hanno elementi validi. Avrebbe senso caricare lo script 20-30 volte? Ovviamente no.

loadScript() è abbastanza intelligente da rilevare se lo script è già stato caricato. Non caricherà duplicati e non ricaricherà uno script se è già stato scaricato. Puoi vederlo qui.

fullUrl sopra è l’URL che passi a loadScript() quando lo chiami, proprio come nel tuo esempio.

Quindi, ora che lo sappiamo, possiamo capire perché non funziona durante la navigazione successiva.

Visiti topic-a > ha un elemento audio > loadScript() carica lo script > lo script fa la cosa "auto init" sofisticata > lo script si inizializza sui tuoi elementi > ottieni elementi audio personalizzati

poi...

visiti topic-b > ha elementi audio > loadScript() vede che lo script è già caricato > niente "auto init" sofisticato > ottieni gli elementi audio predefiniti > arriva la tristezza

Quindi, come risolvi questo problema? Bene, è per questo che serve l’opzione 2 di prima.

  1. Puoi aggiungere una classe CSS e alcuni attributi all’elemento, e lo script lo gestirà automaticamente per te.

  2. Puoi inizializzarlo manualmente.

Quindi, facciamolo. È già documentato sulla pagina che hai condiviso. Dobbiamo chiamarlo sul nostro elemento di destinazione in questo modo:

// Puoi usare sia una stringa per l'ID del player (cioè `player`),
// sia `document.querySelector()` per qualsiasi selettore
var player = new MediaElementPlayer("player", {
  // ... opzioni
});

Il tuo codice gestisce già ogni singolo elemento audio separatamente :+1:, quindi dobbiamo solo modificare questo:

audioplayers.forEach(function (el) {
  el.classList.add("mejs__player");
  const controls = settings.theme_uploads.mejs - controls;
  // console.log("controls: " + controls);
  el.setAttribute(
    "data-mejsoptions",
    `{
                    "pluginPath": "https://cdnjs.cloudflare.com/ajax/libs/mediaelement/5.0.4/",
                    "iconSprite": "https://friends.jimkleiber.com/uploads/default/original/1X/a17d9708a19654d9155dd9b79a79a05dea580067.svg",
                    "alwaysShowControls": "true",
                    "features": ["playpause", "current", "progress", "duration", "volume"]
                }`
  );
  el.setAttribute("preload", "auto");
});

in questo:

audioplayers.forEach(function (el) {
-- el.classList.add("mejs__player");
-- const controls = settings.theme_uploads.mejs - controls;
-- // console.log("controls: " + controls);
-- el.setAttribute(
--   "data-mejsoptions",
--   `{
--                     "pluginPath": "https://cdnjs.cloudflare.com/ajax/libs/mediaelement/5.0.4/",
--                     "iconSprite": "https://friends.jimkleiber.com/uploads/default/original/1X/a17d9708a19654d9155dd9b79a79a05dea580067.svg",
--                     "alwaysShowControls": "true",
--                     "features": ["playpause", "current", "progress", "duration", "volume"]
--                 }`
-- );
-- el.setAttribute("preload", "auto");
++ 
++ new MediaElementPlayer(el, {
++  // ... opzioni
++ });
});

e poi prendiamo le opzioni che usavamo per aggiungere come attributo e le passiamo come oggetto in questo modo:

audioplayers.forEach(function (el) {
 new MediaElement(el, {
++ pluginPath: "//cdnjs.cloudflare.com/ajax/libs/mediaelement/5.0.4/",
++ iconSprite: settings.theme_uploads["mejs-controls"],
++ alwaysShowControls: "true",
++ features: ["playpause", "current", "progress", "duration", "volume"]
 });
});

Mettiamo questo da parte per ora e guardiamo il resto del decoratore. Ecco cosa abbiamo finora:

let loadScript = require("discourse/lib/load-script").default;

api.decorateCookedElement(
  element => {
    const audioplayers = element.querySelectorAll("audio");
    // console.log("player: " + audioplayers[0]);
    if (Object.entries(audioplayers).length > 0) {
      // console.log("audioplayers has length");
      loadScript(
        `https://cdnjs.cloudflare.com/ajax/libs/mediaelement/5.0.4/mediaelement.min.js`
      );
      loadScript(
        `https://cdnjs.cloudflare.com/ajax/libs/mediaelement/5.0.4/mediaelement-and-player.min.js`
      );
    }

    // forEach usato per essere qui
  },
  { id: "mediaelement-js", onlyStream: true }
);

Se noti, stai chiamando loadScript() su due script diversi. Non sono sicuro che sia intenzionale, ma ti serve solo uno di quelli. Pensaci come un bundle completo e uno leggero. Vuoi il lettore audio personalizzato. Quindi, hai bisogno del bundle completo. Rimuoviamo l’altro.

let loadScript = require("discourse/lib/load-script").default;

api.decorateCookedElement(
  element => {
    const audioplayers = element.querySelectorAll("audio");
    // console.log("player: " + audioplayers[0]);
    if (Object.entries(audioplayers).length > 0) {
      // console.log("audioplayers has length");
--    loadScript(
--      `https://cdnjs.cloudflare.com/ajax/libs/mediaelement/5.0.4/mediaelement.min.js`
--    );
      loadScript(
        `https://cdnjs.cloudflare.com/ajax/libs/mediaelement/5.0.4/mediaelement-and-player.min.js`
      );
    }

    // forEach usato per essere qui
  },
  { id: "mediaelement-js", onlyStream: true }
);

Stai controllando se ci sono lettori audio nel post e carichi condizionalmente lo script in base a questo. Questo può essere semplificato così. Prima, controlla direttamente la lunghezza.

let loadScript = require("discourse/lib/load-script").default;

api.decorateCookedElement(
  element => {
    const audioplayers = element.querySelectorAll("audio");
    // console.log("player: " + audioplayers[0]);
--    if (Object.entries(audioplayers).length > 0) {
++    if (audioplayers.length) {
      // console.log("audioplayers has length");

      loadScript(
        `https://cdnjs.cloudflare.com/ajax/libs/mediaelement/5.0.4/mediaelement-and-player.min.js`
      );
    }

    // forEach usato per essere qui
  },
  { id: "mediaelement-js", onlyStream: true }
);

poi spostalo in cima e usa return se la lunghezza è falsa (length < 0). Ho anche rimosso i commenti nel codice.

let loadScript = require("discourse/lib/load-script").default;

api.decorateCookedElement(
  element => {
    const audioplayers = element.querySelectorAll("audio");

++  if (!audioplayers.length) {
++   return;
++  }

    loadScript(
      `https://cdnjs.cloudflare.com/ajax/libs/mediaelement/5.0.4/mediaelement-and-player.min.js`
    );

    // forEach usato per essere qui
  },
  { id: "mediaelement-js", onlyStream: true }
);

Dato che il src dello script non cambierà mai, spostiamolo in una const. loadScript() è sempre lo stesso. Rendiamolo una const anche questo.

++ const loadScript = require("discourse/lib/load-script").default;

++ const MEDIA_ELEMENT_SRC =
++  "https://cdnjs.cloudflare.com/ajax/libs/mediaelement/5.0.4/mediaelement-and-player.min.js";

api.decorateCookedElement(
  element => {
    const audioplayers = element.querySelectorAll("audio");

    if (!audioplayers.length) {
      return;
    }

++  loadScript(MEDIA_ELEMENT_SRC);

    // forEach usato per essere qui
  },
  { id: "mediaelement-js", onlyStream: true }
);

e mettiamo anche questo da parte. Prima di continuare, dobbiamo parlare di come funziona loadScript() per un po’ di più.

Se vuoi usare una parte di uno script, vuoi assicurarti che sia caricato prima di fare qualsiasi lavoro, no? Bene, loadScript() si occupa di questo per te. Restituisce una Promise. Le Promise sembrano spaventose all’inizio, ma sono davvero semplici. Una Promise è letteralmente questo… una promessa.

Vuoi fare un lavoro… prometti al browser che gli farai sapere quando il lavoro è finito… il browser aspetta. È davvero semplice così. Il resto è solo capire la sintassi.

Non mi dilungherò molto su questo perché è un po’ fuori ambito per questo argomento.

Continuiamo. loadScript() è basato su Promise. Discourse promette al browser di farlo sapere quando lo script è completamente caricato, sia che lo script non esista e debba essere caricato, sia semplicemente per verificare se è già stato caricato.

Quindi, se facciamo qualcosa come questo:

const loadScript = require("discourse/lib/load-script").default;

const MEDIA_ELEMENT_SRC =
  "https://cdnjs.cloudflare.com/ajax/libs/mediaelement/5.0.4/mediaelement-and-player.min.js";

api.decorateCookedElement(
  element => {
    const audioplayers = element.querySelectorAll("audio");

    if (!audioplayers.length) {
      return;
    }

++  loadScript(MEDIA_ELEMENT_SRC).then(() => {
++    // questo si attiverà SOLO se lo script è stato/è stato caricato
++    console.log("my script has loaded");
++  });

    // forEach usato per essere qui
  },
  { id: "mediaelement-js", onlyStream: true }
);

Quindi, ora possiamo tornare al nostro ciclo forEach di prima e aggiungerlo proprio lì, e sapremmo con certezza che lo script sarà disponibile.

const loadScript = require("discourse/lib/load-script").default;

const MEDIA_ELEMENT_SRC =
  "https://cdnjs.cloudflare.com/ajax/libs/mediaelement/5.0.4/mediaelement-and-player.min.js";

api.decorateCookedElement(
  element => {
    const audioplayers = element.querySelectorAll("audio");

    if (!audioplayers.length) {
      return;
    }

    loadScript(MEDIA_ELEMENT_SRC).then(() => {
++    audioplayers.forEach(function (el) {
++      new MediaElementPlayer(el, {
++        pluginPath: "//cdnjs.cloudflare.com/ajax/libs/mediaelement/5.0.4/",
++        iconSprite: settings.theme_uploads["mejs-controls"],
++        alwaysShowControls: "true",
++        features: ["playpause", "current", "progress", "duration", "volume"]
++      });
++    });
    });
  },
  { id: "mediaelement-js", onlyStream: true }
);

Ora, guarda il frammento sopra e vedi se c’è qualcosa che ti disturba…

Le opzioni che passiamo all’istanza dello script sono sempre le stesse, ma non le abbiamo in una const. Sistemiamolo.

    const loadScript = require("discourse/lib/load-script").default;
    
    const MEDIA_ELEMENT_SRC =
      "https://cdnjs.cloudflare.com/ajax/libs/mediaelement/5.0.4/mediaelement-and-player.min.js";
    
++  const MEDIA_ELEMENT_CONFIG = {
++    pluginPath: "//cdnjs.cloudflare.com/ajax/libs/mediaelement/5.0.4/",
++    iconSprite: settings.theme_uploads["mejs-controls"],
++    alwaysShowControls: "true",
++    features: ["playpause", "current", "progress", "duration", "volume"]
++  };
    
    api.decorateCookedElement(
      element => {
        const audioplayers = element.querySelectorAll("audio");
    
        if (!audioplayers.length) {
          return;
        }
    
        loadScript(MEDIA_ELEMENT_SRC).then(() => {
          audioplayers.forEach(function (el) {
++          new MediaElementPlayer(el, MEDIA_ELEMENT_CONFIG);
          });
        });
      },
      { id: "mediaelement-js", onlyStream: true }
    );

e questo è tutto. Il tuo codice era molto vicino; avevi solo bisogno di capire meglio loadScript().

Ora, mettiamo tutto insieme.

Scheda common > header

<script type="text/discourse-plugin" version="0.8.42">
  const loadScript = require("discourse/lib/load-script").default;

  const MEDIA_ELEMENT_SRC =
    "https://cdnjs.cloudflare.com/ajax/libs/mediaelement/5.0.4/mediaelement-and-player.min.js";

  const MEDIA_ELEMENT_CONFIG = {
    pluginPath: "//cdnjs.cloudflare.com/ajax/libs/mediaelement/5.0.4/",
    iconSprite: settings.theme_uploads["mejs-controls"],
    alwaysShowControls: "true",
    features: ["playpause", "current", "progress", "duration", "volume"]
  };

  api.decorateCookedElement(
    element => {
      const audioplayers = element.querySelectorAll("audio");

      if (!audioplayers.length) {
        return;
      }

      loadScript(MEDIA_ELEMENT_SRC).then(() => {
        audioplayers.forEach(function (el) {
          new MediaElementPlayer(el, MEDIA_ELEMENT_CONFIG);
        });
      });
    },
    { id: "mediaelement-js", onlyStream: true }
  );
</script>

e poi un po’ di CSS per caricare il CSS dello script e prevenire il jitter mentre lo script sostituisce gli elementi.

common > css

@import "https://cdnjs.cloudflare.com/ajax/libs/mediaelement/5.0.4/mediaelementplayer.min.css";

.cooked {
  --audio-player-height: 40px;

  .mejs__container,
  audio {
    // allinea l'altezza del player Media-Element.js per prevenire il jitter
    height: var(--audio-player-height);
    display: block;
  }
}

Ora dovresti vedere il lettore personalizzato su ogni post che ha elementi validi.

Con questo chiarito, dovresti notare che la libreria che hai scelto è piuttosto vecchia. È transpilata per browser antichi e cerca di polyfillare molte funzionalità che sono diventate standard da allora.

Se sai perché vuoi usarla, va bene. Tuttavia, se la stai usando solo per personalizzare l’aspetto del lettore, ti consiglio di evitarla. Non ho controllato, ma probabilmente ci sono alternative moderne molto più leggere.

La cosa migliore di tutto questo è che l’implementazione non cambia rispetto a quanto sopra. Indipendentemente dagli elementi che vuoi prendere di mira e dagli script che vuoi usare. Lo stesso pattern si applica. L’unica cosa che cambia è l’inizializzazione dello script personalizzato. Ogni libreria decente ha una documentazione piuttosto buona che ti guiderà attraverso questo. Poi, devi solo inserirlo nel pattern sopra.

È in momenti come questi che vorrei che Meta utilizzasse Discourse Reactions, perché un cuore non sembra sufficiente per l’amore e la pura gratitudine che provo in questo momento.

Speravo che qualcuno mi desse almeno un consiglio e invece ti sei preso il tempo di scrivere una spiegazione molto approfondita di come sono arrivato dove sono e mi hai guidato su come farlo funzionare, insegnandomi molto lungo la strada (anche con i piccoli dettagli sul jitter!)

Sto imparando come la struttura di Discourse possa aiutare a incoraggiare tale comportamento, nel senso che se rispondo bene una volta, gli altri possono vederlo e non dovrò rispondere di nuovo, il che mi incoraggia a continuare a costruire community con questo; eppure, non credo che spieghi completamente perché mi hai scritto questo e la tua disponibilità a farlo potrebbe incoraggiarmi a utilizzare questa piattaforma ancora di più.

Grazie.


Per quanto riguarda Mediaelement, sì, è vecchio, ma si abbina bene al sito WordPress che ho e l’ho personalizzato molto lì, cercando di fornire un aspetto familiare all’utente (e anche non cercando di imparare un’altra libreria al momento :smiley:)

Un follow-up e forse una domanda stupida: sto cercando di caricare più script ora, poiché Mediaelement consente script plugin. Voglio assicurarmi che tutti gli script vengano caricati prima di restituire la promise.

Ho provato a farlo scorrendo le costanti delle sorgenti degli script e quindi creando un array di promise, quindi utilizzando Promise.all() per l’inizializzazione dei player, e tuttavia quando lo faccio, ricevo un errore che dice che mejs non viene trovato, che credo sia il namespace o qualcosa per chiamare diverse funzioni all’interno di mediaelement-and-player.

      let scripts_loaded = [];

      MEDIA_ELEMENT_SCRIPTS.forEach(function (src, index){
          scripts_loaded[index] = loadScript(src);
      });

      Promise.all(scripts_loaded).then(() => {
            audioplayers.forEach(function (el) {
              new MediaElementPlayer(el, MEDIA_ELEMENT_CONFIG);
            });
      });

Tuttavia, se li concateno manualmente, sembra funzionare, così:

      loadScript(MEDIA_ELEMENT_SRC).then(() => {
          loadScript(MEDIA_ELEMENT_SPEED_SRC).then(() => {
            loadScript(MEDIA_ELEMENT_SKIP_BACK_SRC).then(() => {
                loadScript(MEDIA_ELEMENT_JUMP_FORWARD_SRC).then(() => {
                    audioplayers.forEach(function (el) {
                        new MediaElementPlayer(el, MEDIA_ELEMENT_CONFIG);
                    });
                });
            });
        });
      });

In questo caso, sto usando solo alcuni script, quindi li ho digitati tutti manualmente, ero solo curioso se mi manca qualcosa di ovvio sulla funzione Promise.all(), o se esiste una funzione Discourse che mi consente di caricare più script da un array.

Il tuo codice dovrebbe funzionare correttamente. Ti sei appena imbattuto in un bug in Discourse.

loadScript() non imposta l’attributo async per gli script che carica. Quindi, per impostazione predefinita, è async="true" e questo scombina il tuo ordine di caricamento. È una stranezza del browser. Devi forzare async="false" per gli script caricati con JS.

I plugin sono più piccoli, quindi si caricano più velocemente del bundle principale, ma essendo async non rispettano più l’ordine di caricamento: attendono che il bundle principale venga caricato ed eseguito prima di eseguirsi.

Probabilmente è passato inosservato perché loadScript non è annidato da nessuna parte nel core, per quanto ne so. Normalmente raggrupperesti file che devono lavorare insieme. Quindi, per rispondere alla tua altra domanda. No, non ci sono funzioni di Discourse che gestiscono quel tipo di cose.

Anche il tuo altro snippet dovrebbe funzionare. Per renderlo un po’ più facile da leggere, forse prova a concatenarli senza annidarli.

// Questo va al di fuori del decoratore
const PLUGINS = {
  speed: "https://example.com/foo.js",
  skipBack: "https://example.com/bar.js",
  jumpForward: "https://example.com/baz.js"
};

// Quindi fai il tuo lavoro all'interno del decoratore
loadScript(MEDIA_ELEMENT_SRC)
  .then(() => loadScript(PLUGINS.speed))
  .then(() => loadScript(PLUGINS.skipBack))
  .then(() => loadScript(PLUGINS.jumpForward))
  .then(() => {
    audioplayers.forEach(function (el) {
      new MediaElementPlayer(el, MEDIA_ELEMENT_CONFIG);
    });
  });

Oh cielo, grazie per avermelo fatto sapere!

Ok, va bene.

Wow, apprezzo moltissimo questo. Molto, molto più pulito. Grazie ancora :pray: