soy Kseso y esto EsCSS

Arrastrando sin arrastrar. Control de elementos por arrastre de cursor sobre un pad

Cómo crear áreas donde desplazar el puntero o el dedo y que ese movimiento cambie un valor. No siempre será un arrastre propiamente dicho, a veces puede ser un mouseover. Incluye algunos ejemplos que solamente sirven para casos muy particulares, pero que si alguien debe resolverlos, no tiene muchas otras opciones prácticas.

Arrastrando sin arrastrar. Control de elementos por arrastre de cursor sobre un pad

✎ 0
COLABORACIÓN AUTOR INVITADO
Arrastrando sin arrastrar. Control de elementos por arrastre de cursor sobre un pad

Todos conocemos las barras con botones deslizables, como las scrollbars, las trackbars o los input type=range, donde podemos arrastrar un elemento (virtual o real) para cambiar un valor con bastante precisión; aunque su punto fuerte sea el control del movimiento, y su punto más débil la limitación del rango.

En usos como el desplazamiento de una página, el límite lo pone el mismo documento con su tamaño, y no habrá necesidad de exceder ese punto. Pero puede ocurrir que precisemos un botón que nos deje aumentar o disminuir un valor hasta extremos que en principio desconocemos. Y entonces aparecen las dragbars, donde no se arrastra un botón sino simplemente el cursor o el dedo sobre la barra, y dependiendo de su dirección el valor va a aumentar o disminuir, pero ya sin ningún límite.

Trackbar libre, sin botón

Éste es un ejemplo típico. Arrastrando el cursor hacia la derecha sobre la barra gris la imagen que tiene debajo va a aumentar su tamaño. Si en el arrastre "se nos termina la barra", simplemente volvemos al comienzo y arrastramos de nuevo. Hacia la izquierda la imagen se achica, pero aquí suele haber un límite para que el ejemplo no desaparezca.

See the Pen Endless dragbar. by solipsistaCP (@solipsistacp) on CodePen.

En el CSS y el HTML ponemos algunas reglas y atributos para evitar que durante el arrastre se "seleccione" el documento, pero esta vez prácticamente todo el mecanismo es javascript.

/* VARIABLES INICIALES */ var imagen, boton, refX = 0; var Xcremento = 2; //PIXELES DE INCREMENTO O DECREMENTO /* FUNCIÓN QUE SE EJECUTA AL CARGAR LA PÁGINA */ window.addEventListener("load", function(){ /* REFIERE A LA IMAGEN QUE CAMBIARÁ DE TAMAÑO */ imagen = document.querySelector("figure>img"); /* CONFIRMA EN UNA VAR SI SE APRETÓ EL BOTÓN DEL MAUS */ document.querySelector("#barra").addEventListener("mousedown", function(){boton=1}); /* CONFIRMA EN ESA VAR SI SE LIBERÓ EL BOTÓN ... */ document.querySelector("#barra").addEventListener("mouseup", function(){boton=0}); /* ... O SI EL PUNTERO SALIÓ DE LA BARRA */ document.querySelector("#barra").addEventListener("mouseout", function(){boton=0}); /* EJECUTA UNA FUNCIÓN SI EL PUNTERO SE MUEVE SOBRE LA BARRA */ document.querySelector("#barra").addEventListener("mousemove", arrastra); }, false); /* FUNCIÓN QUE CAMBIA EL TAMAÑO DE LA IMAGEN */ function arrastra(evento) { /* POSICIÓN DEL CURSOR */ var puntoX = evento.clientX; /* SI EL BOTÓN DEL MAUS ESTÁ APRETADO */ if(boton == 1) { /* Y SI SU POSICIÓN ES MAYOR A LA DE REFERENCIA */ if(puntoX > refX) { // document.title = puntoX+">"; /* SE AUMENTA EL ANCHO DE LA IMAGEN */ imagen.width = imagen.offsetWidth + Xcremento; } /* DE OTRA FORMA, SI LA POSICIÓN ES MENOR */ else if(puntoX < refX) { // document.title = puntoX+"<"; /* SE REDUCE EL ANCHO DE LA IMAGEN */ imagen.width = imagen.offsetWidth - Xcremento; } /* SE ACTUALIZA EL PUNTO DE REFERENCIA */ refX = puntoX; } }

La imagen contiene la palabra "PIXEL" escrita con la tipografía "3x3" diseñada por Anders de Flon, y tiene puestas algunas reglas CSS que impiden el "borroneo" del dibujo al aumentar de tamaño. Estas reglas no están aún correctamente implementadas, o son sencillamente de sintaxis propietaria, pero de momento y para algunos navegadores nos van a servir.

  • Creamos las variables globales iniciales, incluyendo una que contiene una referencia para la posición del puntero al arrastrar; y otra que podemos modificar según el caso, ya que es la cantidad de pixeles que queremos aumentar o disminuir en cada paso.
  • Empezamos escribiendo un escuchador del evento de carga de todo el documento, que ejecuta una función anónima donde asignamos la referencia a la imagen en su variable y aplicamos los demás escuchadores de evento para la barra, que nos van a devolver las condiciones conque estemos trabajando: si bajamos el botón del ratón, se asigna un valor a su variable; si lo levantamos o salimos de la barra, se cambia por otro; y si movemos el puntero sobre la barra, se ejecuta la siguiente función.
  • Como ya sabemos, esta función recibe como primer argumento el mismo evento (mousemove) y con clientX leemos las coordenadas horizontales en cada movimiento del cursor.
  • Esta posición se lee todo el tiempo que pasemos sobre la barra, pero solamente la vamos a aprovechar si además está apretado el botón primario del ratón; y para eso creamos los eventos que cambian el valor de la variable boton, porque con una flag condicional confirmamos si vale '1' y entonces cambiamos el tamaño de la imagen.
  • Para eso usamos el valor de referencia refX que creamos al comienzo, que como es '0' siempre va a ser menor a la coordenada X del puntero, pero de cualquier forma con otro condicional comparamos ambos valores y si el que envía el ratón es mayor significa que se está moviendo hacia la derecha. Entonces aumentamos el tamaño de la imagen según el incremento que tenemos elegido.
  • Si el puntero se mueve a la izquierda, el primer valor será mayor, pero inmediatamente (al reescribir la variable) ya se detecta con el otro condicional que empieza a ser mas chico que el valor previo, por lo que se repite el mecanismo anterior, pero ahora decrementando el ancho de la imagen. (Existe una propiedad que nos ahorra muchos cálculos para saber a qué lado se está moviendo el cursor, es movementX y se supone que ya está "recomendada", pero aún hay navegadores que no le dan soporte o lo hacen con algún código propietario, así que vamos a seguir usando el método clásico. )
  • En cualquier caso, actualizamos la variable refX con la nueva posición para usarla en el siguiente movimiento.

Ni hablar que el método funciona para todo formato que use valores numéricos.

Pad + button (combinados)

Como imaginarán, no es el único modo de cambiar valores sin límite. Esta versión es un poco más extraña y usa un botón que además de servir como "pad" para el movimiento del cursor, permite cambiar la dirección con un click. Ya no hay un arrastre, sino simplemente un mousemove continuo para mantener el incremento o el decremento de un valor.

Una vez que prueben mover el puntero sobre el botón, haciendo click para cambiar la dirección del globo o para detenerlo, vemos el código.

See the Pen Button + cursor pad. Change values by movement. by solipsistaCP (@solipsistacp) on CodePen.

La imagen del globo del pen previo y la inicial del artículo es gentileza de "Internet Archive Book Images"

var pasoNubes = 1999; // VALOR ARBITRARIO PARA INICIO DE FONDO var pasoBoton = 1; // VALOR INICIAL PARA CALCULAR ÍNDICE DEL ÍCONO EN BOTÓN var icoBoton = ["▬", "▲", "▬", "▼"]; // SERIE DE ÍCONOS PARA EL BOTÓN /* CAMBIA EL ÍCONO DEL BOTÓN CON CADA CLICK */ function cambiaIco(T) { /* PONE DENTRO UNO DE LOS ELEMENTOS DE ARRAY, SEGÚN SEA EL RESTO */ T.innerHTML = icoBoton[pasoBoton%4]; /* AUMENTA LA VARIABLE PARA CAMBIAR EL RESTO EN LA DIVISIÓN */ pasoBoton++; } /* SIMULA EL ASCENSO Y DESCENSO DEL GLOBO */ function flota(T) { /* SI EL ÍCONO ES UNA FLECHA ABAJO ... */ if(T.innerHTML == "▼") { /*... REDUCE EL VALOR DE LA VARIABLE */ pasoNubes -= 1.33; } /* SI EL ÍCONO ES LA FLECHA ARRIBA ... */ else if(T.innerHTML == "▲") { /*... AUMENTA EL VALOR DE LA VARIABLE */ pasoNubes += 1.33; } /* DE OTRO MODO (ES EL GUIÓN), ... */ else { /*... MANTIENE EL VALOR */ pasoNubes = pasoNubes; } /* SI EL ÍCONO NO ES UN GUIÓN (ES UNA FLECHA) */ if(T.innerHTML != "▬"){ /* SE USA LA VARIABLE ANTERIOR PARA MODIFICAR LA POSICIÓN DEL FONDO */ document.querySelector("main").style.backgroundPosition = "40% "+(41+pasoNubes).toFixed(2)+"%, 5% "+(31+pasoNubes).toFixed(2)+"%, 70% "+(23+pasoNubes).toFixed(2)+"%"; } //document.title = pasoNubes; }

Es tremendamente simple. Se aumenta o disminuye un valor mientras se mueve el puntero en el botón (sin arrastrar), y ese valor se pasa a una regla CSS de algún elemento, que para el caso son las capas de fondo en el body. Allí modificamos el "top" de las nubes, y con una operación diferente en cada capa simulamos el paralax.

  • Empezamos declarando tres variables. La primera tiene un valor elegido de manera arbitraria, puede ser cualquiera y a mí se me ocurrió ése. La segunda tiene que ver con el ícono del botón, y lo explico más abajo. La tercera es un array que contiene esos íconos en orden de aparición, como entre cada flecha debe detenerse la animación, entonces están alternados una flecha y un guión.
  • La primera función es justamente para "rotar" el ícono del botón con cada click. En el HTML ya ponemos un "—" que indica un stop, y que en el array es el ítem cero. Por eso la variable pasoBoton es 1; para que al hacer click ponga el siguiente caracter de la serie. El cálculo se hace según el módulo de la división entre esa variable y la cantidad de elementos de array (para nuestro caso, 4). Resulta que si dividimos 1; 2 ó 3 entre 4 no es posible obtener un resto, así que nos devuelve el mismo valor; por eso los primeros clicks nos van a traer esos mismos números, pero vemos que en la siguiente línea se incrementa la variable en uno, por lo que al al tercer click ya vale 4; y entonces sí hay un resto, que va a ser cero. Es decir, que ya vemos cómo funciona el método: una vez que llegamos a 4 cada click va a hacer que el resto aumente en 1 y al llegar a un nuevo múltiplo de 4 va a devolver un cero. De esa forma si aumentamos la variable infinitamente, siempre vamos a obtener como resultado la serie 1; 2; 3; 0; 1; 2; ... Que coinciden con los ítems del array para recomenzar la serie ciclicamente.
  • Una vez cambiado el ícono, lo vamos a usar como referencia directa para la función que simula la flotación ascendente y descendente. Con unos condicionales confirmamos si dentro del botón hay una flecha hacia abajo, y entonces reducimos la variable pasoNubes una cantidad elegida según el diseño que usemos (en este caso puse 1.33), si el ícono es una flecha hacia arriba, el valor se aumenta, y si no hay flecha, se queda igual y el globo no se moverá más que por el vaivén que tiene en el CSS.
  • Para crear la ilusión del desplazamiento vertical, en la siguiente línea sumamos pasoNubes a algunos valores diferentes entre sí y los pasamos como posición "Y" de cada capa de fondo, que son los dibujos de nubes hechos con gradientes. Éstas van a desplazarse mientras movamos el cursor dentro del botón con una flecha, aumentando su distancia al top si es "▲" o disminuyendo si es "▼", o se detendrán si es "▬". Los valores para aplicar al CSS están en porcentaje, y los redondeamos a dos dígitos por prolijidad (en la mayoría de los casos el resultado de las operaciones va a ser un decimal periódico).
Por supuesto que esta versión también funciona con cualquier formato que use valores numéricos, o quizá arrays con valores no-numéricos. ;-)

Potenciómetro, que sí se arrastra

El último ejemplo ya no va a ser un control de arrastre sinfín. O sí, pero con otra lógica. Vamos a ensayar un mecanismo para botones circulares, pero de rotación, como los clásicos potenciómetros para subir o bajar el nivel de algo.

En este caso tenemos diez pasos, que aumentan arrastrando la perilla en sentido horario. Existen versiones que siguen aumentando mientras se mantenga la dirección del giro, y tendría más que ver con los ejemplos precedentes, pero sólo es práctico en el mundo real; andar moviendo el puntero en círculos siguiendo la marca del botón virtual es de lo más incómodo.

Así que éste trabaja como los de verdad, salvo por el detalle de que al llegar al máximo, si seguimos girando salta al mínimo y empieza de nuevo. Y lo mismo si giramos en sentido antihorario: llega a cero y salta a nueve.

See the Pen Potentiometer. by solipsistaCP (@solipsistacp) on CodePen.

Para esta clase de efectos yo soy un convencido del mapeo de imágenes, especialmente porque no se leen las coordenadas sino que se pasa el cursor por áreas que deberían tener forma de porción de torta (algo fácil de hacer con shape=poly); pero es un método que se está dejando de usar, así que nos vamos a complicar con elementos acomodados en círculo cada uno con su correspondiente mouseover.

Como observarán, ajustar el funcionamiento requiere de un montón de eventos más en diferentes partes del botón, y hasta de variables de control que se terminan cambiando por tiempo antes que por evento. Casi resulta más fácil eliminar el arrastre y dejar que funcione solamente con el mouseover, pero alguien se va a quejar porque tiene que retirar el puntero hacia el borde siguiendo la línea del radio para no cambiar a otro valor al salir.

En fin, lo pueden adaptar quitando lo que sobre y así eligen por ustedes mismos.

/* VARIABLES INICIALES */ var perilla, barra, desactiva, boton = 0; /* FUNCIÓN DE INICIO */ function rotativa() { /* REFIERE AL CONTENEDOR */ var pote = document.querySelector("#pote"); /* ALTURA O DIÁMETRO DE LA PERILLA */ var diamPote = pote.offsetHeight; /* CÁLCULO A OJÍMETRO PARA POSICIÓN DE CADA CAPA ACTIVA */ var calculado = diamPote/2.66; /* REFIERE AL DIV QUE SIMULA UNA PERILLA */ perilla = document.querySelector("#pote div"); /* REFIERE A LA BARRA DE PROGRESO */ barra = document.querySelector("#escala"); /* AGREGA EVENTO PARA SABER CUÁNDO SE "SUELTA LA PERILLA" */ pote.addEventListener("mouseup", function(){boton=0}, false); /* LOOP PARA CREAR LAS 10 CAPAS ACTIVAS */ for(i=0; i<10; i++) { /* CALCULA RADIANES DE A 36° */ var radian = 2*Math.PI*(i/10); //EL 10 ES POR LA CANTIDAD DE CAPAS /* CREA CADA CAPA */ var puntoAct = document.createElement("span"); /* AGREGA UN EVENTO PARA SABER CUÁNDO SE EMPIEZA A ARRASTRAR */ puntoAct.setAttribute("onmousedown","boton=1"); /* AGREGA UN EVENTO PARA SABER SOBRE CUÁL CAPA SE MUEVE */ puntoAct.setAttribute("onmousemove","mide("+ i +")"); //puntoAct.setAttribute("draggable","false"); /* CÁLCULO A OJÍMETRO DEL TAMAÑO DE LA CAPA */ puntoAct.style.height = diamPote/4 +"px"; puntoAct.style.width = diamPote/4 +"px"; /* CÁLCULO DE POSICIÓN SIGUIENDO UN CÍRCULO */ puntoAct.style.top = (Math.cos(radian)*calculado)+calculado +"px"; puntoAct.style.left = -(Math.sin(radian)*calculado)+calculado +"px"; /* AGREGA LA CAPA A SU CONTENEDOR */ pote.appendChild(puntoAct); } } /* LEE EL VALOR DE LA CAPA Y GIRA LA PERILLA */ function mide(V) { // document.title=boton; /* CANCELA EL VALOR DE FIN DE ARRASTRE */ clearTimeout(desactiva); /* SI SE ESTÁ ARRASTRANDO ... */ if(boton==1) { /* ...LA PERILLA SE ROTA PARA QUE COINCIDA CON EL PUNTERO */ perilla.style.transform = "rotate("+ V*36 +"deg)"; /* Y SE PASA SU ARGUMENTO A LA BARRA DE PROGRESO */ barra.value = V; /* INICIA EL CONTADOR QUE DESACTIVA EL VALOR DE ARRASTRE */ desactiva = setTimeout(function(){boton=0}, 1000); } } /* CARGA LA FUNCIÓN DE INICIO */ onload = rotativa;

Desde ya les adelanto que no voy a explicar la mayoría de las cuentas que hace el escript. Son matemática, y especialmente trigonometría —que no es mi fuerte para nada— y no tienen tanto que ver con la mecánica para el efecto creado. Solamente sirven para poner las capas en "círculo" y que coincidan con el recorrido del botón para el arrastre. De cualquier forma, si van a cambiar las medidas o la cantidad de capas, con hacer un par de prueba/errores lo van a entender en seguida. Como dije, son matemática y no hay tanto de javascript.

  • Empezamos siempre con las variables globales, y una de referencia para saber si se está apretando el botón del maus, que inicia en cero.
  • La función de inicio escribe todas las capas activas para saber en qué posición está el cursor.
  • Hacemos una referencia al contenedor y otra a su altura, que será el diámetro de la perilla. También a un valor relativo al tamaño del cuadrado contenedor y la distancia a los 0° desde donde empezar a ubicar cada capa activa en círculo. Otra variable apunta al elemento redondo que será la perilla y la última a la barra que sirve para confirmar el cambio de valor. Antes que nada, agregamos un escuchador de evento para saber cuando se suelta el botón del maus dentro del conjunto.
  • Y empezamos el bucle para crear las 10 capas para los puntos activos. Lo primero es convertir a radianes cada 36°; esos 36 salen de los 360° entre las 10 áreas que vamos a crear para que detecten la posición del puntero (si queremos más sensibilidad, podemos usar 20 capas a 18°), y los radianes es porque las funciones que usamos más abajo reconocen esa unidad en vez de los grados.
  • Luego creamos cada capa y le asignamos los eventos para saber cuándo levantamos el botón sobre ella (y cambiamos la variable de referencia a cero) y cuándo le movemos el puntero encima y así disparar una función que contiene el argumento para incrementar.
  • El tamaño de los cuadraditos activos es bastante subjetivo, pero en principio deben estar cada uno pegado o superpuesto parcialmente al siguiente. Que tengan de lado la cuarta parte del contenedor me pareció una buena medida. Los podemos hacer redondos con CSS, pero eso también es un gusto personal.
  • Ahora hay que ubicarlos en ronda por coordenadas, y aprovechamos que existen funciones para seno y coseno que devuelven esas distancias por radián y se ajustan con los valores elegidos para el caso y que así queden "centrados". Recordemos que el punto de inicio no es el centro de cada cuadrado sino la esquina superior izquierda.
  • Por último, agregamos cada cuadrito al final del contenedor.
  • La siguiente función es la que se ejecuta al pasar sobre cada capa activa, y sus argumentos crecen en sentido horario. Con CSS se rotó el conjunto para que el inicio no quedara al norte y el diseño se pareciera más a un potenciómetro real.
  • Empezamos desactivando un contador que en 1 segundo cancela el valor para saber si se hizo un mousedown; en la primera vuelta no habrá ningún setTimeout asignado, pero a partir de la siguiente ya va a ser necesario, porque no podemos confiar solamente en los eventos de mouseout o mouseup, y 1000ms es un valor razonable de espera. (En realidad, en lo que no podemos confiar es en el movimiento prolijo y uniforme de quien use el ratón; ni en Chrome, que es bastante arisco para seguir al puntero :-P .)
  • Entonces, si aún está el botón abajo, se rota la perilla propiamente dicha tantos grados como den el valor elegido multiplicado por los 36 grados de cada paso, y así su marca va a coincidir con el lugar del puntero. Luego se pasa el valor del argumento al artlugio que querramos modificar (que para nuestro caso es el valor de una barra de nivel), ya que todo esto es justamente para pasar este valor a algún lado.
  • Justo al final de la función ponemos el contador de tiempo para "desactivar" el mousedown de prepo, hasta que se baje el botón de nuevo.
  • Terminamos, como de costumbre, con un evento que ejecuta la función de inicio al cargar el documento.

Existen más maneras de crear controles sinfín, como el spin button que ya está incorporado a los input type=number, por ejemplo. Pero de momento nos quedamos con estos.

Para cualquier ocurrencia, hay un espacio de comentarios aquí abajo.

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.