Este será un post ligeramente largo, ya que publicaste en Development. Sin embargo, es muy general y se trata más de explicar los patrones a utilizar en Discourse, independientemente de la biblioteca que quieras integrar o de los elementos del post que desees apuntar. Solo que resulta que utiliza el script que elegiste como ejemplo principal.
Así que, MediaElement te ofrece varias formas de inicializar un nuevo reproductor de audio.
-
Puedes agregar una clase CSS y algunos atributos al elemento, y luego lo manejará automáticamente por ti.
-
Lo inicializas manualmente.
Actualmente estás usando la opción 1, así que veamos eso por un momento. Cuando un script te ofrece “inicialización automática en un elemento”, generalmente es una mejora de calidad de vida que el autor del script añade. En el script, normalmente escuchan el evento de carga del documento y realizan algunas tareas en los elementos del DOM que tienen la clase o los atributos que te indican que agregues.
Bien, ¿por qué no está funcionando? Como viste, funciona perfectamente en la carga inicial de la página, pero no en la navegación posterior dentro de la aplicación. ¿Qué sucede?
La respuesta corta es que Discourse es una aplicación de una sola página (SPA). Elementos como las etiquetas <HTML> y <body> se envían una sola vez. En cierto sentido, el documento solo se carga una vez. Por lo tanto, cuando navegas después de que la página inicial se ha cargado, ya no se envían más eventos nativos de “carga”. Recuerda, el documento ya se cargó en la vista inicial de la página. Todo lo que sucede después de eso es manejado por Discourse.
Por supuesto, eso no significa que no haya eventos que se disparen en la navegación posterior. Sin embargo, esos son eventos específicos de Discourse. Por lo tanto, los autores de scripts de terceros no tendrían forma de conocerlos con anticipación. Imagina ser un autor de scripts y tener que adaptarte a cientos de plataformas diferentes. No es bueno, ¿verdad?
Así que no podemos usar la forma de calidad de vida que el autor del script añadió tan amablemente. ¿Qué sigue? Bueno, recuerda que aún podemos inicializar manualmente el script en los elementos objetivo. Así que intentemos hacerlo.
Antes mencioné que solo hay un evento de carga nativo (a nivel de navegador), pero una plataforma como Discourse no funcionaría bien si no tuviera su propio sistema de eventos. Por ejemplo, la API de plugins tiene un método que te permite ejecutar scripts en la navegación virtual de páginas.
¿Deberías usar ese método? No. Ese método es muy útil para cosas como análisis, etc. No tiene sentido ejecutar un script que solo maneja etiquetas <audio> en cada página, especialmente si la página no tiene ninguna de ellas.
Entonces, ¿qué sigue? Bueno, la buena noticia es que ya lo has descubierto. decorateCookedElement es el método correcto a utilizar aquí.
Te ofrece una forma de… espera por ello… decorar publicaciones ![]()
Discourse garantiza que cualquier decorador que agregues se aplicará a cada publicación.
Bueno, estás cargando el script en un decorador de publicaciones, por lo que debería agregarse y debería funcionar. ¿Por qué no funciona en la navegación posterior?
Para esto, tienes que entender cómo funciona loadScript(). Tu código ya verifica si hay elementos objetivo válidos antes de cargar el script, así que ![]()
Sin embargo, imagina una situación en la que tienes 20 o 30 publicaciones seguidas que todas tienen elementos válidos. ¿Tendría sentido cargar el script 20 o 30 veces? Obviamente no.
loadScript() es lo suficientemente inteligente como para detectar si el script ya ha sido cargado. No cargará duplicados y tampoco recargará un script si ya ha terminado de descargarse. Puedes ver eso aquí.
fullUrl arriba es la URL que pasas a loadScript() cuando la llamas, igual que en tu ejemplo.
Así que, ahora que sabemos esto, podemos ver por qué no funciona en la navegación posterior.
Visitas el tema-a > tiene un elemento de audio > loadScript() carga el script > el script hace la cosa elegante de "inicialización automática" > el script se inicializa en tus elementos > obtienes elementos de audio personalizados
luego...
visitas el tema-b > tiene elementos de audio > loadScript() ve que el script ya está cargado > no hay "inicialización automática" elegante > obtienes los elementos de audio predeterminados > sobreviene la tristeza
Entonces, ¿cómo solucionas esto? Bueno, para eso sirve la opción 2 de antes.
Puedes agregar una clase CSS y algunos atributos al elemento, y luego lo manejará automáticamente por ti.
Lo inicializas manualmente.
Así que hagámoslo. Ya está documentado en la página que compartiste. Necesitamos llamar a esto en nuestro elemento objetivo de la siguiente manera:
// Puedes usar una cadena para el ID del reproductor (es decir, `player`),
// o `document.querySelector()` para cualquier selector
var player = new MediaElementPlayer("player", {
// ... opciones
});
Tu código ya maneja cada elemento de audio individualmente por separado
, así que solo necesitamos modificar esto:
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");
});
a esto:
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, {
++ // ... opciones
++ });
});
y luego tomamos las opciones que solíamos agregar como un atributo y las pasamos como un objeto de la siguiente manera:
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"]
});
});
Dejemos esto en espera por ahora y veamos el resto del decorador. Esto es lo que tenemos hasta ahora:
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 used to be here
},
{ id: "mediaelement-js", onlyStream: true }
);
Si te fijas, estás llamando a loadScript() en dos scripts diferentes. No estoy seguro si esto es intencional, pero solo necesitas uno de ellos. Piénsalo como un paquete completo y uno ligero. Quieres el reproductor de audio personalizado. Por lo tanto, necesitas el paquete completo. Eliminemos el otro.
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 used to be here
},
{ id: "mediaelement-js", onlyStream: true }
);
Estás verificando si hay reproductores de audio en la publicación y cargas el script condicionalmente basado en eso. Esto se puede simplificar de la siguiente manera. Primero, verifica la longitud directamente.
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 used to be here
},
{ id: "mediaelement-js", onlyStream: true }
);
luego muévelo a la parte superior y simplemente return si la longitud es falsa (longitud < 0). También eliminé los comentarios del código.
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 used to be here
},
{ id: "mediaelement-js", onlyStream: true }
);
Dado que el src del script nunca cambiará, movámoslo a una const. loadScript() también es siempre el mismo. Hagámoslo una const también.
++ 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 used to be here
},
{ id: "mediaelement-js", onlyStream: true }
);
y dejemos esto en espera también. Antes de continuar, necesitamos hablar un poco más sobre cómo funciona loadScript().
Si quieres usar alguna parte de un script, quieres asegurarte de que esté cargado antes de hacer cualquier trabajo, ¿verdad? Bueno, loadScript() se encarga de eso por ti. Devuelve una Promesa. Las promesas suenan aterradoras al principio, pero son realmente simples. Una promesa es literalmente eso… una promesa.
Quieres hacer algún trabajo… prometes al navegador que le avisarás cuando el trabajo esté terminado… el navegador espera por ti. Es tan simple como eso. El resto es solo entender la sintaxis.
No me extenderé mucho en eso porque está un poco fuera del alcance de este tema.
Continuemos. loadScript() está basado en promesas. Discourse le promete al navegador que le avisará cuando el script se haya cargado completamente, ya sea que el script no exista y tenga que cargarse o simplemente para verificar si ya se cargó.
Así que, si hacemos algo como esto:
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(() => {
++ // esto SOLO se disparará si el script se ha cargado o está cargando
++ console.log("mi script se ha cargado");
++ });
// forEach used to be here
},
{ id: "mediaelement-js", onlyStream: true }
);
Ahora podemos volver a nuestro bucle forEach de antes y agregarlo justo allí, y sabremos con certeza que el script estará disponible.
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 }
);
Ahora, mira el fragmento de arriba y ve si algo te molesta…
Las opciones que pasamos a la instancia del script son siempre las mismas, pero no las tenemos en una const. Arreglemos eso.
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 }
);
y eso es todo. Estuviste muy cerca en tu código; solo necesitabas entender un poco más loadScript().
Ahora, juntamos todo eso.
Pestaña 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>
y luego un poco de CSS para cargar el CSS del script y evitar el parpadeo mientras el script cambia los elementos.
common > css
@import "https://cdnjs.cloudflare.com/ajax/libs/mediaelement/5.0.4/mediaelementplayer.min.css";
.cooked {
--audio-player-height: 40px;
.mejs__container,
audio {
// igualar la altura del reproductor de Media-Element.js para evitar el parpadeo
height: var(--audio-player-height);
display: block;
}
}
Ahora deberías ver el reproductor personalizado en cada publicación que tenga elementos válidos.
Con eso fuera del camino, debes notar que la biblioteca que elegiste es bastante antigua. Está transpilada para navegadores antiguos y trata de polifill muchas características que desde entonces se han convertido en estándar.
Si sabes por qué quieres usarla, está bien. Sin embargo, si solo la estás usando para personalizar la apariencia del reproductor, te recomiendo que la evites. No lo he verificado, pero probablemente haya alternativas modernas mucho más ligeras.
Lo mejor de todo esto es que la implementación no cambia de lo anterior. No importa qué elementos quieras apuntar y qué scripts quieras usar. El mismo patrón se aplica. Lo único que cambia es la inicialización del script personalizado. Cada biblioteca decente tiene una documentación bastante buena que te guiará a través de eso. Luego, solo tienes que poner eso en el patrón anterior.