Como añadir hotkeys útiles en el editor con JS en WordPress

Contents

Introducción

En este tutorial detallado aprenderás a añadir hotkeys (atajos de teclado) útiles en el editor de WordPress usando JavaScript. Veremos dos entornos principales: el editor clásico (TinyMCE y Quicktags) y el editor de bloques (Gutenberg). Incluyo ejemplos completos de código para encolar los scripts en el administrador, para registrar atajos en ambos editores y buenas prácticas para evitar conflictos y respetar accesibilidad.

Resumen de la estrategia

  • Encolar un script JS solo en las pantallas de edición (post-new.php / post.php).
  • Detectar el tipo de editor activo (clásico o Gutenberg) y registrar los atajos de forma no intrusiva.
  • Comprobar el foco y los contextos donde deben funcionar los atajos para evitar interferir con otros campos.
  • Manejar combinaciones comunes (Ctrl/Cmd tecla) y permitir personalización o desactivación.

Preparación: encolar scripts en el admin

Primero, añadiremos el código PHP necesario para encolar el script solo en las pantallas de edición. Este fragmento se puede incluir en el functions.php del tema o en un plugin.

 function_exists( is_gutenberg_page ) ? true : false,
    ) )
}
add_action( admin_enqueue_scripts, mi_plugin_enqueue_editor_hotkeys )
?>

Editor clásico (TinyMCE y Quicktags)

En el editor clásico hay dos modos principales: el editor visual basado en TinyMCE y el editor de texto (Quicktags). La técnica más segura es registrar listeners de teclado en el iframe del editor de TinyMCE o usar la API de TinyMCE cuando esté disponible, además de añadir un listener para el textarea del modo texto/HTML.

Ejemplo: Hotkey para aplicar negrita en TinyMCE y en modo texto

El siguiente ejemplo añade Ctrl/Cmd B para alternar negrita en ambos modos.

// mi-editor-hotkeys.js (parte para el editor clásico)
(function() {
    // Helpers
    function isMac() {
        return /MaciPodiPhoneiPad/.test(navigator.platform)
    }

    function isEditorPage() {
        // Si necesitas comprobaciones extra, agrégalas aquí
        return true
    }

    // TinyMCE: cuando está inicializado
    if ( typeof tinymce !== undefined ) {
        tinymce.PluginManager.add(mi_hotkeys_plugin, function(editor) {
            editor.on(keydown, function(e) {
                var key = e.key  String.fromCharCode(e.keyCode).toLowerCase()
                var ctrlOrCmd = e.ctrlKey  e.metaKey
                if ( ctrlOrCmd  key.toLowerCase() === b ) {
                    e.preventDefault()
                    // Ejecutar comando de TinyMCE para negrita
                    editor.execCommand(Bold)
                }
            })
        })

        // Asegurar que el plugin se cargue en la inicialización de tinymce
        if ( tinymce.editors ) {
            for ( var id in tinymce.editors ) {
                if ( tinymce.editors.hasOwnProperty(id) ) {
                    var ed = tinymce.editors[id]
                    // Registrar el plugin en ed
                    ed.on(BeforeExecCommand, function() {})
                }
            }
        }
    }

    // Quicktags / textarea (modo texto)
    document.addEventListener(keydown, function(e) {
        var active = document.activeElement
        if (!active) return

        // Comprueba si estamos en un textarea del editor (modo texto)
        if ( active.tagName === TEXTAREA 
             ( active.id === content  active.classList.contains(wp-editor-area) ) ) {

            var key = e.key  String.fromCharCode(e.keyCode).toLowerCase()
            var ctrlOrCmd = e.ctrlKey  e.metaKey
            if ( ctrlOrCmd  key.toLowerCase() === b ) {
                e.preventDefault()
                var start = active.selectionStart
                var end = active.selectionEnd
                var selected = active.value.substring(start, end)
                var before = active.value.substring(0, start)
                var after = active.value.substring(end)
                // Si ya está entre  , quita si no, añade
                if ( selected.indexOf() === 0  selected.lastIndexOf() === selected.length-2 ) {
                    selected = selected.substring(2, selected.length-2)
                } else {
                    selected =    selected   
                }
                active.value = before   selected   after
                // restaurar selección
                active.selectionStart = start
                active.selectionEnd = start   selected.length
            }
        }
    }, false)
})()

Notas:

  • Registrar un plugin TinyMCE de la forma oficial es más robusto si controlas la inicialización de TinyMCE. Si no, usar el manejador de eventos keydown también funciona.
  • En el ejemplo de Quicktags hacemos una manipulación simple de texto Markdown/estrella para demostrar el comportamiento adáptalo según tu convención.

Editor de Bloques (Gutenberg)

Gutenberg es un entorno React. Lo más simple y compatible es añadir un listener global de teclado que actúe solo cuando el foco esté dentro de las áreas editables del editor de bloques. Evita interferir cuando el usuario esté en inputs, selects, modales o cuando una caja de diálogo tenga el foco.

Patrones seguros en Gutenberg

  • Verificar que document.activeElement esté dentro de las áreas con clase conocidas de Gutenberg (por ejemplo, editor-styles-wrapper, block-editor, wp-block-editor__editable, editor-post-title__input).
  • Evitar atajos que usen teclas que ya maneja Gutenberg (por ejemplo, Ctrl S para guardar está ya reservado por el navegador/WordPress).
  • Usar los paquetes de WordPress si desarrollas un plugin React para un script simple encolado, la comprobación del DOM es suficiente.

Ejemplo: atajos en Gutenberg (insertar párrafo y alternar negrita con Ctrl/Cmd B)

// mi-editor-hotkeys.js (parte para Gutenberg)
(function() {
    function isEditableGutenbergTarget( el ) {
        if (!el) return false
        // Verifica elementos con las clases que Gutenberg usa para áreas editables
        return !!el.closest(.editor-styles-wrapper, .wp-block-editor__editable, .editor-post-title__input, .block-editor-rich-text__editable)
    }

    window.addEventListener(keydown, function(e) {
        var key = e.key ? e.key.toLowerCase() : String.fromCharCode(e.keyCode).toLowerCase()
        var ctrlOrCmd = e.ctrlKey  e.metaKey
        var active = document.activeElement

        // No interferir cuando el usuario está en un input, textarea, select o contentEditable no deseado
        if ( active  ( active.tagName === INPUT  active.tagName === SELECT  active.tagName === TEXTAREA ) ) {
            return
        }

        // Verifica que el foco esté en el editor Gutenberg
        if ( !isEditableGutenbergTarget( active ) ) {
            return
        }

        // Ctrl/Cmd B -> alternar negrita en el bloque de texto actual
        if ( ctrlOrCmd  key === b ) {
            e.preventDefault()
            // Intentamos simular Cmd/Ctrl B disparando el comando de formato
            // En Gutenberg el editor escucha atajos nativos, pero si necesitas forzar:
            // Intentar ejecutar el comando que el editor entiende (foco en editable)
            document.execCommand(bold) // funcionará en la mayoría de contentEditable
            return
        }

        // Alt Shift P -> insertar párrafo debajo (ejemplo de hotkey personalizado)
        if ( e.altKey  e.shiftKey  key === p ) {
            e.preventDefault()
            // Crear un nuevo bloque párrafo al final o después del bloque actual
            // La forma robusta es usar la data API de WP si está disponible
            if ( window.wp  wp.data  wp.data.dispatch ) {
                try {
                    var blocks = wp.data.select(core/block-editor)  wp.data.select(core/editor)
                    var dispatch = wp.data.dispatch(core/block-editor)  wp.data.dispatch(core/editor)
                    if ( blocks  dispatch  dispatch.insertBlocks ) {
                        var paragraph = wp.blocks.createBlock(core/paragraph, { content:  })
                        dispatch.insertBlocks(paragraph)
                        return
                    }
                } catch (err) {
                    // Fallback simple: insertar un salto de línea en el contentEditable activo
                    var sel = window.getSelection()
                    if (sel  sel.rangeCount) {
                        var range = sel.getRangeAt(0)
                        var br = document.createElement(p)
                        br.innerHTML = 
range.collapse(false) range.insertNode(br) // Mover cursor dentro del nuevo párrafo range.setStart(br, 0) sel.removeAllRanges() sel.addRange(range) } } } } }, false) })()

Notas sobre este ejemplo:

  • document.execCommand(bold) sigue funcionando en contentEditable y es una vía rápida para alternar negrita si el foco está en un rich text. Para integraciones profundas, usa la API de datos de Gutenberg (wp.data) y los bloques (wp.blocks).
  • El ejemplo muestra un fallback si las APIs de wp. no están disponibles.

Buenas prácticas y compatibilidad

  1. Restringir el alcance: Ejecuta los atajos solo cuando el foco está en el editor para no colisionar con atajos en el admin o en otros plugins.
  2. Usar combinaciones estándar: Preferir Ctrl/Cmd letra para acciones comunes usar combinaciones con Alt/Shift para atajos menos habituales.
  3. Permitir desactivar: Si distribuyes el código, ofrece una opción para desactivar los atajos o para personalizar combinaciones.
  4. Detectar plataforma: Ten en cuenta Mac (Meta/Command) vs Windows/Linux (Control).
  5. Evitar conflictos: Antes de elegir un atajo, comprueba atajos nativos del navegador y de WordPress (por ejemplo, Ctrl S, Ctrl Z, Ctrl Y).
  6. Accesibilidad: Mantén la posibilidad de usar la interfaz con teclado estándar y evitar interferir con tecnologías de asistencia.

Lista de hotkeys útiles sugeridas

  • Ctrl/Cmd B — Negrita
  • Ctrl/Cmd I — Cursiva
  • Ctrl/Cmd K — Insertar/editar enlace
  • Alt Shift P — Insertar párrafo/crear nuevo bloque párrafo (Gutenberg)
  • Alt Shift H — Mostrar ayuda o lista de atajos personalizada
  • Ctrl/Cmd Alt M — Insertar una plantilla o bloque personalizado

Ejemplo completo: pequeño plugin que añade atajos

A continuación tienes un ejemplo de plugin mínimo que encola un JS y contiene la lógica para ambos editores. Guarda el archivo PHP en una carpeta de plugin y crea el archivo JS en la subcarpeta js/.


// js/meh-hotkeys.js
(function() {
    // Detección simple de plataforma
    var isMac = /MaciPodiPhoneiPad/.test(navigator.platform)

    // Comprueba si el elemento activo pertenece al editor de bloques
    function isGutenbergEditable(el) {
        return !!el  !!el.closest  !!el.closest(.editor-styles-wrapper, .wp-block-editor__editable, .editor-post-title__input, .block-editor-rich-text__editable)
    }

    function handleGlobalKeydown(e) {
        var key = e.key ? e.key.toLowerCase() : String.fromCharCode(e.keyCode).toLowerCase()
        var ctrlOrCmd = e.ctrlKey  e.metaKey
        var active = document.activeElement

        // No interferir con inputs o selects
        if (active  (active.tagName === INPUT  active.tagName === SELECT  active.tagName === TEXTAREA)) {
            return
        }

        // Ctrl/Cmd B -> negrita
        if (ctrlOrCmd  key === b) {
            // Solo si el foco está en el editor clásico o Gutenberg
            var inClassicTextArea = active  active.tagName === TEXTAREA  (active.id === content  active.classList.contains(wp-editor-area))
            var inTiny = (window.tinymce  window.tinymce.activeEditor  window.tinymce.activeEditor.targetElm  window.tinymce.activeEditor.targetElm.contains(active))
            var inGutenberg = isGutenbergEditable(active)

            if (inClassicTextArea) {
                e.preventDefault()
                // Lógica simple para envolver con 
                var ta = active
                var start = ta.selectionStart
                var end = ta.selectionEnd
                var sel = ta.value.substring(start, end)
                if (sel.indexOf() === 0  sel.lastIndexOf() === sel.length-2) {
                    sel = sel.substring(2, sel.length-2)
                } else {
                    sel =    sel   
                }
                ta.value = ta.value.substring(0, start)   sel   ta.value.substring(end)
                ta.selectionStart = start
                ta.selectionEnd = start   sel.length
                return
            }

            if (inTiny) {
                e.preventDefault()
                try {
                    window.tinymce.activeEditor.execCommand(Bold)
                } catch (err) {}
                return
            }

            if (inGutenberg) {
                e.preventDefault()
                // Intentamos usar execCommand en contentEditable
                try {
                    document.execCommand(bold)
                } catch (err) {}
                return
            }
        }

        // Alt Shift P -> insertar párrafo/crear bloque en Gutenberg
        if (e.altKey  e.shiftKey  key === p) {
            var inG = isGutenbergEditable(active)
            if (!inG) return
            e.preventDefault()
            if (window.wp  wp.data  wp.data.dispatch  wp.blocks  wp.blocks.createBlock) {
                try {
                    var paragraph = wp.blocks.createBlock(core/paragraph, { content:  })
                    wp.data.dispatch(core/block-editor).insertBlocks(paragraph)
                    return
                } catch (err) {
                    // fallback simple
                }
            }
            // fallback DOM: insertar 


en el contentEditable try { var sel = window.getSelection() if (sel sel.rangeCount) { var range = sel.getRangeAt(0) var p = document.createElement(p) p.innerHTML =
range.collapse(false) range.insertNode(p) range.setStart(p, 0) sel.removeAllRanges() sel.addRange(range) } } catch (err) {} } } window.addEventListener(keydown, handleGlobalKeydown, false) })()

Conclusión

Con las técnicas mostradas puedes añadir atajos útiles tanto al editor clásico como a Gutenberg. La clave es encolar el script solo en las pantallas de edición, detectar correctamente el contexto (foco y tipo de editor), y ofrecer un comportamiento que respete la experiencia del usuario y la accesibilidad. Adapta las combinaciones y lógicas según tus necesidades: puedes añadir opciones de configuración, registrar atajos condicionales o integrarlos más profundamente con las APIs de WordPress si tu plugin usa React y las dependencias de @wordpress/.



Acepto donaciones de BAT's mediante el navegador Brave 🙂



Deja una respuesta

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