soy Kseso y esto EsCSS

NavMenu flotante o ContextMenu alternativo

Nueva colaboración de Furoya con el blog. Artículo dedicado a la elaboración de un menú contextual que se muestra con el botón secundario del ratón y a su control mediante javascriprt.

NavMenu flotante o ContextMenu alternativo

✎ 0
COLABORACIÓN AUTOR INVITADO

Ya vimos en otro artículo cómo poner un menú de navegación de forma que moleste lo menos posible integrándolo en un header que se oculta cuando el documento está quieto (Control del scroll. Eventos y efectos al desplazar la página); pero no es el único método del que disponemos.

Ahora vamos a aprovechar que ya sabemos hacer las cuentas con JS y presentamos otro menú más práctico y menos llamativo, sólo recomendable para sitios donde prima el contenido largo de texto y la fidelidad de los usuarios.

NavMenu flotante o ContextMenu alternativo

Todos conocemos el uso del botón secundario del ratón (en las máquinas que aceptan un maus con al menos dos botones). Sirve para abrir un menú contextual al elemento donde clickeamos. Pero si lo combinamos con alguna tecla podemos hacer que abra nuestro propio menú, que ya no debe ser necesariamente de contexto, sino que puede ser uno de navegación que aparezca donde lo necesitamos y no que esté molestando en la pantalla todo el tiempo.

Para esta demo usé la tecla [Alt] y así abrir un menú alternativo que en principio será una maqueta de relleno, solamente para hacer pruebas. Hay navegadores que ya tienen su propia función asociada a la tecla [Alt] (como poner foco en la barra de menúes), así que el manejo práctico lo verán en cada caso.

See the Pen Custom context menu, or NavMenu (1). by solipsistaCP (@solipsistacp) on CodePen.

El detalle del escript tiene varias novedades:

<script type="text/javascript"> var navbar; //VARIABLE PARA NUESTRO MENÚ /*ASIGNA AL DOCUMENTO EL EVENTO PARA EL MENÚ CONTEXTUAL Y LA LLAMADA A FUNCIÓN*/ document.addEventListener("contextmenu", menu, false); /*ASIGNA AL DOCUMENTO EL EVENTO PARA EL RATÓN Y LA LLAMADA A FUNCIÓN*/ document.addEventListener("mousemove", coordenadas, false); /*FUNCIÓN QUE ABRE NUESTRO MENÚ*/ function menu(eventoContextMenu) { /*SI ABRE MENÚ CONTEXTUAL JUNTO A LA TECLA Alt ...*/ if(eventoContextMenu.altKey) { /*... SE DETIENE EL MENÚ DEL NAVEGADOR ...*/ eventoContextMenu.preventDefault(); /*... Y SE HACE VISIBLE NUESTRO MENÚ*/ navbar.style.visibility = "visible"; } } /*OCULTA NAVBAR*/ function cierra() { navbar.style.visibility = "hidden"; } /*LEE COORDENADAS DEL PUNTERO*/ function coordenadas(eventoPosicion) { /*COORDENADA HORIZONTAL*/ var puntoX = eventoPosicion.clientX; /*COORDENADA VERTICAL*/ var puntoY = eventoPosicion.clientY; // document.title = puntoX+":"+puntoY; /*MUEVE LA NAVBAR A LAS COORDENADAS DEL PUNTERO*/ navbar.style.left = (puntoX)+"px"; navbar.style.top = (puntoY)+"px"; } /*REFERENCIA NUESTRO MENÚ EN LA VARIABLE GLOBAL*/ function inicia() { navbar = document.getElementById("miMenu"); } /*EJECUTA LA FUNCIÓN UNA VEZ CARGADO EL DOCUMENTO*/ onload = inicia; </script>

Seguramente ya probaron la demo y al llevar el menú hasta los bordes notaron un problema que es común a la mayoría de los ejemplos que están en la web para hacer menúes que siguen al puntero. De momento, vamos a ver cómo funciona lo que tenemos, después nos ocupamos del ajuste final, que requiere un poco más de matemática.

  • Como hay varias funciones que apuntan a nuestro menú, empezamos poniendo una variable global que lo identifique y sirva para todas.
  • Después agregamos los escuchadores de evento. Uno para detectar cuándo se hace el click secundario que abre un menú contextual (debería servir también para el teclado, pero no siempre es así) y dispara la función menu que por las características de addEventListener lleva como argumento a la función el mismo evento del click contextual. El otro escuchador detecta el movimiento del ratón, y también manda a la función mueve todo nuevo movimiento en tiempo real.
  • La primera función solamente tiene un condicional para saber si se abre el menú junto con la tecla [Alt] presionada. Ya sabemos que el evento ocurre, porque se disparó la función, pero al agregarle la propiedad .altKey también confirmamos que al mismo tiempo se está presionando la tecla Altern; por lo que se detiene la aparición del menú del navegador con preventDefault() y se hace visible el nuestro.
  • La segunda es nada más que para ocultar nuevamente el menú cuando se haga un mouse-up en él.
  • La última es la que va a leer las coordenadas del puntero, para saber a dónde mover el menú visible. También lo mueve cuando no está visible, pero de momento eso no nos preocupa, porque entonces no lo vemos. Aquí tenemos dos variables que capturan las coordenadas horizontal y vertical del puntero con cada movimiento; y se la pasan a los estilos left y top de nuestro menú para que su punto de inicio coincida con el punto activo del cursor. Esto hace que al moverlo la capa que creamos lo siga en su posición, y es nada más para hacer pruebas.
  • El evento onload y la función inicia() a estas alturas no necesitan explicación.

Insistí mucho en que la demo era un experimento previo. Nos podría ser útil si quisiéramos hacer un falso cursor de imagen propia (sin meterlo en el CSS) o de un elemento que muestre un valor que no se pueda poner en un .cur; pero a nosotros nos interesa más que la navbar simplemente "aparezca" en donde hicimos el click, y que se quede ahí.

En el próximo ejemplo ya vamos a cambiar el evento por uno más práctico, por ahora los invito a mover de nuevo el cartel del menú hasta el borde derecho o al inferior, para ver cómo una parte siempre queda oculta.

Eso es lo que nos va a pasar si hacemos un click secundario + [Alt] para que aparezca ahí, sin que se mueva. El verdadero menú contextual del navegador se acomoda para que siempre quede totalmente visible, y nosotros vamos a tener que hacer lo mismo con el nuestro, o no va a funcionar en cualquier lugar de la ventana.

Centrando el menú, para que siempre se muestre completo

Supongamos que estamos invocando al menú cerca del borde izquierdo del documento, como la nueva capa aparece a partir de las coordenadas del puntero, su borde izquierdo va a coincidir con el punto de click y todo el menú va a quedar a su derecha. Y visible, claro.

Haciendo click en el medio de la pantalla seguramente también va a quedar a la vista, aunque su borde izquierdo coincida con la mitad del area disponible, el menú no puede ser tan ancho como para ocupar más de la mitad derecha del documento.

Ahora, si el click se hace encima del borde derecho, el inicio del menú queda tan cerca que ya no se podrá ver si no se desplaza luego el documento.

Un modo práctico de acomodar esto es que el menú no coincida siempre en su lado izquierdo con el punto de click. Vamos a explicarlo con unas medidas de ancho de 800px para el viewport y 100px para el menú.

Si la coordenada está a la izquierda (x=0) el punto coincide con el lado izquierdo de la capa. Si está por el medio a 400px, que quede en el medio del menú, al que desplazamos a la derecha medio ancho (50px). Si la coordenada es toda a la derecha (x=800px), que el menú también se desplace todo a la izquierda (100px) y el punto de click va a coincidir con su lado derecho.

Éste sería un ejemplo del desplazamiento con mouseover para apreciar cómo un escript compensa la posición de la capa según las coordenadas del puntero, y en cualquier caso queda visible.

See the Pen Custom context menu, or NavMenu (2). by solipsistaCP (@solipsistacp) on CodePen.

Bueno, "en cualquier caso", no. Vamos a ver el código y la explicación, después nos ocupamos de los nuevos problemas.

<script type="text/javascript"> var menu, anchoVentana, altoVentana, anchoMenu, altoMenu; document.addEventListener("contextmenu", menu, false); document.addEventListener("mousemove", coordenadas, false); function menu(eventoContextMenu) { if(eventoContextMenu.altKey) { eventoContextMenu.preventDefault(); menu.style.visibility = "visible"; } } function cierra() { menu.style.visibility = "hidden"; } function coordenadas(eventoPosicion) { var puntoX = eventoPosicion.clientX; var puntoY = eventoPosicion.clientY; var izquierda = anchoMenu / (anchoVentana / puntoX); var arriba = altoMenu / (altoVentana / puntoY); menu.style.left = puntoX+"px"; menu.style.top = puntoY+"px"; menu.style.transform = "translate(-"+izquierda+"px, -"+arriba+"px)"; } function inicia() { menu = document.getElementById("miMenu"); anchoMenu = menu.offsetWidth; altoMenu = menu.offsetHeight; anchoVentana = window.innerWidth; altoVentana = window.innerHeight; menu.style.width = anchoMenu+"px"; menu.style.height = altoMenu+"px"; } onload = inicia; </script>

El código es muy parecido al anterior, solamente le agregamos los nuevos valores que necesitamos para empezar el ajuste fino del proyecto.

  • La función inicia() pone en variables globales no sólo el elemento de menú sino sus medidas junto con las del viewport (todas en pixeles, sin la unidad). Además fija el ancho y el alto del menú para evitar ajustes de linea (wrapping) al acercarse a los bordes.
  • Estos valores sirven a la fórmula que vamos a usar para ubicar a la capa que tiene posición absoluta. Si seguimos suponiendo al ancho visible del documento en 800px y el menú en 100px, la cuenta sería
    ancho menú / (ancho viewport / coordenada horizontal cursor) = traslación menú
       100     / (     800       /            0                ) = 0 
       100     / (     800       /           400               ) = 50 
       100     / (     800       /           800               ) = 100 
    

    Si estoy en la mitad del ancho, el ancho total dividido entre los pixeles en "x" donde está el puntero me va a dar 2 . Si divido el ancho del menú entre 2 me da 50, y si lo desplazo a la izquierda esa medida me va a quedar también justo a la mitad. Y lo mismo pasa con cualquier posición del cursor; si estoy a un cuarto del ancho, el resultado será un cuarto del menú y eso será lo que desplace transform:translate() como valor negativo.
  • Esas operaciones y la traslación se hacen en la función coordenadas(), que cambia de valores con el movimiento del cursor. En la versión final —insisto— ya no va a leer el cambio de posición sino nada más la que tenga el click secundario.

Hasta acá parece que todo funciona bien. Pero hagamos otra prueba. Una vez cerrado el menú (con un click adentro) se puede hacer un doble click en el documento. Entonces el body duplica su altura, y podemos probar mejor el funcionamiento cuando el documento supera el tamaño del viewport y aparecen las barras de desplazamiento.

Abran nuevamente el menú personalizado y desplacen el documento para ver más abajo.

. . .

Exactamente. Todas las mediciones se calcularon sobre el area del viewport porque coincidía con la del documento, pero si el documento es más grande las coordenadas del puntero van a seguir aumentando (en el ejemplo, hacia abajo), y nuestra posición del menú no las va a acompañar porque las cuentas están hechas sobre la altura de la ventana que podemos ver (repito, el viewport).

Bajando el menú, para que al final también quede visible.

Por supuesto que no nos vamos a asustar por ese detalle. Con capturar los pixeles de desplazamiento que tenga el documento y sumárselos a la coordenada correspondiente, ya tenemos hecha la corrección.

La siguiente demo tiene una versión más creíble de documento con una navbar (o navmenu) de enlaces reales.

See the Pen Custom context menu, or NavMenu (3). by solipsistaCP (@solipsistacp) on CodePen.

La parte "corregida" es ésta:

function coordenadas(eventoPosicion) { var puntoX = eventoPosicion.clientX; var puntoY = eventoPosicion.clientY; var izquierda = anchoMenu / (anchoVentana / puntoX); var arriba = altoMenu / (altoVentana / puntoY); /*AQUÍ SE SUMA A LA POSICIÓN DEL CURSOR LA DISTANCIA DESPLAZADA DEL DOCUMENTO*/ menu.style.left = (puntoX + pageXOffset)+"px"; menu.style.top = (puntoY + pageYOffset)+"px"; menu.style.transform = "translate(-"+izquierda+"px, -"+arriba+"px)"; }

Si el documento es más ancho que el viewport y nos desplazamos a la derecha, nuestro pageXOffset cuenta los pixeles que quedan ocultos a la izquierda, y se los sumamos a la posición left del menú para que lo deje más a la derecha, donde estamos mirando.

Lo mismo pasa si es más alto (caso mucho más común) y pageYOffset devuelve los pixeles desplazados arriba, que sumados a la posición vertical del menú lo muestran más abajo, donde nosotros estamos haciendo el click.

Huelga decir que en vez de display: block o none se pueden usar otras reglas combinadas con animaciones para que aparezca o desaparezca el menú; que también puede mostrar diferentes opciones dependiendo del elemento sobre el que hagamos el click.

Pero eso, ya va siendo otra historia.

Autoría

Furoya: Autor del artículo

Artículo original de Furoya.
La intención del autor con sus colaboraciones no es que los artículos sirvan para hacer un copy&paste de códigos sino que comprendas y aprendas la lógica y el cómo trabaja javaScript.
Y a partir de lo expuesto experimentes tú.
El autor del post y el editor del blog te animamos a que plantees tus dudas o reflexiones y que compartas tus realizaciones en base a lo expuesto en los comentarios. Recuerda que puedes incluir pens (ejemplos en Codepen.io) en ellos.