HTML5 - Arrastrar y Soltar Nativo
HTML5 - Arrastrar y Soltar Nativo
HTML5 - Arrastrar y Soltar Nativo
La función de arrastrar y soltar (Drag and Drop, DnD) tiene una gran importancia en HTML5. En la
especificación se define un mecanismo basado en eventos, el API de JavaScript y elementos de marcado
adicionales para indicar que prácticamente cualquier tipo de elemento de una página se pueda
arrastrar. Es difícil tener algo en contra de la compatibilidad nativa de un navegador con una
determinada función. La compatibilidad nativa del navegador con la función DnD permite ofrecer
aplicaciones web más interactivas.
Si hay que utilizar un API, se debe emplear siempre la detección de funciones en lugar de rastrear el
user-agent del navegador. Una de las mejores bibliotecas de detección de funciones es Modernizr.
Modernizr establece una propiedad booleana para cada una de las funciones que comprueba. Por tanto,
la comprobación de la disponibilidad de la función DnD es así de fácil:
if (Modernizr.draganddrop) {
// Browser supports HTML5 DnD.
} else {
// Fallback to a library solution.
}
Para ver un ejemplo, vamos a empezar creando columnas reorganizables. El marcado básico puede ser
así:
<div id="columns">
<div class="column" draggable="true"><header>A</header></div>
<div class="column" draggable="true"><header>B</header></div>
<div class="column" draggable="true"><header>C</header></div>
</div>
Cabe destacar que, en la mayoría de los navegadores, los elementos de anclaje, los elementos de
imágenes y las selecciones de texto que tienen un atributo href se pueden arrastrar de forma
predeterminada. Por ejemplo, al arrastrar el logotipo de google.com, se genera una imagen fantasma:
Con un pequeño toque mágico de CSS3, podemos retocar el marcado para obtener columnas. Si
añadimos cursor: move, los usuarios recibirán una señal visual de que un elemento se puede mover:
<style>
/* Prevent the text contents of draggable elements from being selectable. */
[draggable] {
-moz-user-select: none;
-khtml-user-select: none;
-webkit-user-select: none;
user-select: none;
/* Required to make elements draggable in old WebKit */
-khtml-user-drag: element;
-webkit-user-drag: element;
}
.column {
height: 150px;
width: 150px;
float: left;
border: 2px solid #666666;
background-color: #ccc;
margin-right: 5px;
-webkit-border-radius: 10px;
-ms-border-radius: 10px;
-moz-border-radius: 10px;
border-radius: 10px;
-webkit-box-shadow: inset 0 0 3px #000;
-ms-box-shadow: inset 0 0 3px #000;
box-shadow: inset 0 0 3px #000;
text-align: center;
cursor: move;
}
.column header {
color: #fff;
text-shadow: #000 0 1px;
box-shadow: 5px;
padding: 5px;
background: -moz-linear-gradient(left center, rgb(0,0,0), rgb(79,79,79), rgb(21,21,21));
background: -webkit-gradient(linear, left top, right top,
color-stop(0, rgb(0,0,0)),
color-stop(0.50, rgb(79,79,79)),
color-stop(1, rgb(21,21,21)));
background: -webkit-linear-gradient(left center, rgb(0,0,0), rgb(79,79,79), rgb(21,21,21));
background: -ms-linear-gradient(left center, rgb(0,0,0), rgb(79,79,79), rgb(21,21,21));
border-bottom: 1px solid #ddd;
-webkit-border-top-left-radius: 10px;
-moz-border-radius-topleft: 10px;
-ms-border-radius-topleft: 10px;
border-top-left-radius: 10px;
-webkit-border-top-right-radius: 10px;
-ms-border-top-right-radius: 10px;
-moz-border-radius-topright: 10px;
border-top-right-radius: 10px;
}
</style>
Con el ejemplo anterior, la mayoría de los navegadores crean una imagen fantasma del contenido que
se arrastra. Otros (concretamente, Firefox) requieren el envío de algunos datos en la operación de
arrastre. En la siguiente sección, haremos que nuestro ejemplo de las columnas empiece a ponerse
interesante añadiendo detectores para el procesamiento del modelo de eventos de arrastrar/soltar.
dragstart
drag
dragenter
dragleave
dragover
drop
dragend
Para organizar el flujo de DnD, necesitamos un elemento de origen (en el que se origina el movimiento
de arrastre), la carga de datos (lo que queremos soltar) y un elemento de destino (el área en la que se
soltarán los datos). El elemento de origen puede ser una imagen, una lista, un enlace, un objeto de
archivo, un bloque de HTML o cualquier otra cosa. El elemento de destino es la zona de arrastre (o un
conjunto de zonas de arrastre) donde se aceptan los datos que el usuario intenta soltar. Ten en cuenta
que no todos los elementos pueden ser elementos de destino (por ejemplo, las imágenes).
Este código hará que la opacidad de la columna pase a un 40% cuando el usuario empiece a arrastrarla:
function handleDragStart(e) {
this.style.opacity = '0.4'; // this / e.target is the source node.
}
var cols = document.querySelectorAll('#columns .column');
[].forEach.call(cols, function(col) {
col.addEventListener('dragstart', handleDragStart, false);
});
Resultado:
Como el elemento de destino del evento dragstart es nuestro elemento de origen, si se establece
this.style.opacity en un 40%, el usuario notará que el elemento corresponde al objeto seleccionado que
se está moviendo. No debemos olvidarnos de volver a fijar la opacidad de la columna en el 100% una vez
completada la operación de arrastre. Un lugar bastante obvio para indicarlo es el evento dragend.
Volveremos sobre este punto más adelante.
<style>
.column.over {
border: 2px dashed #000;
}
</style>
function handleDragStart(e) {
this.style.opacity = '0.4'; // this / e.target is the source node.
}
function handleDragOver(e) {
if (e.preventDefault) {
e.preventDefault(); // Necessary. Allows us to drop.
}
function handleDragEnter(e) {
// this / e.target is the current hover target.
this.classList.add('over');
}
function handleDragLeave(e) {
this.classList.remove('over'); // this / e.target is previous target element.
}
Se deben tener en cuenta varios aspectos del código utilizado en este ejemplo:
this/e.target cambia con cada tipo de evento en función del lugar del modelo de eventos de DnD en
el que nos encontremos.
Cuando se arrastra un elemento como un enlace, hay que impedir el comportamiento
predeterminado del navegador, que es abrir la página a la que dirige el enlace. Para ello, se debe
llamar a e.preventDefault() en el evento dragover. Otro sistema que puede funcionar es utilizar
return false en ese mismo controlador. No todos los navegadores lo necesitan, pero no está de más
añadirlo.
El controlador dragenter permite utilizar la clase "over" en lugar de dragover. Con dragover, la clase
CSS se tendría que utilizar muchas veces mientras se siguiera activando el evento dragover al
colocar el ratón sobre una columna. Finalmente, ese procedimiento se traduciría en una gran
cantidad de trabajo innecesario para el renderizador del navegador. Siempre es aconsejable reducir
este tipo de operaciones al mínimo.
En nuestro ejemplo de las columnas no podríamos hacer gran cosa sin el evento drop, pero antes
debemos llevar a cabo una mejora inmediata, que consiste en utilizar dragend para eliminar la clase
"over" de cada columna:
...
function handleDrop(e) {
// this / e.target is current target element.
if (e.stopPropagation) {
function handleDragEnd(e) {
// this/e.target is the source node.
[].forEach.call(cols, function (col) {
col.classList.remove('over');
});
}
Si has seguido detenidamente los pasos del ejemplo que se han descrito hasta ahora, puede que
observes que la columna aún no se puede soltar correctamente. Introduce el objeto DataTransfer.
4. El objeto DataTransfer
La propiedad dataTransfer es el centro de desarrollo de toda la actividad de la función DnD, ya que
contiene los datos que se envían en la acción de arrastre. La propiedad dataTransfer se establece en el
evento dragstart y se lee/procesa en el evento drop. Al activar e.dataTransfer.setData(format, data), se
establece el contenido del objeto en el tipo MIME y se transmite la carga de datos en forma de
argumentos.
dataTransfer también tiene el formato getData necesario para la extracción de los datos de arrastre por
tipo MIME. A continuación se indica la modificación necesaria para el procesamiento de la acción de
arrastre de columna:
function handleDrop(e) {
// this/e.target is current target element.
if (e.stopPropagation) {
Hemos añadido una variable global llamada dragSrcEl para facilitar el cambio de posición de la columna.
En handleDragStart(), la propiedad innerHTML de la columna de origen se almacena en esa variable y,
posteriormente, se lee en handleDrop() para cambiar el HTML de las columnas de origen y destino.
Resultado:
dataTransfer.effectAllowed
Restringe el "tipo de arrastre" que puede realizar el usuario en el elemento. Se utiliza en el modelo
de procesamiento de la operación de arrastrar y soltar para la inicialización de dropEffect durante
los eventos dragenter y dragover. Esta propiedad admite los siguientes valores: none, copy,
copyLink, copyMove, link, linkMove, move, all y uninitialized.
dataTransfer.dropEffect
Controla la información que recibe el usuario durante los eventos dragenter y dragover. Cuando el
usuario coloca el ratón sobre un elemento de destino, el cursor del navegador indica el tipo de
operación que se va a realizar (por ejemplo, una operación de copia, un movimiento, etc.). La
propiedad de efecto admite los siguientes valores: none, copy, link y move.
e.dataTransfer.setDragImage(img element, x, y)
En lugar de utilizar la información predeterminada del navegador (la "imagen fantasma"), puedes
establecer un icono de arrastre.
Nota: Como resultado debería verse el logotipo de Google al arrastrar estas columnas.
5. Arrastre de archivos
Las API de DnD permiten arrastrar archivos del escritorio a una aplicación web en la ventana del
navegador. Como ampliación de este concepto, Google Chrome permite arrastrar objetos de archivo del
navegador al escritorio.
function handleDrop(e) {
e.stopPropagation(); // Stops some browsers from redirecting.
e.preventDefault();
var files = e.dataTransfer.files;
for (var i = 0, f; f = files[i]; i++) {
// Read the File objects in this FileList.
}
}
Puedes consultar una guía completa sobre el proceso de arrastre de archivos del escritorio al navegador
en la sección sobre el uso de la acción de arrastrar y soltar para seleccionar elementos del tutorial de
lectura de archivos locales en JavaScript.
6. Ejemplos
Este sería el producto final, con algún retoque adicional y un contador para cada movimiento:
Lo más interesante de este ejemplo es que las columnas actúan como elemento de origen de la acción
de arrastrar y elemento de destino de la acción de soltar. Es más habitual que los elementos de origen y
destino sean diferentes. Puedes ver una demo en la página html5demos.com/drag.
7. Conclusión
Nadie puede negar que el modelo DnD de HTML5 es algo complejo en comparación con otras soluciones
como JQuery UI. Sin embargo, te recomendamos que aproveches las API nativas del navegador siempre
que puedas. Después de todo, ese es el objetivo de HTML5: ofrecer un amplio conjunto de API
estandarizadas de forma nativa en el navegador. Es posible que las bibliotecas más populares que
incluyen la función DnD acaben ofreciendo compatibilidad nativa con HTML5 de forma predeterminada
y una solución JavaScript personalizada como alternativa.