Posiciones con eventos

Introducción

Posicionamientos con eventos Podemos necesitar la posición (x, y) donde se ejecuta un evento de ratón si luego hemos de ubicar en ese punto la apertura de un mensaje emergente, un menú contextual o cualquier otro componente. Por ejemplo, cuando hacemos click en un elemento podemos obtener la posición con respecto a la esquina superior izquierda de ese mismo elemento. O bien con respecto a la página, que resulta útil para el cometido antes señalado. El objeto event nos ofrece varias posiciones como pageX, pageY, offsetX y offsetY. Pero también hay otros valores y no todos son soportados por los distintos navegadores.

Además event también nos devuelve el elemento donde se originó el evento. Para algunas versiones de Interner Explorer se obtiene con evt.srcElement y para los navegadores más actualizados con evt.target. En definitiva, conviene tener una función que recoga y gestione el evento para obtener valores que funcionen en la mayor cantidad posible de navegadores. Ya tenía la función recogeEvento() en el módulo general.js. Pero no funcionaba bien para algunos posicionamientos con Opera o Explorer. Así que he puesto manos a la obra para modificar esa función y tratar de ajustarla para que funcione igual en los navegadores principales. Pero no dudo que tenga que arreglar algo inesperado que ahora se me escapa. Después de todo, esto de los eventos en JavaScript es una de las cosas más enrevesadas con las que hemos de enfrentarnos, principalmente por el diferente tratamiento de los navegadores.

Practicar con posicionamientos

El siguiente ejemplo sirve para poner en práctica todo lo relacionado con los distintos estilos de posición static, relative, absolute o fixed y los valores de las coordenadas donde se originó un evento. Usaremos el evento onclick sobre elementos <div>. Hay uno con id="externo" y color amarillo que alberga al hijo con id="interno" de color azul. Inicialmente el externo tiene posicionamiento relativo (position: relative) y para el interno es absoluto (position: absolute). Éste tiene una imagen de fondo en forma de dos diagonales que se cruzan en el centro del elemento cuya finalidad es marcar un punto para hacer click y probar los resultados en distintos navegadores.

Estos elementos de prueba no tienen bordes, rellenos o márgenes (border, padding, margin), pero si tienen contorno (outline). El contorno siempre es exterior al borde, digamos que no forma parte de las dimensiones del elemento y así no afectarán a la prueba. Vea que para el elemento amarillo su contorno azul se ubica encima del borde verde del contenedor que agrupa todo el ejemplo. El elemento está en la posición izquierda y superior de ese contenedor verde, inmediatamente después del borde verde. Y su contorno azul se ubica por fuera del elemento y por tanto encima del borde verde. Cuando hacemos click en uno de los dos elementos cambiamos su contorno con línea de puntos roja de 2 píxeles para evidenciar que está seleccionado.

Ejemplo:

Elemento seleccionado
AtributoValor
id
position
Padres del elemento seleccionado
PadreTagNameIdSignificado
parentElement    El padre del elemento tal como está en el DOM
offsetParent    El padre del elemento desde el cual se calculan las posiciones offsetLeft y offsetTop
Posiciones del elemento seleccionado
PropiedadXYPosiciones x,y relativas a la distancia desde la esquina superior izquierda ...
style.left/topdel parentElement tal como está en el DOM
offsetLeft/Top (*)    del offsetParent
(*) Los valores offsetLeft/Top obtenidos para un elemento incluyen el padding y border de ese elemento, pero no su margin.
elemento seleccionado con valores de estilo position, left y top.
Evento click gestionado en recogeEvento()
PropiedadXYPosiciones x,y relativas a la distancia desde la esquina superior izquierda ...
obj.pagx/pagy    de la página
obj.x/y    del elemento
Evento click
PropiedadXYPosiciones x,y relativas a la distancia desde la esquina superior izquierda ...
x/y    del más cercano ascendiente posicionado relativo, sumando el offsetLeft/Top del ascendiente. Si no hay ninguno será de la página.
layerX/Y    del elemento
offsetX/Y    del elemento
pageX/Y    de la página
clientX/Y    del área cliente de la ventana del navegador
screenX/Y    del monitor
Ventana
PosiciónXYSignificado
window.scrollX/Y    Scroll de la ventana

Las posiciones anteriores para el elemento seleccionado obtenidas desde el evento y de la ventana tienen distinto soporte en los navegadores consultados Chrome 25 (CH), Safari 5.1.7 (SA), Firefox 19 (FF), Opera 12 (OP), Internet Explorer 8 (IE8) y 9 (IE9):

PosiciónCHSAFFOPIE8IE9
event.x/y
event.layerX/Y
event.offsetX/Y
event.pageX/Y
event.clientX/Y
event.screenX/Y
window.scrollX/Y

De estos valores no nos sirve de mucho screenX/Y que nos da la posición relativa al monitor. Los valores de offsetX/Y nos dan la posición donde ocurre el evento relativa a la esquina superior izquierda del elemento. En el caso de Firefox hemos de utilizar layerX/Y para ese cometido. De aquí se obtiene el nuevo posicionamiento (x, y) en la función recogeEvento(). El relativo a la página lo podemos obtener de pageX/Y. Pero veámos primero que entendemos por relativo a la página.

Podemos entender el área cliente de la ventana del navegador como el área del navegador destinada a presentar los contenidos de una web. Su ancho y alto cambian si modificamos el tamaño de la ventana del navegador, que además del área cliente incluye las barras de botones y otros elementos propios del navegador. Por otro lado la página puede ser mayor que el área cliente en cuyo caso se activan las barras de desplazamiento o scroll. Si la página es igual o menor que la ventana, los valores pageX/Y y clientX/Y coincidirán al hacer click en un elemento. También sucederá lo mismo si la página es mayor y los scrolles están a cero y el elemento está en la parte superior izquierda de la página, en la superficie que coincide con el área cliente. En otro caso clientX/Y nos dará la posición del evento respecto al área cliente y pageX/Y respecto a la página pues tiene en cuenta el valor de los scrolles.

El scroll de la ventana lo obtenemos con window.scrollX/Y. Es una propiedad de window, no del evento que se está ejecutando sobre un elemento. Pero sumándolo a clientX/Y podemos obtener pageX/Y. Sería una forma de obtener pageX/Y para IE8 que no lo soporta. Pero es que tampoco soporta window.scrollX/Y. Para intentar resolver todo esto tengo la función recogeEvento() del módulo general.js, que tras la última modificación queda así:

El relativo al elemento lo obtenemos de event.offsetX/Y a excepción de Firefox con el que usamos event.layerX/Y. Para el relativo a la página usamos pageX/Y en los navegadores que lo soporten. Para IE8 diferenciamos entre elementos que no sean de bloque y con posición estática, así como para los fijos, tomándolo desde event.x/y. Para otros elementos sumamos event.offsetX/Y y element.offsetLeft/Top. Si hay un offsetParent también le sumamos su separación.

Pruebas

Las pruebas a realizar consistirán en aplicar estilo de posiciones absolute, estatic y fixed al contenedor azul (con id="interno").

Interno (hijo)Externo (padre)Notas
AbsolutoRelativo
EstáticoRelativo/EstáticoSe comporta igual con esos dos posicionamientos para el padre.
FixedRelativo/EstáticoSe comporta igual con esos dos posicionamientos para el padre, pero para IE8 el padre ha de ser estático.

El padre con color amarillo (con id="externo") se mantiene con posición relative en la primera prueba y con relative o static para las otras dos. Esto es porque el posicionamiento relativo se comporta igual que el estático en lo que afecta al propio elemento, pero a efectos de sus hijos sirve de referencia para posicionarlos absolutamente. Hay más combinaciones hijo-padre que podemos hacer con relative, absolute, static y fixed, pero no parece que tales combinaciones puedan ser útiles a la hora de maquetar los contenidos. Un par de ellas son estas:

Interno (hijo)Externo (padre)NotasMotivo de no usar esta combinación
RelativoRelativo/EstáticoSe comporta igual con esos dos posicionamientos para el padre. En este caso se obtienen los mismos valores que en la primera prueba Hijo absoluto - Padre relativo.No veo la utilidad de esta combinación.
AbsolutoEstáticoAl ser el padre estático, el posicionamiento absoluto del hijo se hace en relación a la página. Sus posiciones con respecto a la página serán (75, 75) y no se verá afectado por los scrolles.Si necesitamos un elemento posicionado absolutamente respecto a la página lo pondríamos directamente en el <body> del documento (adquiriendo así la condición de posicionarse con respecto a la página) y no dentro de un contenedor con posición estática.

Posicionamiento absoluto

Usaremos el ejemplo de pruebas anterior tal como aparece al cargarse la página para tomar algunos valores en varios navegadores. El elemento <div id="interno"> (color azul) está inicialmente posicionado absolutamente pues su estilo es position: absolute. Ese posicionamiento es relativo a su padre <div id="externo"> (color amarillo) dado que tiene el estilo position: relative. Si este padre y todos sus ascendientes tuviesen un posicionamiento estático, entonces el posicionamiento del contenedor azul se hace en relación a la página. El elemento azul está posicionado en left="50px" top="50px" en relación con el padre amarillo. Haciendo click exactamente donde se cruzan las diagonales obtenemos los valores (pagx, pagy) y (x, y) a partir de la función recogeEvento() del módulo general.js.

NavegadorDistancia X,Y relativa a la...Scrolles
página (pagx, pagy)elemento (x, y)
Chrome 2585, 111325, 25Indiferente
Safari 5.1.785, 110825, 25Indiferente
Firefox 1985, 111325, 25Indiferente
Opera 1285, 110525, 25Indiferente
IE 885, 111125, 25Indiferente
IE 985, 113225, 25Indiferente

Los valores(pagx,pagy) nos dan la posición del punto donde se activó el evento relativo a la página. Esta medida no es afectada por los scrolles, por lo tanto sea cual sea la posición de los mismos siempre obtendremos la posición correcta respecto a la página. La distancia horizontal es de 85 píxeles en todos los navegadores consultados. Es la suma de 8 píxeles de margen (margin) del <body>, más 2 píxel del borde verde del contenedor que agrupa el ejemplo, más 50 píxeles de la posición left del elemento y más 25 píxeles que es el punto central del elemento: 8+2+50+25 = 85. Los márgenes del elemento <body> son generalmente de 8 píxeles en los navegadores. En Developers Tools de Chrome podemos verlo:

Márgenes en BODY de 8 píxeles

Los otros 8 píxeles de relleno del contenedor <id="contenido"> son el resultado de su estilo padding: 0.5em declarado en el archivo interior.css. Dado que la fuente usada es de 1em que equivale a 16px entonces resultan esos 8 píxeles. En resumen, la separación horizontal respecto a la página es igual en todos los navegadores. Pero la vertical no tiene porque serlo. De hecho si prueba el ejemplo verá que puede obtener valores diferentes. La distancia que separa el contenedor azul desde el inicio de la página depende de múltiples factores. Habrán contenidos previos con posición estática y sin dimensiones cuyo altura se ajustará según el ancho de la ventana, como pueden ser elementos <p> cuya altura se ajusta para mostrar todo el texto. Otro factor es el uso de medidas relativas en em's. Cómo son en relación a la fuente, pueden existir pequeñas diferencias entre navegadores para el tamaño efectivo final de la fuente que esté usando el navegador. Además los navegadores puede aplicar estilos iniciales a los elementos HTML que pueden diferir entre ellos.

Por lo tanto los valores de posición relativos a la página no deben usarse como valores absolutos. Pero eso no nos preocupa cuando lo que nos interesa es captar en que punto exacto se produjo un evento y, por ejemplo, abrir en ese (x,y) relativo a la página una aplicación como podría ser un contenedor emergente (pop-up) o un menú contextual. Estos componentes estarían envueltos en un contenedor posicionado también absolutamente con respecto a la página y los ubicaríamos en el punto (pagx, pagy) donde se produjo el evento aplicándolo en sus propiedades left y top.

La distancia (x, y) respecto al elemento si tiene que ser la misma en todos los navegadores, pues el punto central donde cruzan las dos diagonales está exactamente en (25, 25) respecto de la esquina superior izquierda del elemento. Está en el centro de ese contenedor azul que es de 50×50 píxeles. Es el mismo obligatoriamente para todos los navegadores porque tiene dimensiones específicamente declaradas con el estilo width: 50px y height: 50px.

Posicionamiento estático

Dándole posicionamiento estático al elemento de color azul (id="interno"), ahora es indiferente si el padre id="externo" tiene posicionamiento relativo o estático, pues su hijo no resulta posicionado. Es decir, no se aplican las propiedades left y top (aparecerán los valores vacíos). Estos son los valores que ahora obtenemos cuando hacemos click en el centro de las diagonales:

NavegadorDistancia X,Y relativa a la...Scrolles
página (pagx, pagy)elemento (x, y)
Chrome 2535, 106325, 25Indiferente
Safari 5.1.735, 105825, 25Indiferente
Firefox 1935, 106325, 25Indiferente
Opera 1235, 105525, 25Indiferente
IE 835, 106125, 25Indiferente
IE 935, 108325, 25Indiferente

También en este caso no le afecta la posición de los scrolles. La separación horizontal de 35 píxeles en relación con la página es la suma de 8 píxeles de margen del <body>, más 2 píxeles del borde verde del contenedor que agrupa el ejemplo y más 25 píxeles hasta el punto central de las diagonales: 8+2+25 = 35. Como antes, la separación vertical relativa a la página dependerá del flujo de los contenidos previos. Y la posición respecto al elemento sigue siendo (25, 25), el punto central de las diagonales.

Posicionamiento fijo

Por último podemos darle un posicionamiento fijo al contenedor azul. También ahora es indiferente el posicionamiendo relativo o estático del padre de color amarillo, aunque no para IE8 que debe ser estático, como explicaremos más abajo. El azul se ubicará en la posición (50, 50) píxeles respecto a la página.

NavegadorDistancia X,Y relativa a la...Scrolles
página (pagx, pagy)elemento (x, y)
Chrome 2575, 7525, 250, 0
Safari 5.1.775, 7525, 250, 0
Firefox 1975, 7525, 250, 0
Opera 1275, 7525, 250, 0
IE 875, 7525, 250, 0
IE 975, 7525, 250, 0

Para este posicionamiento si hay que tener en cuenta los scrolles. Esta prueba se ha realizado con los dos a cero. Con esa situación el punto de cruce de las diagonales está en (75, 75) relativo a la página, que es la suma de su separación (50, 50) desde la esquina superior izquierda de la página más los 25 píxeles hasta el punto donde cruzan esas diagonales. Ese punto (25, 25) es de nuevo el relativo al elemento como en los casos anteriores. Vea que ahora la posición es fijada con lo que no le afecta el flujo de contenidos ni siquiera el scroll. El contenedor se mantiene fijado en esa posición. Pero los valores pagx/y, que se obtienen en recogeEvento() a partir de event.pageX/Y, si que tienen en cuenta los scrolles. Puesto que al hacer click en el elemento nos da la posición sobre la página, no sobre el área cliente de la ventana del navegador. Este valor lo podríamos obtener directamente con event.clientX/Y que es soportado por todos los navegadores.

Si el interno tiene posicionamiento fijo se entiende que es fijo respecto a la página. Por eso el navegador debería ignorar el posicionamiento del padre y posicionar el hijo fijándolo en las posiciones declaradas en su left, top. Los valores obtenidos con scrolles a cero serán también (75, 75) en IE8, pero el padre de color amarillo debe tener posición estática. De no ser así se obtienen unos valores que no se corresponden con ese posicionamiento fijo.