Arrastrar y soltar con precisión dentro de un contenteditable


La configuración

Por lo tanto, tengo un div contenteditable'm Estoy haciendo un editor WYSIWYG: negrita, cursiva, formato, lo que sea, y más recientemente: insertar imágenes de lujo (en un cuadro de lujo, 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 elegantes con un diálogo con el que las presento: rellenan los detalles, cargan la imagen y luego, al igual que las otras funciones del editor, utilizo document.execCommand('insertHTML',false,fancy_image_html); para introducirlas en la selección del usuario.

Funcionalidad deseada

Así que, ahora que mi usuario puede plop en una imagen elegante need necesitan ser capaces de moverlo alrededor. El usuario debe poder hacer clic y arrastrar la imagen (cuadro de fantasía y todo) para colocarla en cualquier lugar que desee dentro del contenteditable. Necesitan ser capaces de moverlo entre párrafos, o incluso dentro de párrafos between entre dos palabras si quieren.

Lo que me da esperanza{[17]]}

Tenga en cuenta in en un contenido editable, llano viejo <img> las etiquetas ya están bendecidas por el agente de usuario con esto encantadora capacidad de arrastrar y soltar. De forma predeterminada, puede arrastrar y soltar <img> etiquetas alrededor de donde quiera; la operación predeterminada de arrastrar y soltar se comporta como uno soñaría.

Por lo tanto, teniendo en cuenta cómo este comportamiento predeterminado ya funciona de manera tan aplastante en nuestros amigos <img> -- y solo quiero extender este comportamiento un poco para incluir un poco más HTML this esto parece algo que debería ser fácilmente posible.

Mis Esfuerzos Hasta Ahora

Primero, establecí mi fancy <a> etiqueta con el atributo arrastrable, y deshabilitado contenteditable (no estoy seguro de si es necesario, pero parece que también podría estar desactivado):

<a class="fancy" [...] draggable="true" contenteditable="false">

Entonces, debido a que el usuario todavía podía arrastrar la imagen fuera del cuadro de fantasía <a>, tuve que hacer un poco de CSS. Estoy trabajando en Chrome, así que solo te estoy mostrando los prefijos - webkit -, aunque he utilizado los demás también.

.fancy {
    -webkit-user-select:none;
    -webkit-user-drag:element; }
    .fancy>img {
        -webkit-user-drag:none; }

Ahora el usuario puede arrastrar todo el cuadro de fantasía, y la pequeña imagen de representación de clic y arrastre parcialmente desvanecida refleja esto can puedo ver que estoy recogiendo toda la caja ahora:)

He probado varias combinaciones de diferentes propiedades CSS, la combinación anterior parece tener sentido para mí, y parece funcionar mejor.

Esperaba que este CSS por sí solo sería suficiente para que el navegador utilice 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 que.

API de Arrastrar y soltar JavaScript de HTML5

Este Arrastrar y soltar cosas parece más complicado de lo que necesita ser.

Entonces, empecé a profundizar en los documentos de la api DnD, y ahora estoy atascado. Por lo tanto, esto es lo que he amañado (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 a la el contenido HTML de fancy box 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 No puedo encontrar la ubicación correcta de la gota, o cualquier forma de insertar en ella. He estado esperando encontrar algún tipo de 'dropLocation' objeto para volcar mi caja de lujo en, algo como dropEvent.dropLocation.content=myFancyBoxHTML;, o tal vez, al menos, algún tipo de valores de ubicación de caída con los que encontrar mi propia manera de poner el contenido ¿ahí? ¿Se me da algo?

¿Lo estoy haciendo completamente mal? ¿Me estoy perdiendo algo completamente?

Traté de usar document.execCommand('insertHTML',false,content); como esperaba que debería ser capaz de, pero desafortunadamente me falla aquí, ya que el cuadro de selección no se encuentra en el lugar preciso de entrega como esperaba.

Descubrí que si comento todos los event.preventDefault();'s, el cuadro de selección se hace visible, y como uno esperaría, cuando el usuario se prepara para drop, desplazando su arrastre sobre el contenteditable, el pequeño recuadro de selección se puede ver corriendo entre los caracteres que siguen el cursor del usuario y la operación de drop indicating indicando al usuario que el recuadro de selección representa la ubicación precisa de drop. Necesito la ubicación de este cuadro de selección.

Con algunos experimentos, probé execCommand-Inserthtml'ing durante el evento drop, y el evento dragend neither neither insert the HTML where the dropping-selection-caret era, en su lugar utiliza cualquier ubicación fue seleccionado antes de la operación de arrastre.

Debido a que el cuadro de selección es visible durante dragover, he tramado un plan.

Durante un tiempo, estaba intentando, en el evento dragover, insertar un marcador temporal, como <span class="selection-marker">|</span>, justo después de $('.selection-marker').remove();, en un intento por que el navegador constantemente (durante dragover) borre todos los marcadores de selección y luego agregue uno en el punto de inserción essentially esencialmente dejando un marcador donde sea que punto de inserción es, 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 caret de selección aparentemente visible como estaba previsto again de nuevo, el execCommand-insertedHTML se colocó donde estaba el caret de selección, antes de la operación de arrastre.

[36] Huff. Entonces, ¿qué me he perdido? ¿Cómo se hace?

Cómo obtengo, o insertar 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 surely seguramente debo haber pasado por alto un detalle importante y flagrante de algún tipo? ¿Tuve que profundizar en JavaScript, o tal vez haya una manera de hacer esto solo con atributos como arrastrable, droppable, contenteditable y algunos CSS3 de fantasía?

Todavía estoy a la caza still todavía jugueteando around voy a publicar de nuevo tan pronto como yo averigüe lo que he estado fallando en:)


La búsqueda continúa (edita después del post original)


Farrukh publicó una buena sugerencia use uso:

console.log( window.getSelection().getRangeAt(0) );

Para ver dónde está realmente el cuadro de selección. Metí esto en el evento dragover , que es cuando imagino que el cuadro de selección está saltando visiblemente entre mi contenido editable en el contenteditable.

Por desgracia, el objeto de rango que se devuelve, informa índices de desplazamiento que pertenecen al carete de selección antes de la operación de arrastrar y soltar.

Fue un esfuerzo valiente. Gracias Farrukh.

Entonces, ¿qué está pasando aquí? Estoy teniendo la sensación de que el pequeño cuadro de selección que veo saltando alrededor, no es el cuadro de selección en absoluto! Creo que es un impostor!

Después De Una Inspección Adicional!

Resulta que es un impostor! El real cuidado de selección permanece en su lugar durante toda la operación de arrastre! Puedes ¡mira al pequeño cabrón!

Estaba leyendo MDN Drag and Drop Docs , y encontré esto:

Naturalmente, es posible que también necesite mover el marcador de inserción alrededor de un evento de arrastre. Puede usar las propiedades clientX y clientY del evento como con otros eventos del ratón para determinar la ubicación del puntero del ratón.

Yikes, ¿significa esto que se supone que debo averiguarlo por mí mismo, basado en clientX y clientY?? Usando coordenadas del ratón para determinar la ubicación de la selección caret yo mismo? De miedo!!

Miraré en hacerlo mañana unless a menos que yo mismo, o alguien más aquí leyendo esto, pueda encontrar una solución sana:)

Author: ROMANIA_engineer, 2013-02-04

2 answers

Gota de Dragón

He hecho una cantidad ridícula de jugueteo. Así, tanto jsFiddling.

Esta no es una solución robusta o completa; puede que nunca se me ocurra una. Si alguien tiene alguna solución mejor, soy todo oídos-yo no quiero tener que hacerlo de esta manera, pero es la única manera que he podido descubrir hasta ahora. El siguiente jsFiddle, y la información que estoy a punto de vomitar, funcionó para mí en este caso en particular con mis versiones particulares de Firefox y Chrome en mi configuración WAMP particular y la computadora. No vengas llorando a mí cuando no funciona en tu sitio web. Esta basura de arrastrar y soltar es claramente sálvese quien pueda.

jsFiddle: La caída del Dragón de Moskal

Entonces, yo estaba aburriendo los sesos de mi novia, y ella pensó que seguía diciendo "dragon drop" cuando en realidad, solo estaba diciendo "arrastrar y soltar". Se pegó, así que eso es lo que llamo mi pequeño amigo JavaScript He creado para manejar estas situaciones de arrastrar y soltar.

Resulta que it es una pesadilla. La API de arrastrar y soltar HTML5, incluso a primera vista, es horrible. Entonces, casi te calientas, a medida que empiezas a entender y aceptar la forma en que se supone que funciona.. Entonces te das cuenta de lo aterradora pesadilla que es en realidad, a medida que aprendes cómo Firefox y Chrome van sobre esta especificación en su propia manera especial, y parecen ignorar por completo todos sus requiere. Te encuentras haciendo preguntas como: "Espera, ¿qué elemento está siendo arrastrado en este momento? ¿Cómo obtengo esa información? ¿Cómo cancelo esta operación de arrastre? ¿Cómo puedo detener el manejo único predeterminado de este navegador en particular de esta situación?"... Las respuestas a tus preguntas: "¡Estás por tu cuenta, PERDEDOR! ¡Sigue hackeando cosas, hasta que algo funcione!".

Así que, así es como logré Arrastrar y Soltar con precisión Elementos HTML Arbitrarios dentro, alrededor y entre múltiples contenidos editables. (nota: No voy a entrar completamente en profundidad con cada detalle, tendrás que mirar el jsFiddle para eso just Solo estoy divagando detalles aparentemente relevantes que recuerdo de la experiencia, ya que tengo un tiempo limitado)

Mi solución

  • Primero, apliqué CSS a los draggables (fancybox) needed necesitábamos user-select:none; user-drag:element; en el fancy box, y luego específicamente user-drag:none; en la imagen dentro del fancy box (y cualquier otro elemento, ¿por qué no?). Desafortunadamente, esto no fue suficiente para Firefox, que requería que el atributo draggable="false" se estableciera explícitamente en la imagen para evitar que fuera arrastrable.
  • A continuación, apliqué los atributos draggable="true" y dropzone="copy" en los contenteditables.

A los draggables (fancyboxes), enlazo un manejador para dragstart. Configuramos la transferencia de datos para copiar una cadena en blanco de HTML '' because porque necesitamos engañarlo para que piense que vamos a arrastrar HTML, pero estamos cancelando cualquier valor predeterminado comportamiento. A veces el comportamiento predeterminado se desliza de alguna manera, y resulta en un duplicado (como hacemos la inserción nosotros mismos), por lo que ahora el peor fallo es un '' (espacio) que se inserta cuando falla un arrastre. No podíamos confiar en el comportamiento predeterminado, ya que fallaría a menudo, así que encontré que esta es 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 gota, ato un manejador para dragleave y drop. El controlador dragleave existe solo para Firefox, como en Firefox, el arrastrar y soltar funcionaría (Chrome denies you by default) cuando intentaste arrastrarlo fuera del contenteditable, por lo que realiza una comprobación rápida contra el solo Firefox relatedTarget. [18] Huff.

Chrome y Firefox tienen diferentes formas de adquirir el objeto de rango, por lo que el esfuerzo tuvo que ser puesto en hacerlo de manera diferente para cada navegador en el evento drop. Chrome construye un rango basado en coordenadas del ratón (sí, eso es correcto) , pero Firefox lo proporciona en los datos del evento. document.execCommand('insertHTML',false,blah) resulta ser cómo manejamos la entrega. OH, olvidé mencionar we no podemos usar dataTransfer.getData() en Chrome para obtener nuestro conjunto de dragstart HTML appears parece ser algún tipo de error extraño en la especificación. Firefox llama a las especificaciones en su bullcrap y nos da los datos de todos modos Chrome pero Chrome no lo hace, por lo que nos inclinamos hacia atrás y para establecer el contenido a un global, y pasar por el infierno para matar a todo el comportamiento por defecto...

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();
});

Vale, estoy harto de esto. He escrito todo lo que quiero sobre esto. Lo estoy llamando un día, y podría actualizar esto si pienso en algo importante.

Gracias a todos. //Perseguir.

 39
Author: ChaseMoskal,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2013-02-06 06:27:15
  1. evento dragstart; dataTransfer.setData("text/html", "<div class='whatever'></div>");
  2. 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);
 0
Author: holistic,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-08-10 09:34:34