web-dev-qa-db-fra.com

Glisser-déposer précis dans un contenu modifiable

La mise en place

Donc, j'ai un div contentable - je fais un éditeur WYSIWYG: gras, italique, formatage, et plus récemment: insérer des images fantaisies (dans une boîte fantaisie, avec une légende).

<a class="fancy" href="i.jpg" target="_blank">
    <img alt="" src="i.jpg" />
    Optional Caption goes Here!
</a>

L'utilisateur ajoute ces images fantaisistes avec une boîte de dialogue que je leur présente: elles remplissent les détails, téléchargent l'image, puis, tout comme les autres fonctions de l'éditeur, j'utilise document.execCommand('insertHTML',false,fancy_image_html); pour la replacer dans la sélection de l'utilisateur .

Fonctionnalité souhaitée

Donc, maintenant que mon utilisateur peut se plonger dans une image de fantaisie - il doit pouvoir la déplacer. L'utilisateur doit pouvoir cliquer et faire glisser l'image (boîte de fantaisie et tout) pour la placer où bon lui semble dans le contenu éditable. Ils doivent pouvoir le déplacer entre les paragraphes, ou même à l'intérieur des paragraphes - entre deux mots s'ils le souhaitent.

Ce qui me donne de l'espoir

Gardez à l'esprit - dans une ancienne balise contentable modifiable <img> sont déjà bénis par l'agent utilisateur avec ce joli glisser-déposer capacité de goutte. Par défaut, vous pouvez glisser-déposer <img> balises partout où vous le souhaitez; l'opération de glisser-déposer par défaut se comporte comme on pourrait le rêver.

Donc, compte tenu de la façon dont ce comportement par défaut fonctionne déjà de manière si fracassante sur nos amis <img> - et je veux seulement étendre ce comportement un peu pour inclure un peu plus de HTML - cela semble être quelque chose qui devrait être facilement possible .

Mes efforts jusqu'à présent

Tout d'abord, j'ai configuré ma balise fantaisie <a> Avec l'attribut draggable, et désactivé contenteditable (je ne sais pas si c'est nécessaire, mais il semble que cela pourrait tout aussi bien être désactivé):

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

Ensuite, parce que l'utilisateur pouvait toujours faire glisser l'image hors de la boîte fantaisie <a>, J'ai dû faire du CSS. Je travaille dans Chrome, donc je ne vous montre que les préfixes -webkit-, bien que j'aie aussi utilisé les autres.

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

Maintenant, l'utilisateur peut faire glisser toute la boîte fantaisie, et la petite image de représentation de clic-glisser partiellement fanée reflète cela - je peux voir que je prends la boîte entière maintenant :)

J'ai essayé plusieurs combinaisons de différentes propriétés CSS, le combo ci-dessus semble avoir du sens pour moi et semble fonctionner le mieux.

J'espérais que ce CSS à lui seul serait suffisant pour que le navigateur utilise l'élément entier comme élément déplaçable, accordant automatiquement à l'utilisateur la fonctionnalité dont je rêvais ... Il semble cependant être plus compliqué que ça.

API JavaScript Drag and Drop de HTML5

Ce truc de glisser-déposer semble plus compliqué qu'il ne devrait l'être.

J'ai donc commencé à me plonger dans les documents api DnD, et maintenant je suis bloqué. Alors, voici ce que j'ai arrangé (oui, 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');
    });

Voici donc où je suis coincé: Cela fonctionne presque complètement. Presque complètement. Tout fonctionne, jusqu'à la fin. Dans l'événement de dépôt, j'ai maintenant accès au contenu HTML de la boîte fantaisie que j'essaie d'insérer à l'emplacement de dépôt. Tout ce que je dois faire maintenant, c'est l'insérer au bon endroit!

Le problème est Je ne trouve pas le bon emplacement de dépôt, ni aucun moyen de l'insérer. J'espérais trouver une sorte de = 'dropLocation' objet dans lequel vider ma boîte fantaisie, quelque chose comme dropEvent.dropLocation.content=myFancyBoxHTML;, ou peut-être à au moins, une sorte de valeurs de lieu de dépôt avec lesquelles trouver ma propre façon de mettre le contenu là-bas? Suis-je donné quelque chose?

Est-ce que je le fais complètement mal? Suis-je complètement raté quelque chose?

J'ai essayé d'utiliser document.execCommand('insertHTML',false,content); comme je m'attendais à pouvoir le faire, mais cela m'échoue malheureusement ici, car le curseur de sélection est ne se trouve pas à l'emplacement de dépôt précis comme je l'espère.

J'ai découvert que si je commente tous les event.preventDefault();, le curseur de sélection devient visible, et comme on pourrait l'espérer, lorsque l'utilisateur se prépare à déposer, en faisant glisser son curseur sur le contenu édité, le petit curseur de sélection peut être vu courir entre les caractères en suivant le curseur de l'utilisateur et l'opération de dépôt - indiquant à l'utilisateur que le curseur de sélection représente l'emplacement de dépôt précis. J'ai besoin de l'emplacement de ce curseur de sélection.

Avec quelques expériences, j'ai essayé execCommand-insertHTML'ing pendant l'événement drop et l'événement dragend - ni insérer le code HTML où se trouvait le curseur de sélection-sélection, mais il utilise à la place l'emplacement sélectionné avant l'opération de glisser.

Parce que le curseur de sélection est visible pendant le survol, j'ai hachuré un plan.

Pendant un certain temps, j'essayais, dans l'événement de survol, d'insérer un marqueur temporaire, comme <span class="selection-marker">|</span>, Juste après $('.selection-marker').remove();, dans une tentative pour que le navigateur supprime constamment (pendant le survol) la suppression tous les marqueurs de sélection, puis en ajouter un au point d'insertion - en laissant essentiellement un marqueur où que ce point d'insertion se trouve, à tout moment. Le plan était bien sûr de remplacer ensuite ce marqueur temporaire par le contenu glissé que j'ai.

Bien sûr, rien de tout cela n'a fonctionné: je n'ai pas pu insérer le marqueur de sélection dans le curseur de sélection apparemment visible comme prévu - encore une fois, le execCommand-inséré HTML s'est placé là où se trouvait le curseur de sélection, avant l'opération de glisser.

Souffler. Alors qu'est-ce que j'ai raté? Comment est-il fait?

Comment puis-je obtenir ou insérer l'emplacement précis d'une opération de glisser-déposer? J'ai l'impression que c'est, évidemment , une opération courante parmi les glisser-déposer - je dois sûrement avoir négligé un détail important et flagrant d'une certaine sorte? Ai-je même dû approfondir JavaScript, ou peut-être qu'il existe un moyen de le faire simplement avec des attributs comme draggable, droppable, contenteditable, et un peu de fantaisie CSS3?

Je suis toujours à la chasse - bricoler toujours - je posterai dès que je découvrirai ce que j'ai échoué :)


La chasse continue (modifications après le message d'origine)


Farrukh a publié une bonne suggestion - utilisez:

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

Pour voir où se trouve réellement le curseur de sélection. Je l'ai inséré dans l'événement de survol , c'est-à-dire lorsque je pense que le curseur de sélection saute visiblement entre mon contenu modifiable dans le contenu modifiable.

Hélas, l'objet Range renvoyé, rapporte les indices de décalage qui appartiennent au curseur de sélection avant l'opération de glisser-déposer.

Ce fut un vaillant effort. Merci Farrukh.

Alors que se passe-t-il ici? J'ai l'impression que le petit curseur de sélection que je vois sautiller n'est pas du tout le curseur de sélection! Je pense que c'est un imposteur!

Après une inspection approfondie!

Il s'avère que c'est un imposteur! Le curseur de sélection réel reste en place pendant toute l'opération de glissement! Vous pouvez voir le petit bougre!

Je lisais MDN Drag and Drop Docs , et j'ai trouvé ceci:

Naturellement, vous devrez peut-être également déplacer le marqueur d'insertion autour d'un événement de survol. Vous pouvez utiliser les propriétés clientX et clientY de l'événement comme avec d'autres événements de souris pour déterminer l'emplacement du pointeur de la souris.

Oui, cela signifie-t-il que je suis censé le comprendre par moi-même, basé sur clientX et clientY ?? Utiliser les coordonnées de la souris pour déterminer moi-même l'emplacement du curseur de sélection? Effrayant!!

J'envisagerai de le faire demain - à moins que moi-même, ou quelqu'un d'autre ici lisant ceci, ne puisse trouver une solution saine :)

60
ChaseMoskal

Dragon Drop

J'ai fait une quantité ridicule de tripotage. Donc, tellement jsFiddling.

Ce n'est pas une solution robuste ou complète; Je n'arriverai peut-être jamais tout à fait à en trouver un. Si quelqu'un a de meilleures solutions, je suis à l'écoute - je ne voulais pas avoir à le faire de cette façon, mais c'est le seul moyen que j'ai pu découvrir jusqu'à présent. Le jsFiddle suivant et les informations que je suis sur le point de vomir ont fonctionné pour moi dans ce cas particulier avec mes versions particulières de Firefox et Chrome sur ma configuration WAMP et mon ordinateur particuliers. Ne venez pas me pleurer quand cela ne fonctionne pas sur votre site Web. Cette merde de glisser-déposer est clairement chacun pour soi.

jsFiddle: Chase Moskal's Dragon Drop

Donc, j'ennuyais le cerveau de ma copine et elle pensait que je n'arrêtais pas de dire "dragon drop" alors qu'en fait, je disais simplement "drag-and-drop". Il est coincé, c'est donc ce que j'appelle mon petit copain JavaScript que j'ai créé pour gérer ces situations de glisser-déposer.

Il s'avère que c'est un peu un cauchemar. L'API HTML5 Drag-and-Drop, même à première vue, est horrible. Ensuite, vous vous réchauffez presque, alors que vous commencez à comprendre et à accepter la façon dont c'est supposé pour travailler .. Ensuite, vous réalisez à quel point c'est un cauchemar terrifiant, en apprenant comment Firefox et Chrome traitent cette spécification à leur manière et semblent ignorer complètement tous vos besoins. Vous vous retrouvez à poser des questions comme: "Attendez, quel élément est même en train d'être glissé en ce moment? Comment obtenir ces informations? Comment annuler cette opération de glisser? Comment puis-je arrêter la gestion par défaut unique de ce navigateur particulier de cette situation? "... Les réponses à vos questions:" Vous êtes seul, PERDU! Continuez à pirater les choses, jusqu'à ce que quelque chose fonctionne! ".

Alors, voici comment j'ai accompli un glisser-déposer précis d'éléments HTML arbitraires à l'intérieur, autour et entre plusieurs contenus modifiables. (note: I ' Je ne vais pas en profondeur avec chaque détail, vous devrez regarder le jsFiddle pour cela - je ne fais que divaguer des détails apparemment pertinents dont je me souviens de l'expérience, car j'ai un temps limité) =

Ma solution

  • Tout d'abord, j'ai appliqué CSS aux draggables (fancybox) - nous avions besoin de user-select:none; user-drag:element; Sur la boîte fantaisie, puis spécifiquement user-drag:none; Sur l'image dans la boîte fantaisie (et tout autre élément, pourquoi pas ?). Malheureusement, cela ne suffisait pas pour Firefox, qui nécessitait que l'attribut draggable="false" Soit explicitement défini sur l'image pour l'empêcher d'être déplaçable.
  • Ensuite, j'ai appliqué les attributs draggable="true" Et dropzone="copy" Aux contenus modifiables.

Aux draggables (fancyboxes), je lie un gestionnaire pour dragstart. Nous définissons le dataTransfer pour copier une chaîne vide de HTML '' - parce que nous devons le faire croire que nous allons faire glisser HTML, mais nous annulons tout comportement par défaut. Parfois, le comportement par défaut se glisse d'une manière ou d'une autre, et il en résulte un doublon (comme nous le faisons nous-mêmes), alors maintenant le pire problème est l'insertion d'un '' (espace) lorsqu'un glisser échoue. Nous ne pouvions pas compter sur le comportement par défaut, car il échouait trop souvent, j'ai donc trouvé que c'était la solution la plus polyvalente.

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

Aux dropzones, je lie un gestionnaire pour dragleave et drop. Le gestionnaire dragleave existe uniquement pour Firefox, comme dans Firefox, le glisser-déposer fonctionnerait (Chrome vous refuse par défaut) lorsque vous tentiez de le faire glisser en dehors de contenteditable, il effectue donc une vérification rapide par rapport à Firefox uniquement relatedTarget. Huff.

Chrome et Firefox ont différentes manières d'acquérir l'objet Range, , donc des efforts ont dû être faits pour le faire différemment pour chaque navigateur dans l'événement drop. Chrome construit une plage basée sur les coordonnées de la souris (ouais c'est vrai) , mais Firefox le fournit dans les données d'événement. document.execCommand('insertHTML',false,blah) s'avère être la façon dont nous traitons la goutte. OH, j'ai oublié de mentionner - nous ne pouvons pas utiliser dataTransfer.getData() sur Chrome pour obtenir notre ensemble HTML de démarrage par glisser-déplacer - cela semble être une sorte de bogue bizarre dans le Firefox appelle la spécification sur son bullcrap et nous donne les données de toute façon - mais Chrome ne le fait pas, donc nous nous penchons en arrière et pour définir le contenu sur un global, et passons par l'enfer pour tuer tout le comportement par défaut ...

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

D'accord, j'en ai marre de ça. J'ai écrit tout ce que je voulais à ce sujet. Je l'appelle un jour et je pourrais mettre à jour cela si je pense à quelque chose d'important.

Merci à tous. //Chasse.

39
ChaseMoskal

Comme je voulais voir cela dans une solution JS native, j'ai travaillé un peu pour supprimer toutes les dépendances jQuery. J'espère que cela peut aider quelqu'un.

Tout d'abord le balisage

    <div class="native_receiver" style="border: 2px solid red;padding: 5px;" contenteditable="true" >
      WAITING  FOR STUFF
    </div>
    <div class="drawer" style="border: 2px solid #AAE46A; padding: 10px">
      <span class="native_drag" data-type="dateselector" contenteditable="false" draggable="true" style="border: 2px solid rgba(0,0,0,0.2);padding:4px; margin:2px">
        Block 1
      </span>
      <span class="native_drag" data-type="dateselector" contenteditable="false" draggable="true" style="border: 2px solid rgba(0,0,0,0.2);padding:4px; margin:2px">
        Second Blk
      </span>
    </div>

Puis quelques aides

    function addClass( elem, className ){
        var classNames = elem.className.split( " " )
        if( classNames.indexOf( className ) === -1 ){
            classNames.Push( className )
        }
        elem.className = classNames.join( " " )
    }
    function selectElem( selector ){
        return document.querySelector( selector )
    }
    function selectAllElems( selector ){
        return document.querySelectorAll( selector )
    }
    function removeElem( elem ){
         return elem ? elem.parentNode.removeChild( elem ) : false
    }

Ensuite, les méthodes réelles

    function nativeBindDraggable( elems = false ){
        elems = elems || selectAllElems( '.native_drag' );
        if( !elems ){
            // No element exists, abort
            return false;
        }else if( elems.outerHTML ){
            // if only a single element, put in array
            elems = [ elems ];
        }
        // else it is html-collection already (as good as array)

        for( let i = 0 ; i < elems.length ; i++ ){
            // For every elem in list, attach or re-attach event handling
            elems[i].dataset.transferreference = `transit_${ new Date().getTime() }`;
            elems[i].ondragstart = function(e){
                if (!e.target.id){
                    e.target.id = (new Date()).getTime();
                }

                window.inTransferMarkup = e.target.outerHTML;
                window.transferreference = elems[i].dataset.transferreference;
                addClass( e.target, 'dragged');
            };
        };
    }

    function nativeBindWriteRegion( elems = false ){
        elems = elems || selectAllElems( '.native_receiver' );
        if( !elems ){
            // No element exists, abort
            return false;
        }else if( elems.outerHTML ){
            // if only a single element, put in array
            elems = [ elems ];
        }
        // else it is html-collection

        for( let i = 0 ; i < elems.length ; i++ ){
            elems[i].ondragover = function(e){
                e.preventDefault();
                return false;
            };
            elems[i].ondrop = function(e){
                receiveBlock(e);
            };
        }
    }

    function receiveBlock(e){
        e.preventDefault();
        let content = window.inTransferMarkup;

        window.inTransferMarkup = "";

        let 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);
        }
        let sel = window.getSelection();
        sel.removeAllRanges(); 
        sel.addRange( range );
        e.target.focus();

        document.execCommand('insertHTML',false, content);
        sel.removeAllRanges();

        // reset draggable on all blocks, esp the recently created
        nativeBindDraggable(
          document.querySelector(
            `[data-transferreference='${window.transferreference}']`
          )
        );
        removeElem( selectElem( '.dragged' ) );
        return false;
    }

Et enfin instancier

nativeBindDraggable();
nativeBindWriteRegion();

Ci-dessous l'extrait de fonctionnement

function addClass( elem, className ){
            var classNames = elem.className.split( " " )
            if( classNames.indexOf( className ) === -1 ){
                classNames.Push( className )
            }
            elem.className = classNames.join( " " )
        }
        function selectElem( selector ){
            return document.querySelector( selector )
        }
        function selectAllElems( selector ){
            return document.querySelectorAll( selector )
        }
        function removeElem( elem ){
             return elem ? elem.parentNode.removeChild( elem ) : false
        }
        
      
        function nativeBindDraggable( elems = false ){
                elems = elems || selectAllElems( '.native_drag' );
                if( !elems ){
                        // No element exists, abort
                        return false;
                }else if( elems.outerHTML ){
                        // if only a single element, put in array
                        elems = [ elems ];
                }
                // else it is html-collection already (as good as array)
            
                for( let i = 0 ; i < elems.length ; i++ ){
                        // For every elem in list, attach or re-attach event handling
                        elems[i].dataset.transferreference = `transit_${ new Date().getTime() }`;
                        elems[i].ondragstart = function(e){
                                if (!e.target.id){
                                        e.target.id = (new Date()).getTime();
                                }

                                window.inTransferMarkup = e.target.outerHTML;
                                window.transferreference = elems[i].dataset.transferreference;
                                addClass( e.target, 'dragged');
                        };
                };
        }
        
        function nativeBindWriteRegion( elems = false ){
                elems = elems || selectAllElems( '.native_receiver' );
                if( !elems ){
                        // No element exists, abort
                        return false;
                }else if( elems.outerHTML ){
                        // if only a single element, put in array
                        elems = [ elems ];
                }
                // else it is html-collection
                
                for( let i = 0 ; i < elems.length ; i++ ){
                        elems[i].ondragover = function(e){
                                e.preventDefault();
                                return false;
                        };
                        elems[i].ondrop = function(e){
                                receiveBlock(e);
                        };
                }
        }
        
        function receiveBlock(e){
                e.preventDefault();
                let content = window.inTransferMarkup;
                
                window.inTransferMarkup = "";
                
                let 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);
                }
                let sel = window.getSelection();
                sel.removeAllRanges(); 
                sel.addRange( range );
                e.target.focus();
                
                document.execCommand('insertHTML',false, content);
                sel.removeAllRanges();
                
            // reset draggable on all blocks, esp the recently created
                nativeBindDraggable(
              document.querySelector(
                `[data-transferreference='${window.transferreference}']`
              )
            );
                removeElem( selectElem( '.dragged' ) );
                return false;
        }


    nativeBindDraggable();
    nativeBindWriteRegion();
        <div class="native_receiver" style="border: 2px solid red;padding: 5px;" contenteditable="true" >
          WAITING  FOR STUFF
        </div>
        <div class="drawer" style="border: 2px solid #AAE46A; padding: 10px">
          <span class="native_drag" data-type="dateselector" contenteditable="false" draggable="true" style="border: 2px solid rgba(0,0,0,0.2);padding:4px; margin:2px">
            Block 1
          </span>
          <span class="native_drag" data-type="dateselector" contenteditable="false" draggable="true" style="border: 2px solid rgba(0,0,0,0.2);padding:4px; margin:2px">
            Second Blk
          </span>
        </div>
1
25r43q
  1. dragstart d'événement; dataTransfer.setData("text/html", "<div class='whatever'></div>");
  2. suppression d'événement: 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
holistic