web-dev-qa-db-ja.com

contenteditable内での正確なドラッグアンドドロップ

セットアップ

だから、私はコンテンツ編集可能なdivを持っています-私はWYSIWYGエディタを作っています:太字、斜体、フォーマット、何でも、そして最近では:キャプション付きの派手な画像を挿入します。

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

ユーザーはこれらの派手な画像をダイアログに追加します。詳細を入力し、画像をアップロードします。その後、他のエディター機能と同様に、document.execCommand('insertHTML',false,fancy_image_html);を使用してユーザーの選択で取り込みます。

必要な機能

だから、今では私のユーザーは派手なイメージを取り入れることができます-彼らはそれを動かすことができる必要があります。ユーザーは、画像(派手なボックスなど)をクリックしてドラッグし、contenteditable内の好きな場所に配置できる必要があります。必要に応じて、段落間、または段落内、さらには2つの単語間で移動できる必要があります。

私に希望を与えるもの

心に留めておいてください-内容が編集可能な、昔ながらの<img>タグは、この魅力的なドラッグアンドドロップ機能により、ユーザーエージェントによって既に祝福されています。デフォルトでは、好きな場所に<img>タグをドラッグアンドドロップできます。デフォルトのドラッグアンドドロップ操作は、夢のように動作します。

したがって、このデフォルトの動作が<img>のバディで既に非常にうまく機能していることを考えると、この動作を少し拡張して少しHTMLを追加したいだけです。これは簡単に可能になるはずです。

これまでの私の努力

まず、draggable属性を使用してファンシーな<a>タグを設定し、contenteditableを無効にします(必要かどうかはわかりませんが、オフになっているようです)。

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

その後、ユーザーはまだ画像を<a>の派手なボックスからドラッグできるので、CSSを実行する必要がありました。私はChromeで作業しているので、-webkit-プレフィックスのみを表示していますが、他のプレフィックスも使用しています。

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

これで、ユーザーはファンシーボックス全体をドラッグでき、部分的に色あせた小さなクリックドラッグ表現イメージにこれが反映されます。ボックス全体を今ピックアップしていることがわかります:)

さまざまなCSSプロパティの組み合わせをいくつか試しましたが、上記のコンボは理にかなっているようで、最適に機能するようです。

ブラウザが要素全体をドラッグ可能なアイテムとして使用するのにこのCSSだけで十分であることを望んでいました。私が夢見ていた機能を自動的にユーザーに付与します... しかし、より複雑に見えるそれ。

HTML5のJavaScriptドラッグアンドドロップAPI

このドラッグアンドドロップは、必要以上に複雑なようです。

それで、私はDnD APIドキュメントに深く入り始めましたが、今は行き詰まっています。だから、ここに私が装備したものがあります(はい、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');
    });

だからここで私が立ち往生しています:これはほぼ完全に動作します。 ほぼ完全に。最後まで、すべてが機能しています。ドロップイベントで、ドロップ位置に挿入しようとしているファンシーボックスのHTMLコンテンツにアクセスできるようになりました。あとは、正しい場所に挿入するだけです!

問題は正しいドロップ位置を見つけることができない、または挿入する方法がありません。私は空想をダンプするために何らかの種類の'dropLocation'オブジェクトを見つけることを望んでいますdropEvent.dropLocation.content=myFancyBoxHTML;のようなもの、あるいは少なくとも、コンテンツをそこに置くための独自の方法を見つけるための何らかのドロップ位置の値ですか? 私は何か与えられますか?

完全に間違っていますか?完全に何かが足りないのですか?

document.execCommand('insertHTML',false,content);を使用しようとしましたが、残念ながら、選択キャレットが正確なドロップ位置にないため、ここで失敗します。 。

event.preventDefault();をすべてコメントアウトすると、選択キャレットが表示されるようになり、希望するように、ユーザーがドロップする準備をしているときにドラッグして、 contenteditable、小さな選択キャレットは、ユーザーのカーソルとドロップ操作に続く文字間で実行されているのを見ることができます-選択キャレットが正確なドロップ位置を表すことをユーザーに示します。 この選択キャレットの場所が必要です。

いくつかの実験で、dropイベントとdragendイベントの間にexecCommand-insertHTML'ingを試しました。droping-selection-caretがあった場所にHTMLを挿入せず、代わりにドラッグ操作の前に選択された場所を使用します。

選択キャレットはドラッグオーバー中に表示されるため、プランをハッチングしました。

しばらくの間、私はドラッグオーバーイベントで、<span class="selection-marker">|</span>のような一時的なマーカーを$('.selection-marker').remove();の直後に挿入しようとしていました。これはブラウザーがすべての選択マーカーを削除してから挿入ポイントに1つ-挿入ポイントがどこであっても、いつでも基本的に1つのマーカーを残します。もちろん、この一時的なマーカーを、私が持っているドラッグされたコンテンツに置き換える計画でした。

もちろん、これは機能しませんでした:選択マーカーを計画どおりに表示される選択キャレットに挿入することができませんでした-再び、execCommand-insertedHTMLは、ドラッグ操作の前に選択キャレットがある場所に配置されました。

ハフ。だから私は何を見逃しましたか?どうやって?

ドラッグアンドドロップ操作の正確な場所を取得または挿入するにはどうすればよいですか?これは明らかに、ドラッグアンドドロップの一般的な操作だと思います-確かに私はある種の重要で露骨な詳細を見落としていたに違いない? JavaScriptに深く入り込む必要があったのでしょうか、それともドラッグ可能、ドロップ可能、コンテンツ編集可能、そして凝ったCSS3のような属性だけでこれを行う方法がありますか?

私はまだ狩りをしています-まだいじくり回しています-私が失敗したことを見つけたらすぐに投稿します:)


ハントは続く(元の投稿の後に編集)


Farrukhは良い提案を投稿しました-使用:

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

選択キャレットが実際にある場所を確認します。これをdragoverイベントに追加しました。このイベントでは、選択キャレットがcontenteditableの編集可能なコンテンツ間を移動しているように見えます。

残念ながら、返されるRangeオブジェクトは、ドラッグアンドドロップ操作の前に選択キャレットに属するオフセットインデックスを報告します。

それは勇敢な努力でした。 Farrukhに感謝します。

それで、ここで何が起こっているのでしょうか?私は飛び回っている小さな選択キャレットが、選択キャレットではないという感覚を得ています!詐欺師だと思います!

さらに調べて!

結局、それは詐欺師です! real選択キャレットは、ドラッグ操作全体を通じてそのままです!あなたは小さな盗人を見ることができます!

MDN Drag and Drop Docs を読んでいて、これを見つけました:

当然、ドラッグオーバーイベントの周りにも挿入マーカーを移動する必要があります。 他のマウスイベントと同様に、イベントのclientXプロパティとclientYプロパティを使用して、マウスポインターの位置を決定できます。

いいですね、これはclientXclientY?に基づいて、私が自分で解決することになっているということですか?マウス座標を使用して、選択キャレットの位置を自分で決定しますか?怖い!!

明日、そうすることを検討します-私自身、またはここを読んでいる誰かが正気の解決策を見つけられない限り:)

60
ChaseMoskal

ネイティブJSソリューションでこれを確認したかったため、jQueryのすべての依存関係を削除するために少し努力しました。うまくいけば、それが誰かを助けることができます。

最初にマークアップ

    <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>

その後、いくつかのヘルパー

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

以下は機能するスニペットです

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. イベントのドラッグスタート。 dataTransfer.setData("text/html", "<div class='whatever'></div>");
  2. イベントドロップ: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