CSS

Cómo crear una extensión del navegador trucos CSS

Apuesto a que actualmente estás usando una extensión de navegador. Algunos de ellos son muy populares y útiles, como bloqueadores de anuncios, administradores de contraseñas y revisiones de PDF. Estas extensiones (o «complementos») no se limitan a estos objetivos: ¡puede hacer mucho más con ellas! En este artículo te mostraré cómo crear uno. Eventualmente haremos que funcione en múltiples navegadores.

Que estamos haciendo

hacemos un «Escriba de Reddit» Esto mejorará la accesibilidad de Reddit al mover comentarios específicos a la parte superior de la sección de comentarios y agregar aria- Atributos Para lectores de pantalla. También ampliaremos aún más nuestra extensión con la opción de agregar bordes y fondos a los comentarios para mejorar el contraste del texto.

La idea general es que obtendrá una buena introducción sobre cómo desarrollar extensiones de navegador. Comenzaremos creando extensiones para navegadores basados ​​en Chromium como Google Chrome, Microsoft Edge, Brave, etc. En un próximo artículo moveremos la extensión para que funcione con Firefox y Safari. Soporte agregado recientemente para extensiones web En versiones de navegador para MacOS e iOS.

¿prepararse? Vamos paso a paso.

Crear un directorio de trabajo

Primero, necesitamos un espacio de trabajo para nuestro proyecto. Lo que realmente necesitamos es crear una carpeta y darle un nombre (lo llamaré transcribers-of-reddit) Luego crea otra carpeta en esa carpeta src para nuestro código fuente.

definir el punto de entrada

El punto de entrada es un archivo que contiene información general sobre la extensión (es decir, nombre de extensión, descripción, etc.) y define los permisos o scripts para ejecutar.

Nuestro punto de entrada puede ser manifest.json presentar en src La carpeta que acabamos de crear. Agreguemos las siguientes tres propiedades:

{
  "manifest_version": 3,
  "name": "Transcribers of Reddit",
  "version": "1.0"
}

Esta manifest_version Similar a las ediciones en npm o Node. Determina qué API están disponibles (o no disponibles). Trabajaremos al borde del sangrado y utilizaremos la última versión 3 (también conocida como mv3).

La segunda propiedad es name Define nuestra extensión, este nombre es el nombre que se mostrará donde aparezca nuestra extensión, como por ejemplo Tienda virtual de Chrome y chrome://extensions Página cromada.

entonces hay versionMarca la extensión con el número de versión, recuerda que esta propiedad (con manifest_version) es una cadena que solo puede contener números y puntos (por ejemplo, 1.3.5).

Más ▼ manifest.json información

De hecho, podemos agregar más contenido para ayudar a agregar contexto a nuestra extensión. Por ejemplo, podemos proporcionar un description Esto explica lo que hace la extensión. Es una buena idea proporcionar estas cosas porque les da a los usuarios una mejor idea de lo que encontrarán cuando lo usen.

En este caso, hemos agregado no solo una descripción, sino también el ícono y la URL a la que apunta Chrome Web Store en la página de la extensión.

{
  "description": "Reddit made accessible for disabled users.",
  "icons": {
    "16": "images/logo/16.png",
    "48": "images/logo/48.png",
    "128": "images/logo/128.png"
  },
  "homepage_url": "https://lars.koelker.dev/extensions/tor/"
}
  • Esta description aparece en la página de administración de Chrome (chrome://extensions) y debe ser corto, menos de 132 caracteres.
  • Esta icons Usado en muchos lugares Documentación estado, preferiblemente proporciona tres versiones del mismo ícono en diferentes resoluciones, preferiblemente como archivos PNG. Siéntase libre de usar repositorio GitHub para este ejemplo.
  • Esta homepage_url Se puede utilizar para conectar su sitio web con extensiones. Cuando haga clic en «Más detalles» en la página de administración, aparecerá un botón de enlace.
Hemos abierto el mapa de extensiones en la página Gestión de extensiones.

Establecer permisos

La principal ventaja de las extensiones es que su API te permite interactuar directamente con el navegador, pero debemos dar explícitamente estos permisos a la extensión, que también está incluido en manifest.json documento.


{
  "manifest_version": 3,
  "name": "Transcribers of Reddit",
  "version": "1.0",
  "description": "Reddit made accessible for disabled users.",
  "icons": {
    "16": "images/logo/16.png",
    "48": "images/logo/48.png",
    "128": "images/logo/128.png"
  },
  "homepage_url": "https://lars.koelker.dev/extensions/tor/",

  "permissions": [
    "storage",
    "webNavigation"
  ]
}

¿Qué acabamos de dar a esta extensión de permiso? Primero, almacenamiento. Queremos que esta extensión pueda guardar la configuración del usuario, por lo que debemos acceder al almacenamiento web del navegador para guardarla. Por ejemplo, si un usuario quiere un marco rojo en un comentario, lo guardaremos para usarlo en el futuro en lugar de configurarlo nuevamente.

También proporcionamos permiso de extensión para ver cómo navega el usuario a la pantalla actual. Reddit es una aplicación de una página (SPA), lo que significa que no activa una actualización de página. Necesitamos «atrapar» esta interacción, porque Reddit solo cargará comentarios cuando hagamos clic en una publicación. webNavigation.

Ejecutaremos el código en la página más tarde porque requiere una entrada completamente nueva manifest.json.

/ explicación Dependiendo de los permisos permitidos, el navegador puede mostrar una advertencia al usuario para que acepte los permisos. Sin embargo, esto solo es seguro y Chrome tiene un bonito contorno..

Administrar traducciones

Las extensiones del navegador tienen API de internalización integradas (i18n). Le permite administrar las traducciones en varios idiomas (Lista llena). Para usar la API, necesita manifest.json documento:

"default_locale": "en"

Esto define al inglés como un idioma. Si el navegador es configurado en otro idioma no compatible, la extensión volverá a la configuración regional predeterminada (en en este ejemplo).

Nuestra traducción está en _locales contenido. Vamos a crear otra carpeta allí para cada idioma que desee admitir. Cada subdirectorio tiene su propio messages.json documento.

src 
 └─ _locales
     └─ en
        └─ messages.json
     └─ fr
        └─ messages.json

El archivo de traducción consta de varias partes:

  • Clave de traducción («id»): Esta clave se utiliza para hacer referencia a las traducciones.
  • información: Contenido de traducción real
  • Descripción (opcional): Traducciones descriptivas (no las usaría, solo inflan el archivo y su clave de traducción debería ser lo suficientemente descriptiva)
  • sustituto (opcional): Se puede utilizar para insertar contenido dinámico en las traducciones.

Aquí hay un ejemplo que conecta todo esto:

{
  "userGreeting": { // Translation key ("id")
    "message": "Good $daytime$, $user$!" // Translation
    "description": "User Greeting", // Optional description for translators
    "placeholders": { // Optional placeholders
      "daytime": { // As referenced inside the message
        "content": "$1",
        "example": "morning" // Example value for our content
      },
      "user": { 
        "content": "$1",
        "example": "Lars"
      }
    }
  }
}

Usar sustitutos es más difícil. Primero, necesitamos definir sustitutos en el mensaje. El sustituto debe estar envuelto entre ellos. $ figura. Luego, debemos agregar los sustitutos a la «lista de sustitutos». Es un poco poco intuitivo, pero Chrome quiere saber qué valor debe insertarse para nuestro reemplazo. Nosotros (obviamente) queremos usar valores dinámicos aquí, así que usamos especial content valor $1 Se refiere al valor que hemos puesto.

Esta example Las propiedades son opcionales. Se puede usar para solicitar a los traductores valores de sustitución (pero en realidad no los muestra).

Necesitamos definir las siguientes traducciones para nuestra extensión, cópielas y péguelas en messages.json no dude en agregar más idiomas (por ejemplo, si habla alemán, agregue de carpeta dentro _localesetc).

{
  "name": {
    "message": "Transcribers of Reddit"
  },
  "description": {
    "message": "Accessible image descriptions for subreddits."
  },
  "popupManageSettings": {
    "message": "Manage settings"
  },
  "optionsPageTitle": {
    "message": "Settings"
  },
  "sectionGeneral": {
    "message": "General settings"
  },
  "settingBorder": {
    "message": "Show comment border"
  },
  "settingBackground": {
    "message": "Show comment background"
  }
}

Quizás se pregunte por qué registramos permisos sin señales de permisos i18n, ¿verdad? Chrome es un poco extraño en este sentido, porque no es necesario registrar todos los permisos. chrome.i18n) no requiere una entrada en el manifiesto. Otros permisos requieren grabación, pero no se muestran al usuario cuando se instala la extensión. Algunos otros permisos son «mixtos» (por ej. chrome.runtime), lo que significa que algunas de sus funciones se pueden usar sin declarar permisos, pero otras funciones de la misma API requieren entrada en el manifiesto. documento para obtener una descripción completa de las diferencias.

Usar traducciones en manifiestos

Lo primero que verán nuestros usuarios finales es la entrada en Chrome Web Store o la página de vista previa de la extensión. Necesitamos ajustar el archivo de manifiesto para asegurarnos de que todo esté traducido.

{
  // Update these entries
  "name": "__MSG_name__",
  "description": "__MSG_description__"
}

La aplicación de esta gramática utiliza nuestra respectiva traducción messages.json archivo (por ej. _MSG_name_ usar name traducir).

Uso de traducciones en páginas HTML

Aplicar traducciones a archivos HTML requiere un poco de JavaScript.

chrome.i18n.getMessage('name');

Este código devuelve la traducción que hemos definido (es decir. Transcribers of Reddit). Los sustitutos se pueden hacer de manera similar.

chrome.i18n.getMessage('userGreeting', {
  daytime: 'morning',
  user: 'Lars'
});

Aplicar traducciones a todos los elementos de esta manera sería una molestia, pero podemos escribir un pequeño script para ejecutar un data- Atributos Entonces, vamos a crear uno nuevo js carpeta dentro src directorio, luego agregue uno nuevo util.js el archivo está dentro.

src 
 └─ js
     └─ util.js

Esto funciona:

const i18n = document.querySelectorAll("[data-intl]");
i18n.forEach(msg => {
  msg.innerHTML = chrome.i18n.getMessage(msg.dataset.intl);
});

chrome.i18n.getAcceptLanguages(languages => {
  document.documentElement.lang = languages[0];
});

Una vez que agregamos este script a la página HTML, podemos agregar data-intl Una propiedad de elemento para establecer su contenido. El idioma del archivo también se establecerá de acuerdo con el idioma del usuario.

<!-- Before JS execution -->
<html>
  <body>
    <button data-intl="popupManageSettings"></button>
  </body>
</html>
<!-- After JS execution -->
<html lang="en">
  <body>
    <button data-intl="popupManageSettings">Manage settings</button>
  </body>
</html>

Agregar una ventana emergente y una página de opciones

Antes de sumergirnos en la programación real, necesitamos crear dos páginas:

  1. Página de opciones de configuración personalizada
  2. Una página emergente que se abre cuando interactúa con el icono de la extensión junto a la barra de direcciones. Esta página se puede utilizar en varios escenarios (por ejemplo, para mostrar estadísticas o configuraciones rápidas).
La página de opciones contiene nuestra configuración.
La ventana emergente contiene un enlace a la página de opciones.

Aquí hay una breve descripción de las carpetas y archivos que necesitamos para hacer nuestra página:

src 
 ├─ css
 |    └─ paintBucket.css
 ├─ popup
 |    ├─ popup.html
 |    ├─ popup.css
 |    └─ popup.js
 └─ options
      ├─ options.html
      ├─ options.css
      └─ options.js

Esta .css El archivo contiene CSS puro y eso es todo. No entraré en detalles, porque sé que la mayoría de las personas que leen esto ya entienden completamente cómo funciona CSS. Puedes comenzar. repositorio GitHub para este proyecto.

Tenga en cuenta que las ventanas emergentes no son pestañas, su tamaño depende del contenido en ellas. Si desea utilizar un tamaño fijo de ventanas emergentes, puede establecer width y height propiedades de html elemento.

Crear una ventana emergente

Este es un marco HTML que vincula archivos CSS y JavaScript y les agrega títulos y botones. <body>.

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title data-intl="name"></title>

    <link rel="stylesheet" href="https://css-tricks.com/how-to-create-a-browser-extension/css/paintBucket.css">
    <link rel="stylesheet" href="https://css-tricks.com/how-to-create-a-browser-extension/popup.css">

    <!-- Our "translation" script -->
    <script src="https://css-tricks.com/how-to-create-a-browser-extension/js/util.js" defer></script>
    <script src="https://css-tricks.com/how-to-create-a-browser-extension/popup.js" defer></script>
  </head>
  <body>
    <h1 id="title"></h1>
    <button data-intl="popupManageSettings"></button>
  </body>
</html>

Esta h1 contiene la extensión y la versión; esta button Se utiliza para abrir la página de opciones. El título no completa la traducción (porque falta data-intl propiedad), y el botón aún no tiene controladores de clic, por lo que debemos completar nuestro popup.js documento:

const title = document.getElementById('title');
const settingsBtn = document.querySelector('button');
const manifest = chrome.runtime.getManifest();

title.textContent = `${manifest.name} (${manifest.version})`;

settingsBtn.addEventListener('click', () => {
  chrome.runtime.openOptionsPage();
});

El script primero busca un archivo de manifiesto. ofertas cromadas runtime API contiene getManifest método (no requerido para este método en particular) runtime permite) .devuelve nuestro manifest.json como un objeto JSON. Después de completar el título con la extensión y la versión, podemos agregar un detector de eventos al botón de configuración. el usuario interactúa con él, usaremos chrome.runtime.openOptionsPage() (Nuevamente, no se requiere el registro de una licencia).

La página emergente ya está completa, pero la extensión aún no sabe que existe. Necesitamos hacer esto agregando la siguiente propiedad a manifest.json documento.

"action": {
  "default_popup": "popup/popup.html",
  "default_icon": {
    "16": "images/logo/16.png",
    "48": "images/logo/48.png",
    "128": "images/logo/128.png"
  }
},

Crear una página de opciones

El proceso de creación de esta página es muy similar al que acabamos de realizar. Primero, completamos nuestra options.html documento. Aquí hay algunas etiquetas que podemos usar:

<!doctype html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title data-intl="name"></title>

  <link rel="stylesheet" href="https://css-tricks.com/how-to-create-a-browser-extension/css/paintBucket.css">
  <link rel="stylesheet" href="https://css-tricks.com/how-to-create-a-browser-extension/options.css">

  <!-- Our "translation" script -->
  <script src="https://css-tricks.com/how-to-create-a-browser-extension/js/util.js" defer></script>
  <script src="https://css-tricks.com/how-to-create-a-browser-extension/options.js" defer></script>
</head>
<body>
  <header>
    <h1>
      <!-- Icon provided by feathericons.com -->
      <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round" role="presentation">
        <circle cx="12" cy="12" r="3"></circle>
        <path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path>
      </svg>
      <span data-intl="optionsPageTitle"></span>
    </h1>
  </header>

  <main>
    <section id="generalOptions">
      <h2 data-intl="sectionGeneral"></h2>

      <div id="generalOptionsWrapper"></div>
    </section>
  </main>

  <footer>
    <p>Transcribers of Reddit extension by <a href="https://lars.koelker.dev" target="_blank">lars.koelker.dev</a>.</p>
    <p>Reddit is a registered trademark of Reddit, Inc. This extension is not endorsed or affiliated with Reddit, Inc. in any way.</p>
  </footer>
</body>
</html>

Todavía no hay opciones reales (solo un ajuste alrededor de ellas). Necesitamos escribir la página de opciones. Primero, definimos variables para acceder a nuestro shell y la configuración predeterminada dentro options.jsCongelar nuestra configuración predeterminada nos impide cambiarla accidentalmente más adelante.

const defaultSettings = Object.freeze({
  border: false,
  background: false,
});
const generalSection = document.getElementById('generalOptionsWrapper');

Luego necesitamos cargar la configuración guardada. Podemos usar (pre-registrado) storage API. En particular, necesitamos definir si queremos almacenar los datos localmente (chrome.storage.local) o sincronizar la configuración de todos los dispositivos de los usuarios finaleschrome.storage.sync). Usemos el almacenamiento local para este proyecto.

Recuperar el valor requiere uso get método. Acepta dos parámetros:

  1. el registro que queremos cargar
  2. devolución de llamada con valor

Nuestro registro puede ser una cadena (por ejemplo, settings a continuación) o una matriz de registros (útil si desea cargar varios registros). Los argumentos en la función de devolución de llamada contienen el objeto de todos los registros que definimos anteriormente { settings: ... }:

chrome.storage.local.get('settings', ({ settings }) => {
  const options = settings ?? defaultSettings; // Fall back to default if settings are not defined
  if (!settings) {
    chrome.storage.local.set({
     settings: defaultSettings,
    });
 }

  // Create and display options
  const generalOptions = Object.keys(options).filter(x => !x.startsWith('advanced'));
  
  generalOptions.forEach(option => createOption(option, options, generalSection));
});

Para mostrar las opciones, también necesitamos crear un createOption() Característica.

function createOption(setting, settingsObject, wrapper) {
  const settingWrapper = document.createElement("div");
  settingWrapper.classList.add("setting-item");
  settingWrapper.innerHTML = `
  <div class="label-wrapper">
    <label for="${setting}" id="${setting}Desc">
      ${chrome.i18n.getMessage(`setting${setting}`)}
    </label>
  </div>

  <input type="checkbox" ${settingsObject[setting] ? 'checked' : ''} id="${setting}" />
  <label for="${setting}"
    tabindex="0"
    role="switch"
    aria-checked="${settingsObject[setting]}"
    aria-describedby="${setting}-desc"
    class="is-switch"
  ></label>
  `;

  const toggleSwitch = settingWrapper.querySelector("label.is-switch");
  const input = settingWrapper.querySelector("input");

  input.onchange = () => {
    toggleSwitch.setAttribute('aria-checked', input.checked);
    updateSetting(setting, input.checked);
  };

  toggleSwitch.onkeydown = e => {
    if(e.key === " " || e.key === "Enter") {
      e.preventDefault();
      toggleSwitch.click();
    }
  }

  wrapper.appendChild(settingWrapper);
}

en – onchange El detector de eventos para nuestro interruptor (también conocido como el botón de selección) lo llamamos función updateSettingEste método guardará el valor actualizado de nuestro botón de opción en el repositorio.

Usaremos para eso set Característica. Se requieren dos parámetros: el registro que queremos reemplazar y (opcional) una devolución de llamada (que no usamos en este ejemplo). settings el registro no es un booleano o una cadena, sino un objeto que contiene varias configuraciones, usamos el operador de distribución () y simplemente sobrescriba la clave real (configuración) dentro de nosotros settings Propósito.

function updateSetting(key, value) {
  chrome.storage.local.get('settings', ({ settings }) => {
    chrome.storage.local.set({
      settings: {
        ...settings,
        [key]: value
      }
    })
  });
}

Nuevamente, necesitamos hacer esto agregando la siguiente entrada a manifest.json:

"options_ui": {
  "open_in_tab": true,
  "page": "options/options.html"
},

Dependiendo de su caso de uso, también puede configurar el cuadro de diálogo de opciones requeridas para que se abra como una ventana emergente. open_in_tab ellos llegan false.

Instalar la extensión de desarrollo

Ahora que configuramos con éxito el archivo de manifiesto y agregamos la página emergente y de opciones, podemos instalar nuestra extensión para ver si nuestra página realmente funciona. chrome://extensions Y activa el «Modo Programador». Aparecerán tres botones, haga clic en la etiqueta «Cargando desempaquetado» y seleccione src la carpeta de extensión para cargarlo.

La extensión ahora debe instalarse correctamente y nuestro mosaico Reddit Transcriber debe estar en la página.

Ahora podemos interactuar con nuestra extensión. Haga clic en el icono del rompecabezas (() junto a la barra de direcciones de su navegador y luego haga clic en la extensión Reddit Transcriber recién agregada. Ahora debería ver una pequeña ventana emergente con un botón para abrir la página de opciones.

¿hermoso, verdad? Puede verse un poco diferente en su dispositivo porque he activado el modo oscuro de estas capturas de pantalla.

Si habilita la configuración «Mostrar fondo de comentario» y «Mostrar borde de comentario» y luego vuelve a cargar la página, el estado se guardará porque lo guardamos en el almacenamiento del navegador local.

agregar secuencia de comandos de contenido

Bien, ahora podemos activar la ventana emergente e interactuar con la configuración de la extensión, pero la extensión aún no ha hecho nada muy útil. Para darle algo de vida, agregaremos un script de contenido.

agregar un archivo llamado comment.js en – js directorio y asegúrese de manifest.json documento:

"content_scripts": [
  {
    "matches": [ "*://www.reddit.com/*" ],
    "js": [ "js/comment.js" ]
  }
],

Esta content_scripts Está formado por dos partes:

  • matches: Esta matriz contiene las URL que le indican al navegador dónde queremos que se ejecute el script de contenido. Como extensión de Reddit, queremos que funcione en todas las páginas correspondientes. ://www.redit.com/*, donde el asterisco es un comodín que corresponde a todo después del dominio de nivel superior.
  • js: esta matriz contiene los scripts de contenido reales.

Los scripts de contenido no pueden interactuar con otro JavaScript (normal). Esto significa que si el script del sitio web define una variable o función, no tenemos acceso a ella. Por ejemplo:

// script_on_website.js
const username="Lars";

// content_script.js
console.log(username); // Error: username is not defined

Ahora comencemos a crear secuencias de comandos para nuestro contenido. Agreguemos algunas constantes primero comment.jsEstas constantes contienen expresiones regulares y selectores que se utilizarán más adelante. CommentUtils Se utiliza para determinar si una publicación contiene un «comentario tor» o si hay un envoltorio de comentario.

const messageTypes = Object.freeze({
  COMMENT_PAGE: 'comment_page',
  SUBREDDIT_PAGE: 'subreddit_page',
  MAIN_PAGE: 'main_page',
  OTHER_PAGE: 'other_page',
});

const Selectors = Object.freeze({
  commentWrapper: 'div[style*="--commentswrapper-gradient-color"] > div, div[style*="max-height: unset"] > div',
  torComment: 'div[data-tor-comment]',
  postContent: 'div[data-test-id="post-content"]'
});

const UrlRegex = Object.freeze({
  commentPage: //r/.*/comments/.*/,
  subredditPage: //r/.*//
});

const CommentUtils = Object.freeze({
  isTorComment: (comment) => comment.querySelector('[data-test-id="comment"]') ? comment.querySelector('[data-test-id="comment"]').textContent.includes('m a human volunteer content transcriber for Reddit') : false,
  torCommentsExist: () => !!document.querySelector(Selectors.torComment),
  commentWrapperExists: () => !!document.querySelector('[data-reddit-comment-wrapper="true"]')
});

Luego verificamos si el usuario abre directamente la página de comentarios («publicación»), luego lo hacemos verificar y actualizar RegEx directPage Cambiar. Esto sucede cuando el usuario abre la URL directamente (por ejemplo, escribiendo en la barra de direcciones o haciendo clic en<a> elemento en otra página, como Twitter).

let directPage = false;
if (UrlRegex.commentPage.test(window.location.href)) {
  directPage = true;
  moveComments();
}

Además de abrir directamente la página, el usuario suele interactuar con SPA. Para captar esta situación, podemos comment.js archivo usando runtime API.

chrome.runtime.onMessage.addListener(msg => {
  if (msg.type === messageTypes.COMMENT_PAGE) {
    waitForComment(moveComments);
  }
});

Todo lo que necesitamos ahora son funciones. Vamos a crear un moveComments() Característica. Mueve el especial «para comentar» al principio de la sección de comentarios. También aplica condicionalmente el color de fondo y el borde (si los bordes están permitidos en la configuración) a la anotación.Para este propósito llamamos storage API y arranque settings Entrada:

function moveComments() {
  if (CommentUtils.commentWrapperExists()) {
    return;
  }

  const wrapper = document.querySelector(Selectors.commentWrapper);
  let comments = wrapper.querySelectorAll(`${Selectors.commentWrapper} > div`);
  const postContent = document.querySelector(Selectors.postContent);

  wrapper.dataset.redditCommentWrapper="true";
  wrapper.style.flexDirection = 'column';
  wrapper.style.display = 'flex';

  if (directPage) {
    comments = document.querySelectorAll("[data-reddit-comment-wrapper="true"] > div");
  }

  chrome.storage.local.get('settings', ({ settings }) => { // HIGHLIGHT 18
    comments.forEach(comment => {
      if (CommentUtils.isTorComment(comment)) {
        comment.dataset.torComment="true";
        if (settings.background) {
          comment.style.backgroundColor="var(--newCommunityTheme-buttonAlpha05)";
        }
        if (settings.border) {
          comment.style.outline="2px solid red";
        }
        comment.style.order = "-1";
        applyWaiAria(postContent, comment);
      }
    });
  })
}

Esta applyWaiAria() la función se llama internamente moveComments() funcionalidad – agrega aria- Atributos Otra función crea un identificador único para usar aria- Atributos.

function applyWaiAria(postContent, comment) {
  const postMedia = postContent.querySelector('img[class*="ImageBox-image"], video');
  const commentId = uuidv4();

  if (!postMedia) {
    return;
  }

  comment.setAttribute('id', commentId);
  postMedia.setAttribute('aria-describedby', commentId);
}

function uuidv4() {
  return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
    var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
    return v.toString(16);
  });
}

La siguiente función está esperando que los comentarios se carguen y llamen callback parámetro si encuentra el contenedor de comentarios.

function waitForComment(callback) {
  const config = { childList: true, subtree: true };
  const observer = new MutationObserver(mutations => {
    for (const mutation of mutations) {
      if (document.querySelector(Selectors.commentWrapper)) {
        callback();
        observer.disconnect();
        clearTimeout(timeout);
        break;
      }
    }
  });

  observer.observe(document.documentElement, config);
  const timeout = startObservingTimeout(observer, 10);
}

function startObservingTimeout(observer, seconds) {
  return setTimeout(() => {
    observer.disconnect();
  }, 1000 * seconds);
}

Agregar un trabajador de servicio

¿Recuerdas cuando agregamos detectores de mensajes a los guiones de contenido? Este oyente no está recibiendo mensajes actualmente. Necesitamos enviarlo al script de contenido nosotros mismos. Para hacer esto, necesitamos registrar un trabajador de servicio.

debemos ser manifest.json añadiéndole el siguiente código:

"background": {
  "service_worker": "sw.js"
}

no olvides crear sw.js dentro del archivo src directorio (los trabajadores de servicio siempre deben crearse en el directorio raíz de la extensión, src.

Ahora vamos a crear algunas constantes para mensajes y tipos de página:

const messageTypes = Object.freeze({
  COMMENT_PAGE: 'comment_page',
  SUBREDDIT_PAGE: 'subreddit_page',
  MAIN_PAGE: 'main_page',
  OTHER_PAGE: 'other_page',
});

const UrlRegex = Object.freeze({
  commentPage: //r/.*/comments/.*/,
  subredditPage: //r/.*//
});

const Utils = Object.freeze({
  getPageType: (url) => {
    if (new URL(url).pathname === "https://css-tricks.com/") {
      return messageTypes.MAIN_PAGE;
    } else if (UrlRegex.commentPage.test(url)) {
      return messageTypes.COMMENT_PAGE;
    } else if (UrlRegex.subredditPage.test(url)) {
      return messageTypes.SUBREDDIT_PAGE;
    }

    return messageTypes.OTHER_PAGE;
  }
});

Podemos agregar el contenido real al trabajador del servicio. Hacemos esto con la ayuda de oyentes de eventos en el estado de la historia (onHistoryStateUpdated) para saber cuándo se actualiza la página API de historial (Usualmente usado en SPA para navegar sin refrescar la página.) En este oyente solicitamos la sección activa y recuperamos su tabIdLuego enviamos un mensaje a nuestro script de contenido con el tipo de página y la URL.

chrome.webNavigation.onHistoryStateUpdated.addListener(async ({ url }) => {
  const [{ id: tabId }] = await chrome.tabs.query({ active: true, currentWindow: true });

  chrome.tabs.sendMessage(tabId, {
    type: Utils.getPageType(url),
    url
  });
});

¡Listo!

¡Hemos terminado! Vaya a la página de administración de extensiones de Chrome ( chrome://extensions) y haga clic en el icono para volver a cargar la extensión desempaquetada. Si abre una publicación de Reddit que contiene un comentario de «Reddit Transcriber» con una transcripción de imagen esta), siempre que lo activemos en los ajustes de la extensión, se moverá al principio de la sección de comentarios y quedará resaltado.

La extensión Scribe de Reddit resalta un comentario específico moviéndolo a la parte superior de la lista de comentarios de hilos de Reddit y dándole un marco rojo brillante

en conclusión

¿Es tan difícil como crees? Definitivamente es mucho más simple de lo que pensaba antes de enterrarlo.Después de la instalación manifest.json Y para crear cualquier archivo de páginas y activos que necesitemos, todo lo que realmente tenemos que hacer es escribir HTML, CSS y JavaScript como de costumbre.

Si te quedas atascado en el camino, API de cromo La documentación es un gran recurso para volver a encarrilarte.

de nuevo, Este es el repositorio de GitHub Use todo el código que hemos cubierto en este artículo. ¡Léelo, úsalo y dime lo que piensas!

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Botón volver arriba