Contents
Introducción
Este tutorial explica, paso a paso y con todos los detalles prácticos, cómo añadir la funcionalidad de copy-to-clipboard a los bloques de código en un sitio WordPress usando JavaScript. Cubriremos la forma recomendada moderna (Clipboard API), la copia por compatibilidad (execCommand), integración correcta en WordPress (enqueue de scripts y localización), compatibilidad con diferentes plugins/formatos de código y buenas prácticas de accesibilidad y UX (feedback, manejo en móviles, observador para contenido dinámico).
Requisitos previos
- WordPress 5.x o superior (o cualquier instalación donde puedas editar functions.php o crear un plugin simple).
- Acceso a los archivos del tema (functions.php) o a un plugin personalizado para encolar scripts y estilos.
- HTTPS preferible (navigator.clipboard requiere contexto seguro en la mayoría de navegadores).
- Conocimientos básicos de JavaScript, CSS y PHP para integrar el código en WordPress.
Resumen de la estrategia
- Detectar los bloques de código presentes en la página (varias estructuras: pre > code, pre[class=language-], .wp-block-code, plugins como Prism, Highlight.js o Enlighter).
- Insertar un botón Copiar en cada bloque (con marcado accesible y estilos CSS).
- Implementar la acción de copia usando navigator.clipboard.writeText() y proporcionar un fallback a document.execCommand(copy) para navegadores antiguos.
- Proveer retroalimentación visual (tooltip o cambio de texto del botón) y accesible (aria-live, aria-label).
- Encolar los scripts y estilos correctamente en WordPress y proveer traducciones/strings mediante wp_localize_script.
Ejemplo completo de JavaScript (front-end)
Este script detecta bloques de código comunes, añade un botón y gestiona la copia con fallback. También incluye un MutationObserver para detectar contenido cargado dinámicamente.
/
copy-to-clipboard for code blocks
- Detects: pre > code, pre[class=language-], .wp-block-code, .enlighter
- Uses Clipboard API with fallback
/
(function () {
use strict
var strings = {
copy: Copiar,
copied: Copiado,
error: Error
}
// Utilidades
function createButton(label) {
var btn = document.createElement(button)
btn.type = button
btn.className = code-copy-button
btn.setAttribute(aria-label, label)
btn.textContent = label
return btn
}
function getCodeText(block) {
// Si el bloque es ...
var codeEl = block.querySelector(code)
if (codeEl) return codeEl.innerText
// Si el bloque es
...
return block.innerText
}
function copyTextToClipboard(text) {
if (!text) return Promise.reject(new Error(No text to copy))
if (navigator.clipboard navigator.clipboard.writeText) {
return navigator.clipboard.writeText(text)
}
// Fallback usando textarea execCommand
return new Promise(function (resolve, reject) {
var textarea = document.createElement(textarea)
textarea.value = text
// Evitar scroll
textarea.style.position = fixed
textarea.style.left = -9999px
document.body.appendChild(textarea)
textarea.select()
try {
var successful = document.execCommand(copy)
document.body.removeChild(textarea)
if (successful) return resolve()
return reject(new Error(execCommand failed))
} catch (err) {
document.body.removeChild(textarea)
return reject(err)
}
})
}
function showTemporaryFeedback(btn, text) {
var original = btn.textContent
btn.textContent = text
btn.setAttribute(aria-pressed, true)
setTimeout(function () {
btn.textContent = original
btn.setAttribute(aria-pressed, false)
}, 2000)
}
function attachButtonToBlock(block) {
// Evitar duplicados
if (block.querySelector(.code-copy-button)) return
var btn = createButton(strings.copy)
btn.classList.add(code-copy-button-installed)
btn.addEventListener(click, function (e) {
e.preventDefault()
var text = getCodeText(block)
copyTextToClipboard(text).then(function () {
showTemporaryFeedback(btn, strings.copied)
}).catch(function () {
showTemporaryFeedback(btn, strings.error)
})
})
// Insertar el botón al inicio del
o como primer hijo del bloque
// Algunos themes bloquean insertar elementos fuera, pero normalmente funciona.
block.insertBefore(btn, block.firstChild)
}
function findCodeBlocks() {
var blocks = []
// pre > code
document.querySelectorAll(pre > code).forEach(function (el) {
blocks.push(el.parentNode)
})
// pre[class=language-]
document.querySelectorAll(pre[class=language-]).forEach(function (el) {
if (!blocks.includes(el)) blocks.push(el)
})
// wp-block-code (Gutenberg)
document.querySelectorAll(.wp-block-code).forEach(function (el) {
if (!blocks.includes(el)) blocks.push(el)
})
// EnlighterJS / Highlight.js wrappers - intentar capturar igualmente
document.querySelectorAll(pre).forEach(function (el) {
if (!blocks.includes(el)) blocks.push(el)
})
return blocks
}
function init() {
var blocks = findCodeBlocks()
blocks.forEach(function (block) {
attachButtonToBlock(block)
})
}
// Inicializa inmediatamente
if (document.readyState === loading) {
document.addEventListener(DOMContentLoaded, init)
} else {
init()
}
// Observador para contenido dinámico (AJAX o carga diferida)
var observer = new MutationObserver(function (mutations) {
mutations.forEach(function (m) {
if (m.addedNodes m.addedNodes.length) {
// Re-run init para nuevos nodos
init()
}
})
})
observer.observe(document.documentElement document.body, {
childList: true,
subtree: true
})
// Export para pruebas si es necesario
window.__WPCodeCopy = {
init: init
}
})()
Estilos CSS recomendados
Un CSS ligero para posicionar el botón dentro del bloque de código. Adáptalo al diseño de tu tema.
/ Estilos mínimos para el botón de copiar /
.code-copy-button {
position: absolute
right: 0.5rem
top: 0.5rem
background: rgba(0,0,0,0.6)
color: #fff
border: none
padding: 0.25rem 0.5rem
border-radius: 3px
cursor: pointer
font-size: 0.9rem
z-index: 10
}
/ Si el pre no es positioning relative, es mejor hacerlo /
pre {
position: relative
overflow: auto
padding-top: 2.2rem / espacio para el botón /
}
/ Opcional: mejor visibilidad en hover /
.code-copy-button:hover {
background: rgba(0,0,0,0.8)
}
/ Ajustes móviles /
@media (max-width: 480px) {
.code-copy-button {
top: 0.2rem
right: 0.2rem
padding: 0.2rem 0.4rem
font-size: 0.8rem
}
}
Integración en WordPress (functions.php)
Encola el script y los estilos correctamente para que estén disponibles en el front-end. Además, utiliza wp_localize_script para pasar los textos traducibles al script.
__(Copiar, tu-text-domain),
copied => __(Copiado, tu-text-domain),
error => __(Error, tu-text-domain),
)
wp_localize_script(theme-code-copy, WPCodeCopyStrings, strings)
wp_enqueue_script(theme-code-copy)
}
add_action(wp_enqueue_scripts, theme_enqueue_code_copy)
?>
En el JavaScript anterior puedes leer WPCodeCopyStrings para sustituir los textos hardcodeados (en el ejemplo del script general puedes reemplazar la variable strings por WPCodeCopyStrings):
// Dentro del IIFE, reemplazar la asignación de strings por:
var strings = window.WPCodeCopyStrings { copy: Copiar, copied: Copiado, error: Error }
Compatibilidad con plugins y editores
- Gutenberg: los bloques de código quedan con la clase .wp-block-code el script los detecta.
- Prism / Highlight.js / Enlighter: la mayoría envuelven el código en ltpregt o ltpregtltcodegt, por eso el script comprueba ambas estructuras.
- Si tu plugin usa una estructura HTML distinta, adapta el selector en findCodeBlocks() (ej. document.querySelectorAll(.mi-clase-de-codigo)).
Accesibilidad y UX
- Usar un ltbuttongt nativo en lugar de un ltdivgt hace que el control sea focoable y operativo con teclado.
- Proveer aria-label con el texto Copiar y cambiar aria-pressed o usar aria-live para anunciar el estado si es necesario.
- Para usuarios de lectores de pantalla, wp_localize_script permite traducir Copiar / Copiado.
- Evitar seleccionar texto visualmente abrupto el script usa textarea temporal y lo limpia inmediatamente en fallback.
Manejo de casos especiales
- Contenido cargado dinámicamente: el MutationObserver en el script re-evalúa y añade botones a nuevos bloques.
- Bloques muy largos o con scroll: colocar el botón posicionado con position: absolute dentro del pre evita romper el flujo.
- Evitar duplicado al re-renderizar: el script comprueba la presencia del botón antes de insertar uno nuevo.
Seguridad y privacidad
- La Clipboard API solo funciona en contextos seguros (HTTPS) y el navegador puede requerir interacción del usuario (click) — lo cual es exactamente lo que hacemos.
- No copiar ni leer contenido del portapapeles del usuario sin su acción explícita.
- No envíes el contenido copiado a servidores externos. El ejemplo mantiene la operación en el cliente.
Alternativas y mejoras
- Integración directa con highlighters: algunos plugins (Prism) ofrecen hooks o plugins de copia. Si usas Prism, considera usar su plugin oficial prism-copy-to-clipboard.
- Mostrar un tooltip en lugar de cambiar el texto del botón puedes usar aria-live para anunciar Copiado a lectores de pantalla.
- Permitir copia de una selección específica (p. ej. copiar sólo la línea activa) con data attributes por bloque.
- Implementar animaciones o iconos SVG para mejor experiencia visual.
Depuración
- Verifica en consola si el archivo JS se ha encolado correctamente (buscar theme-code-copy en los assets cargados).
- Si el botón no aparece, inspecciona si los selectores detectan el bloque. Prueba en la consola: document.querySelectorAll(pre > code).
- Si la copia falla, intenta verificar si navigator.clipboard está presente y si la acción ocurre tras un evento de usuario (click).
Conclusión
La implementación de un botón copy-to-clipboard en los bloques de código en WordPress es relativamente sencilla: requiere insertar un botón accesible, usar la Clipboard API con fallback y encolar el script y estilos mediante las funciones de WordPress. Con las pequeñas mejoras (observador para contenido dinámico, localización de textos y ajustes responsivos) tendrás una solución robusta y compatible con la mayoría de temas y plugins de sintaxis.
Recursos útiles
|
|
Acepto donaciones de BAT's mediante el navegador Brave 🙂 |
