javascript - span - Arrastrar y soltar con precisión dentro de un contenteditable
span contenteditable (2)
Dragon Drop
He hecho una cantidad ridícula de tocar el violín. Entonces, tanto jsFiddling.
Esta no es una solución robusta o completa; Quizás nunca llegue a encontrar uno. Si alguien tiene mejores soluciones, soy todo oídos. No quería hacerlo de esta manera, pero es la única forma que he podido descubrir hasta ahora. El siguiente jsFiddle, y la información que estoy a punto de vomitar, funcionó para mí en esta instancia particular con mis versiones particulares de Firefox y Chrome en mi configuración y computadora particulares de WAMP. No vengas a llorar cuando no funcione en tu sitio web. Esta mierda de arrastrar y soltar es claramente cada hombre por sí mismo.
jsFiddle: Dragon Drop de Chase Moskal
Entonces, estaba aburriendo a mi novia, y ella pensó que seguía diciendo "dragón caído" cuando en realidad, solo decía "arrastrar y soltar". Se atascó, así que eso es lo que llamo mi pequeño amigo de JavaScript que he creado para manejar estas situaciones de arrastrar y soltar.
Resulta que es una pesadilla. La API de arrastrar y soltar HTML5 incluso a primera vista es horrible. Luego, casi te cantas, cuando empiezas a entender y aceptar la forma en que se supone que funciona ... Entonces te das cuenta de lo aterradora que es en realidad, mientras aprendes cómo Firefox y Chrome hacen esta especificación en su propia especial manera, y parecen ignorar por completo todas sus necesidades. Te encuentras haciendo preguntas como: "Espera, ¿qué elemento está siendo arrastrado en este momento? ¿Cómo puedo obtener esa información? ¿Cómo cancelo esta operación de arrastre? ¿Cómo puedo detener el manejo predeterminado único de este navegador en esta situación?" ... Las respuestas a sus preguntas: "¡Estás solo, PERDIÓ! Sigue pirateando cosas, ¡hasta que algo funcione!".
Por lo tanto, así es como realicé Arrastrar y soltar con precisión elementos HTML arbitrarios dentro, alrededor y entre varios elementos contentables. (nota: no voy a profundizar en cada detalle, tendrás que mirar el jsFiddle para eso; simplemente estoy hablando de detalles aparentemente relevantes que recuerdo de la experiencia, ya que tengo un tiempo limitado )
Mi solución
- Primero, apliqué CSS a los juegos de arrastrar (fancybox) - necesitábamos
user-select:none; user-drag:element;
user-select:none; user-drag:element;
en el elegante cuadro, y luego específicamenteuser-drag:none;
en la imagen dentro de la caja de fantasía (y cualquier otro elemento, ¿por qué no?). Desafortunadamente, esto no fue suficiente para Firefox, que requirió que el atributodraggable="false"
se estableciera explícitamente en la imagen para evitar que se pueda arrastrar. - A continuación, apliqué los atributos
draggable="true"
ydropzone="copy"
en contenteditables.
Para los draggables (fancyboxes), ato un manejador para dragstart
. Configuramos DataTransfer para que copie una cadena en blanco de HTML '''' - porque tenemos que engañarlo para que pensemos que vamos a arrastrar HTML, pero estamos cancelando cualquier comportamiento predeterminado. A veces, el comportamiento predeterminado se desliza de alguna manera, y da como resultado un duplicado (como hacemos la inserción nosotros mismos), por lo que ahora el peor error es un "espacio" que se inserta cuando falla un arrastre. No podíamos confiar en el comportamiento predeterminado, ya que fallaría a menudo, por lo que encontré que esta era la solución más versátil.
DD.$draggables.off(''dragstart'').on(''dragstart'',function(event){
var e=event.originalEvent;
$(e.target).removeAttr(''dragged'');
var dt=e.dataTransfer,
content=e.target.outerHTML;
var is_draggable = DD.$draggables.is(e.target);
if (is_draggable) {
dt.effectAllowed = ''copy'';
dt.setData(''text/plain'','' '');
DD.dropLoad=content;
$(e.target).attr(''dragged'',''dragged'');
}
});
A las zonas de descenso, ato un controlador para dragleave
y drop
. El manejador dragleave existe solo para Firefox, como en Firefox, la acción de arrastrar y soltar funciona (Chrome lo niega por defecto) cuando intentaba arrastrarlo fuera del contenteditable, por lo que realiza una comprobación rápida contra relatedTarget
solo de Firefox. Rabieta.
Chrome y Firefox tienen diferentes formas de adquirir el objeto Range, por lo que se tuvo que esforzar para hacerlo de manera diferente para cada navegador en el evento drop. Chrome construye un rango basado en las coordenadas del mouse (sí, eso es correcto) , pero Firefox lo proporciona en los datos del evento. document.execCommand(''insertHTML'',false,blah)
resulta ser la forma en que manejamos el drop. OH, olvidé mencionar que no podemos usar dataTransfer.getData()
en Chrome para obtener nuestro dragstart set HTML, parece ser un extraño error en la especificación. Firefox llama a la especificación en su bullcrap y nos da los datos de todos modos, pero Chrome no lo hace, así que nos volcamos hacia atrás y para establecer el contenido a un nivel global, y pasar por el infierno para matar a todos los comportamientos predeterminados ...
DD.$dropzones.off(''dragleave'').on(''dragleave'',function(event){
var e=event.originalEvent;
var dt=e.dataTransfer;
var relatedTarget_is_dropzone = DD.$dropzones.is(e.relatedTarget);
var relatedTarget_within_dropzone = DD.$dropzones.has(e.relatedTarget).length>0;
var acceptable = relatedTarget_is_dropzone||relatedTarget_within_dropzone;
if (!acceptable) {
dt.dropEffect=''none'';
dt.effectAllowed=''null'';
}
});
DD.$dropzones.off(''drop'').on(''drop'',function(event){
var e=event.originalEvent;
if (!DD.dropLoad) return false;
var range=null;
if (document.caretRangeFromPoint) { // Chrome
range=document.caretRangeFromPoint(e.clientX,e.clientY);
}
else if (e.rangeParent) { // Firefox
range=document.createRange(); range.setStart(e.rangeParent,e.rangeOffset);
}
var sel = window.getSelection();
sel.removeAllRanges(); sel.addRange(range);
$(sel.anchorNode).closest(DD.$dropzones.selector).get(0).focus(); // essential
document.execCommand(''insertHTML'',false,''<param name="dragonDropMarker" />''+DD.dropLoad);
sel.removeAllRanges();
// verification with dragonDropMarker
var $DDM=$(''param[name="dragonDropMarker"]'');
var insertSuccess = $DDM.length>0;
if (insertSuccess) {
$(DD.$draggables.selector).filter(''[dragged]'').remove();
$DDM.remove();
}
DD.dropLoad=null;
DD.bindDraggables();
e.preventDefault();
});
De acuerdo, estoy harto de esto. He escrito todo lo que quiero sobre esto. Lo llamo un día y podría actualizar esto si pienso en algo importante.
Gracias a todos. //Persecución.
La puesta en marcha
Por lo tanto, tengo una div contenta: estoy haciendo un editor WYSIWYG: negrita, cursiva, formateo, lo que sea, y lo más reciente: insertar imágenes extravagantes (en una caja elegante, con un título).
<a class="fancy" href="i.jpg" target="_blank">
<img alt="" src="i.jpg" />
Optional Caption goes Here!
</a>
El usuario agrega estas imágenes de lujo con un cuadro de diálogo con el que las presento: completan los detalles, cargan la imagen y luego, al igual que las demás funciones del editor, utilizo document.execCommand(''insertHTML'',false,fancy_image_html);
para desplegarlo en la selección del usuario.
Funcionalidad deseada
Entonces, ahora que mi usuario puede desplegar una imagen elegante, necesitan poder moverla. El usuario debe poder hacer clic y arrastrar la imagen (caja elegante y todo) para colocarla en cualquier lugar que le plazca dentro del contexto. Necesitan poder moverlo entre párrafos, o incluso dentro de párrafos, entre dos palabras si así lo desean.
Lo que me da esperanza
Tenga en cuenta que, en una versión contenteditable, las antiguas etiquetas <img>
ya han sido bendecidas por el agente de usuario con esta encantadora capacidad de arrastrar y soltar. De manera predeterminada, puede arrastrar y soltar las etiquetas <img>
donde desee; la operación predeterminada de arrastrar y soltar se comporta como uno soñaría.
Entonces, considerando cómo este comportamiento predeterminado ya funciona de manera tan aplastante en nuestros <img>
amigos, y solo quiero extender este comportamiento un poco para incluir un poco más de HTML, esto parece algo que debería ser fácilmente posible.
Mis esfuerzos hasta el momento
Primero, configuré mi etiqueta <a>
con el atributo "draggable" y desactivé "contentable" (no estoy seguro de si es necesario, pero parece que también puede estar desactivado):
<a class="fancy" [...] draggable="true" contenteditable="false">
Entonces, debido a que el usuario aún podía arrastrar la imagen fuera de la elegante caja <a>
, tuve que hacer algo de CSS. Estoy trabajando en Chrome, así que solo te estoy mostrando los prefijos -webkit-, aunque también usé los otros.
.fancy {
-webkit-user-select:none;
-webkit-user-drag:element; }
.fancy>img {
-webkit-user-drag:none; }
Ahora el usuario puede arrastrar toda la caja de fantasía, y la pequeña imagen de representación de clic y arrastre parcialmente descolorida refleja esto: puedo ver que estoy recogiendo todo el cuadro ahora :)
Probé varias combinaciones de diferentes propiedades de CSS, el combo anterior parece tener sentido para mí y parece funcionar mejor.
Tenía la esperanza de que este CSS por sí solo fuera suficiente para que el navegador utilizara todo el elemento como elemento arrastrable, otorgando automáticamente al usuario la funcionalidad con la que he estado soñando ... Sin embargo, parece ser más complicado que eso.
API de arrastrar y soltar JavaScript de HTML5
Este arrastrar y soltar cosas parece más complicado de lo que necesita ser.
Entonces, comencé a profundizar en los documentos api de DnD, y ahora estoy atascado. Entonces, esto es lo que he preparado (sí, jQuery):
$(''.fancy'')
.bind(''dragstart'',function(event){
//console.log(''dragstart'');
var dt=event.originalEvent.dataTransfer;
dt.effectAllowed = ''all'';
dt.setData(''text/html'',event.target.outerHTML);
});
$(''.myContentEditable'')
.bind(''dragenter'',function(event){
//console.log(''dragenter'');
event.preventDefault();
})
.bind(''dragleave'',function(event){
//console.log(''dragleave'');
})
.bind(''dragover'',function(event){
//console.log(''dragover'');
event.preventDefault();
})
.bind(''drop'',function(event){
//console.log(''drop'');
var dt = event.originalEvent.dataTransfer;
var content = dt.getData(''text/html'');
document.execCommand(''insertHTML'',false,content);
event.preventDefault();
})
.bind(''dragend'',function(event){
//console.log(''dragend'');
});
Así que aquí es donde estoy atascado: Esto funciona casi por completo. Casi completamente. Tengo todo funcionando, hasta el final. En el evento drop, ahora tengo acceso al contenido HTML de la caja elegante que estoy tratando de insertar en la ubicación de entrega. Todo lo que necesito hacer ahora es insertarlo en la ubicación correcta.
El problema es que no puedo encontrar la ubicación correcta para colocarla, ni ninguna forma de insertarla. He estado esperando encontrar algún tipo de objeto ''dropLocation'' para volcar mi caja de fantasía, algo así como dropEvent.dropLocation.content=myFancyBoxHTML;
, o tal vez, al menos, algún tipo de valores de ubicación de colocación con los que encontrar mi propia manera de poner el contenido allí? ¿Me dan algo?
¿Lo estoy haciendo completamente mal? ¿Me estoy perdiendo algo por completo?
Traté de usar document.execCommand(''insertHTML'',false,content);
como esperaba, debería ser capaz de hacerlo, pero desafortunadamente me falla aquí, ya que el cursor de selección no se encuentra en la ubicación precisa de la caída como sería de esperar.
Descubrí que si comento todo el event.preventDefault();
''s, el cursor de selección se hace visible, y como es de esperar, cuando el usuario se prepara para soltar, colocando su arrastre sobre el contenteditable, se puede ver la pequeña selección entre caracteres siguiendo el cursor del usuario y la operación de soltar, lo que indica para el usuario que el cursor de selección representa la ubicación de caída precisa. Necesito la ubicación de este cursor de selección.
Con algunos experimentos, probé execCommand-insertHTML''ing durante el evento drop, y el evento dragend - ni inserto el HTML donde estaba dropping-selection-caret, sino que usa cualquier ubicación que fue seleccionada antes de la operación de arrastre.
Debido a que el cursor de selección es visible durante el dragover, tracé un plan.
Por un tiempo, intenté insertar un marcador temporal, como <span class="selection-marker">|</span>
, justo después de $(''.selection-marker'').remove();
, en un intento de que el navegador elimine constantemente (durante dragover) todos los marcadores de selección y luego agregue uno en el punto de inserción, esencialmente dejando un marcador donde quiera que esté ese punto de inserción, en cualquier momento. El plan, por supuesto, era reemplazar este marcador temporal con el contenido arrastrado que tengo.
Nada de esto funcionó, por supuesto: no pude conseguir que el marcador de selección se insertara en el símbolo de selección aparentemente visible como estaba planeado; una vez más, el execCommand-insertedHTML se ubicó dondequiera que estuviera el cursor de selección, antes de la operación de arrastre.
Rabieta. Entonces, ¿qué me he perdido? ¿Cómo se hace?
¿Cómo obtengo o inserto en la ubicación precisa de una operación de arrastrar y soltar? Siento que esto es, obviamente, una operación común entre arrastrar y soltar. Seguramente debo haber pasado por alto un detalle importante y descarado de algún tipo. ¿Incluso tuve que profundizar en JavaScript, o tal vez hay una manera de hacerlo con atributos como draggable, droppable, contenteditable y algunos fancydancy CSS3?
Todavía estoy en la búsqueda, todavía estoy repasando, publicaré tan pronto como sepa lo que he estado fallando :)
The Hunt Continues (edits after original post)
Farrukh publicó una buena sugerencia: uso:
console.log( window.getSelection().getRangeAt(0) );
Para ver dónde está la selección de caret en realidad. Pellizqué esto en el evento de dragover , que es cuando me imagino que la selección de caret está visualmente saltando entre mi contenido editable en el contenteditable.
Desgraciadamente, el objeto Range que se devuelve informa de los índices de compensación que pertenecen al cursor de selección antes de la operación de arrastrar y soltar.
Fue un esfuerzo valiente. Gracias Farrukh.
Entonces, ¿qué está pasando aquí? Me da la sensación de que la pequeña selección que veo saltando, ¡no es la de la selección en absoluto! ¡Creo que es un impostor!
¡Luego de una inspección adicional!
Resulta que es un impostor! ¡El cursor de selección real permanece en su lugar durante toda la operación de arrastre! ¡Puedes ver al pequeño cabrón!
Estaba leyendo MDN Drag and Drop Docs , y encontré esto:
Naturalmente, es posible que deba mover el marcador de inserción alrededor de un evento de dragover también. Puede usar las propiedades clientX y clientY del evento como con otros eventos de mouse para determinar la ubicación del puntero del mouse.
Yikes, ¿significa esto que se supone que tengo que resolverlo solo, basado en clientX y clientY ? ¿Usar las coordenadas del mouse para determinar la ubicación de la selección? ¡¡De miedo!!
Lo investigaré mañana, a menos que yo mismo, o alguien más aquí leyendo esto, pueda encontrar una solución sensata :)
- evento dragstart;
dataTransfer.setData("text/html", "<div class=''whatever''></div>");
- caída de eventos:
var me = this; setTimeout(function () { var el = me.element.getElementsByClassName("whatever")[0]; if (el) { //do stuff here, el is your location for the fancy img } }, 0);
var me = this; setTimeout(function () { var el = me.element.getElementsByClassName("whatever")[0]; if (el) { //do stuff here, el is your location for the fancy img } }, 0);