Cómo se hace un menú contextual

El menú contextual

Menú contextual Un menú contextual es una lista de acciones que aparecen en un contenedor emergente por encima del resto de contenidos de una aplicación. Suele activarse con el botón derecho del ratón y las acciones ofrecidas son seleccionadas según el contexto donde se haya activado. En inglés podemos encontrar términos como context menu, shortcut o pop-up menu. En ejemplos de aplicación del formulario emergente ya hice un primer intento de un menú contextual configurando el formulario emergente para eso.

Pero me quedó pendiente el tema de la activación con el botón derecho del ratón. Ahora necesitaba un menú contextual para otra aplicación y he creido conveniente no usar el formulario emergente para ese cometido pues es un componente excesivamente pesado para una tarea que no requiere tanto código.

Para empezar puede ver este nuevo menú contextual en acción en el siguiente contenedor de ejemplo, activándose con el botón derecho del ratón:

Ejemplo:

En este contenedor puede probar el menú contextual.

Se guarda el elemento sobre el que se abre el menú, permitiendo ejecutar otras acciones complementarias como ocultar grupos de menú. Aquí hay algunos <input> de prueba que presentarán opciones para ver sus atributos value y type (están dentro de una lista <ul> y a su vez dentro de un <form>). Mientras que para el resto de elementos se presentarán las opciones para extraer outerHTML y chilNodes.length.

Podemos también cancelar la presentación de menú, como sobre este elemento de color azul.

Estos son unos elementos posicionados. El azul es un <div> con position: relative. El verde es un <div> con posición absolute y el rojo es un <span> también posicionado absolutamente.

Y en una tabla:

(1,1)(1,2)
(2,1)(2,2)
(3,1)(3,2)

Podemos crear uno o más menús contextuales en una página. Por ejemplo, sobre este otro elemento podemos abrir otro menú contextual con opciones y estilo diferente.

25 octubre 2013: Tras la actualización para que funcione en móviles, he probado esta aplicación con los siguientes resultados.

  • En navegadores no móviles funciona como se espera en Chrome 30, Firefox 24, Opera 12.16, Safari 5.1.7 e IE8+.
  • En Android 4.2.2 funciona como se espera en los navegadores Chrome 30, Opera 12.16 y en el Chromium que viene por defecto, que equivale a la versión 18 de Chrome. En el navegador Firefox 24 se produce un error no interceptable que impide abrir el menú contextual. La causa es que si se hace no seleccionable un elemento con user-select: none y hacemos un toque largo (long tap) necesario para activar el menú contextual aparecerá el error IndexSizeError: Index or size is negative or greater than the allowed amount @ chrome:‍//‍browser/‍content/‍SelectionHandler.‍js:‍460. Hay un bug Mozilla registrado sobre este fallo pero aún no parece resuelto.
  • En el emulador de Firefox OS (versión 24) también funciona como se espera.
  • En el navegador de Android 2.3.3 no se activa el menú contextual. Dado que no sé cómo depurar remotamente esta versión de Android no sé cuál será el motivo.

12 marzo 2013: He probado esta aplicación en Chrome 25.0, Firefox 19.0, Opera 12.14, Safari 5.1.7, IE8+

Implementar un menú contextual

El menú contextual se genera en objetos Javascript con el módulo menu-context.js. Usa algunas funciones del módulo general.js como agregarEventListener(), recogeEvento(), estiloActual() y esNavegador(). En el código se explica la razón de su uso pudiendo agregar esas funciones al módulo del menú o bien usar directamente general.js. El CSS necesario no es muy extenso y se puede agregar en el encabezado de la página. Para implementar un menú contextual hemos de declarar una instancia de un objeto en el window.onload de la página:

Desde hace algún tiempo estoy usando espacio de nombres para las aplicaciones JavaScript. Wextensible es el espacio de nombres global y wxL es un acortador para Wextensible.local, un objeto donde ubico las variables de la página. En otras partes del código encontrará la referencia wxG para el módulo Wextensible.general cargado desde el archivo general.js. Si no va a usar espacios de nombres tendría que poner las variables en el espacio Global. Por ejemplo, la declaración wxL.menu2 = ... pasaría a ser var menu2 = ....

Este es el código para crear el segundo de los ejemplos anteriores. Primero declaramos las entradas u opciones del menú. Se trata de un objeto literal que contiene uno o mas grupos de menú, con un único grupo en este ejemplo. Cada grupo luego se separa visualmente con una línea horizontal. En cada grupo ponemos las entradas que pueden ser configuradas de varias formas:

  1. TÍTULO: ACCIÓN, dos cadenas de texto siendo la primera el título de la entrada y la segunda un literal de función. Algo como lo que aparece en el ejemplo "Entrada 1": "alert('Acción Entrada 1')". Vea que la acción es una llamada a una función y por tanto es un literal JavaScript. Hemos de escapar a su vez las comillas dobles con simples o bien usar &quot;, pues al final este literal se incluirá en un evento onclick del elemento HTML a generar.
  2. CLAVE: [ACCIÓN, TÍTULO]. El valor es un array, siendo la segunda cadena la acción y la tercera cadena el título. Por lo tanto la primera cadena no será usada pero hay que recordar que es la clave de esa entrada en el objeto literal, por lo que no debe encontrarse duplicada. En el título podemos incluir literales HTML, como las entradas del primer ejemplo donde aparecen imagenes o diferentes estilos para el texto.
  3. En los casos anteriores la ACCIÓN puede ser una referencia a un submenú.

Para declarar submenús veámos un extracto del código del primer ejemplo, eliminado algunas partes del código con puntos suspensivos:

Declararemos los submenús desde los niveles más profundos. En el ejemplo tenemos el nivel principal y dos niveles más. En el nivel 2 tenemos dos submenús que hemos declarado en wxL.submenu2a y wxL.submenu2b. Estos son llamados en las opciones B2 y B3 del submenú de nivel 1. Vea las referencias wxL.submenu2a y wxL.submenu2b en la declaración de las entradas de ese nivel. Luego en el nivel principal declaramos el submenú wxL.submenu1, esta vez dentro de un array para incluir literal HTML en el título usando además sprites CSS.

Al construir un objeto menú usamos la declaración new MenuContext(nombreInstancia, idContenedor, menu, prefijo, esSubmenu) con estos argumentos:

  • nombreInstancia es una cadena para repetir el nombre de la instancia. Por ejemplo, var miMenu = new MenuContext("miMenu", ...).
  • idContenedor es una cadena del identificador de un elemento de la página donde se abrira el menú.
  • menu es un objeto de declaración del menú como comentamos antes.
  • prefijo es una cadena para poder declara estilo CSS específico para cada menú de una página. En el segundo ejemplo hemos declarado el objeto con el prefijo "otro-". Puede ver en el elemento <style> de esta página como hemos dotado de estilo diferente para este menú anteponiendo ese prefijo a los nombres de clases.
  • esSubmenu es un valor booleano para indicar si es un submenú.

Gestionando el contexto

Un menú contextual debe funcionar precisamente según el contexto. En el primer ejemplo vemos que abrimos el menú sobre un elemento obteniendo detalles de ese elemento. Veámos como está implementado con este código del script en la página:

La función ejecutarAlPresentar() nos permite interceptar el evento oncontextmenu antes de que el menú sea presentado. El objetoEvento se genera con la función recogeEvento() incluida en el módulo general.js. Trae el elemento sobre el que se ejecutó el evento. Si tiene id=="sin-menu" devolvemos false y no presentamos el menú (esto es para el elemento en color azul del ejemplo). En otro caso devolvemos true tras gestionar que grupos podemos ocultar o presentar. En el ejemplo lo gestionamos para presentar las entradas con títulos Ver value y Ver type para elementos <input> y outerHTML y childNodes.length para el resto.

Eventos de ratón, de toque y preventDefault()

En cuando al código del módulo menu-context.js no voy a extenderme en exceso, pues le he puesto algunos comentarios que creo suficientes para entenderlo. Sólo expondré lo relacionado con el hecho de que ciertos eventos como actuar con el botón derecho del ratón son captados por el navegador para presentar su propio menú contextual. Una forma de evitarlo es usando event.prevent.Default(). Así conseguimos captar ese botón derecho del ratón.

Código actualizado a Septiembre 2013. Ver nota más abajo.

En este código se observa el manejador para el evento contextmenu. Para los navegadores que lo soporten interceptamos el evento con preventDefault() para que no presente el menú del navegador. Con IE hemos de devolver evento.returnValue = false. Si el número del botón es 2 abrimos nuestro menú contextual.

Actualización Septiembre 2013

Estoy modificando todos los JavaScript para adaptarlos a los eventos de toque (touch events). Con preventDefault() podemos bloquear el menú contextual en un navegador sin eventos de toque. Pero en los móviles que he podido probar no veo forma de bloquear ese menú cuando tocamos encima de texto y el navegador hace una selección. Sólo he podido conseguirlo si hacemos que los elementos sean no seleccionables. Así se observa en el código anterior que si hay eventos de toque entonces la variable wxG.touch será verdadera y procedemos a insertar la propiedad user-select con valor none. Esa variable wxG.touch se consulta cuando se carga el archivo general.js. Se incorpora user-select usando el gestor de vendor prefixes vpForCss. En otro caso habría que ponerle prefijos pues la mayor parte de navegadores los necesitan.

Menú contextual con HTML 5.1

(Apartado agregado el 25 octubre 2013)

Hay una especificación de HTML 5.1 para crear menús contextuales, pero a fecha de hoy parece que solo lo implementa Firefox 24. Existe alguna diferencia con respecto a la especificación, pues usa el atributo type="context" cuando en la especificación dice que hay que usar type="popup". Podemos declarar el siguiente HTML para crear un menú contextual:

Ejemplo:

Dentro de este contenedor se podrá abrir un menú contextual.

?
Si el navegador no implementa el menú HTML aparecerá undefined. En cambio si lo implementa obtendremos [object HTMLMenuElement], pues este contenedor de color verde tendrá entonces una propiedad contextMenu accesible con JavaScript. Almacena la referencia al elemento menú cuyo ID está referenciado en su atributo contextmenu. Esta característica está definida en la especificación y por tanto nos puede servir para detectar la implementación del menú HTML5.

Este es el código de ese ejemplo:

El elemento <menu> tiene un id="menu-html51" que se corresponde con el atributo contextmenu del <div>. No es necesario que el elemento <menu> esté dentro de ese <div>, puede estar en cualquier lugar de la página. Cuando veámos el menú contextual con el botón derecho del ratón en el navegador, se mostrarán las dos entradas de menú junto al resto del menú contextual que debe mostrar el navegador. Esto, como dije, por ahora funciona en Firefox 24, como se observa en esta captura de pantalla:

Menú contextual HTML5
Menú contextual HTML 5.1 en Firefox 24Imagen no disponible
Menú contextual HTML 5.1 en Firefox 24 (navegador no móvil).

En resumen, es obvio que este nuevo elemento <menu> ofrece buenos resultados con sólo un par de líneas de HTML. Veáse que incluso podemos poner un icono a cada entrada sólo usando el atributo icon. Cuando todos los navegadores implementen esta utilidad resulta razonable pensar en no utilizar JavaScript para crear nuestro propio menú contextual. Pero mientras tanto y también para navegadores más antiguos la utilidad del script menu-context.js sigue siendo válida.