ドラッグアンドドロップ

「ドラッグアンドドロップ」は、開発者が使用できるインタラクション設計パターンの中で最も強力です。ドラッグアンドドロップは、特に正しく使用できていると、あまり深く考えずに利用しています。次の簡単な5つのステップに従うと、理想的な実装が保証されます。

ドラッグアンドドロップの定義

ドラッグ操作とは、究極的には、マウスボタンが押下され、マウスが移動する間のUI要素上でのクリック動作です。ドロップ操作は、ドラッグ操作の後にマウスボタンを放すと発生します。ドラッグとドロップの判定の概要は、以下のフローチャートにまとめられています。

Ext Drag and Drop

開発期間を短縮するため、Ext JSには、基本的な判定を管理するExt.ddクラスが用意されています。本ガイドでは、ドロップ誘引、無効なドロップ修正の表示と削除、さらにドロップが成功すると発生する内容のコードについて説明します。

ドラッグアンドドロップクラスの体系化

Ext.ddマニュアルのクラスは、一見複雑そうですが、クラスの説明を読むと、すべてはDragDropクラスから派生し、ほとんどはDragまたはDropグループに分類できることがわかります。さらに、これらのクラスはさらにシングルノードとマルチノードのドラッグまたはドロップインタラクションに分類できることがわかります。

Ext Drag and Drop

ドラッグアンドドロップについて理解するために、シングルドラッグアンドドロップインタラクションを DOMノードに適用する場合について説明します。このために、DDおよびDDTargetクラスを利用します。このクラスは、それぞれのドラッグアンドドロップ動作の基本的実装を提供します。ただし、ドラッグアンドドロップの実装を開始する前に、コードの目的について検討する必要があります。

今回のタスク

今回のアプリケーションについて、レンタルカー会社が所有する乗用車やトラックについて、レンタル可能、レンタル中、修理中の3つの状態を割り当てる機能を開発するように依頼されたと想定します。乗用車やトラックは、“available”(レンタル可能)コンテナに入れることだけが許可されます。

Ext Drag and Drop

最初に、乗用車やトラックを「ドラッグ可能」にすることが必要です。そのために、DDを使用します。rented(レンタル中)、repair(修理中)、vehicle(車輌)のコンテナを「ドロップターゲット」にすることが必要です。それには、DDTargetを使用します。最後に、さまざまなドラッグドロップグループを使用して、乗用車やトラックがそれぞれの“available”コンテナにだけドロップできるという要件を徹底させます。これで、ドラッグ操作を乗用車やトラックに追加して、コード作成を開始できるようになりました。

ステップ 1:ドラッグから開始

車輌DIV要素をドラッグ可能に設定するには、リストを取得し、ループさせてDDの新しいインスタンスを作成することが必要です。このように作成します。

Ext.onReady(function() {
        // Create an object that we'll use to implement and override drag behaviors a little later

        var overrides = {};

        // Configure the cars to be draggable
        var carElements = Ext.get('cars').select('div');
        Ext.each(carElements.elements, function(el) {
            var dd = Ext.create('Ext.dd.DD', el, 'carsDDGroup', {
                isTarget  : false
            });
            //Apply the overrides object to the newly created instance of DD
            Ext.apply(dd, overrides);
        });

        var truckElements = Ext.get('trucks').select('div');
        Ext.each(truckElements.elements, function(el) {
            var dd = Ext.create('Ext.dd.DD', el, 'trucksDDGroup', {
                isTarget  : false
            });
            Ext.apply(dd, overrides);
        });
    });

ドラッグアンドドロップクラスはすべて、そのメソッドをオーバーライドする手段によって実装されるように設計されています。このために、上記のコード例では、overridesという空のオブジェクトを作成し、必要なアクションに固有のオーバーライドを後からオブジェクトに指定します。子div要素すべてのcarsコンテナをクエリするDomQuery選択メソッドを利用して、carおよびtruck要素のリストを取得します。carsおよびtruck要素をドラッグ可能にするために、DDの新しいインスタンスを作成し、ドラッグされるcarまたはtruck要素および参加するドラッグドロップグループに渡します。車輌のタイプそれぞれにドラッグドロップグループがあることに注意してください。これは後で、ドロップターゲットとしてrentedおよびrepairコンテナを設定するときに重要になります。また、Ext.applyを使用して、DDの新しく作成されたインスタンスにoverridesオブジェクトを適用していることにも注意してください。これは、既存のオブジェクトにプロパティまたはメソッドを追加する便利な方法です。実装を続ける前に、画面上の要素をドラッグすると何が発生するかを分析することが必要になります。ここまで理解すれば、残りの実装は簡単です。

ドラッグノードの影響について

carまたはtruck要素を周辺でドラッグしたときに最初に気づくことは、場所を選ばずドロップされてしまうことです。しかし、現時点では、実装を開始したばかりであるため問題ではありません。重要な点は、ドラッグノードがどのように影響を受けているかを理解することです。これを理解しておくと、有効なドロップターゲット以外にドロップされると、元の位置に戻る(「無効ドロップ」と呼ばれます)コードの作成に役立ちます。以下の例では、FireBugのHTML点検パネルを使用し、ドラッグ操作がCamaro要素に適用されると行われる変更を強調表示します。

Ext Drag and Drop

ドラッグ操作中のドラッグ要素を確認すると、position、top、leftという更新された3つのCSS値を使用して、要素にスタイル属性が追加されていることがわかります。さらに詳しく確認すると、ノードが周囲でドラッグされている間にtopとleftの属性に対して設定された位置属性が更新されていることがわかります。ドラッグ動作が完了後、スタイル属性は、その中に含まれたスタイルと同様にそのままです。これは、無効ドロップを修正するコードを作成する際にクリーンアップすることが必要です。適切なドロップターゲットを設定するまで、ドロップ操作はすべて無効と考えられます。

ステップ 2:無効ドロップの修正

無効ドロップを修正する最も簡単な方法は、ドラッグ操作中に適用されるスタイル属性の設定を変更することです。つまり、ドラッグ要素がマウスの下から消失し、元の場所に再び表示されるという基本的なコードです。もっと円滑な動作にするには、Ext.Fxを使用してアニメーション化します。ドラッグアンドドロップクラスは、メソッドをオーバーライドするように設計されていることを思い出してください。修正を実装するには、b4StartDrag、onInvalidDrop、 and endDragメソッドをオーバーライドする必要があります。上記のオーバーライドオブジェクトに以下のメソッドを追加しましょう。次にその内容や実行について説明します。

var overrides = {
        // Called the instance the element is dragged.
        b4StartDrag : function() {
            // Cache the drag element
            if (!this.el) {
                this.el = Ext.get(this.getEl());
            }

            //Cache the original XY Coordinates of the element, we'll use this later.
            this.originalXY = this.el.getXY();
        },
        // Called when element is dropped in a spot without a dropzone, or in a dropzone without matching a ddgroup.
        onInvalidDrop : function() {
            // Set a flag to invoke the animated repair
            this.invalidDrop = true;
        },
        // Called when the drag operation completes
        endDrag : function() {
            // Invoke the animation if the invalidDrop flag is set to true
            if (this.invalidDrop === true) {
                // Remove the drop invitation
                this.el.removeCls('dropOK');

                // Create the animation configuration object
                var animCfgObj = {
                    easing   : 'elasticOut',
                    duration : 1,
                    scope    : this,
                    callback : function() {
                        // Remove the position attribute
                        this.el.dom.style.position = '';
                    }
                };

                // Apply the repair animation
                this.el.setXY(this.originalXY[0], this.originalXY[1], animCfgObj);
                delete this.invalidDrop;
            }
        },

上記のコードでは、最初にb4StartDragメソッドをオーバーライドしています。このメソッドが呼び出された瞬間に、ドラッグ要素は画面でドラッグされ始め、ドラッグ要素と最初のXY座標(このプロセスで後から使用します)をキャッシュする理想的な場所となります。次に、onInvalidDropをオーバーライドします。このメソッドが呼び出されると、ドラッグノードは、同じドラッグドロップグループに参加しているドロップターゲット以外でドロップされます。このオーバーライドで、ローカルのinvalidDropプロパティがtrueに設定され、次のメソッドで使用されます。オーバーライドする最後のメソッドがendDragです。このメソッドが呼び出されると、ドラッグ要素は画面でドラッグされなくなり、ドラッグ要素はマウスの移動によって制御されなくなります。このオーバーライドによって、ドラッグ要素はアニメーションを使用して元のXおよびY位置に戻ります。アニメーションは、elasticOutを使用するように設定したため、アニメーションの最後にスマートでおもしろいバウンスする効果を提供することが簡単になります。

Ext Drag and Drop

これで修正操作が完了しました。ドロップの誘引および有効なドロップ操作で機能するためには、ドロップターゲットを設定することが必要です。

ステップ 3:ドロップターゲットの設定

今回は、乗用車やトラックがrentedおよびrepairコンテナ、ならびにそれぞれの本来のコンテナでドロップできるようにすることが必要です。そのために、DDTargetクラスのインスタンスを作成することが必要です。このように実行します。

// Instantiate instances of Ext.dd.DDTarget for the cars and trucks container
var carsDDTarget = Ext.create('Ext.dd.DDTarget', 'cars','carsDDGroup');
var trucksDDTarget = Ext.create('Ext.dd.DDTarget', 'trucks', 'trucksDDGroup');

// Instantiate instances of DDTarget for the rented and repair drop target elements
var rentedDDTarget = Ext.create('Ext.dd.DDTarget', 'rented', 'carsDDGroup');
var repairDDTarget = Ext.create('Ext.dd.DDTarget', 'repair', 'carsDDGroup');

// Ensure that the rented and repair DDTargets will participate in the trucksDDGroup
rentedDDTarget.addToGroup('trucksDDGroup');
repairDDTarget.addToGroup('trucksDDGroup');

上記のコードのスニペットでは、car、truck、rented、repair要素にドロップターゲットを設定しています。乗用車のコンテナ要素だけが“carsDDGroup”に参加し、トラックのコンテナ要素は“trucksDDGroup”に参加することに注意してください。こうすると、乗用車とトラックは最初のコンテナだけでドロップ可能であるという要件を満たすことになります。次に、rented要素とrepair要素のDDTargetのインスタンスを作成します。最初の段階では、“carsDDGroup”だけに参加するように設定されています。“trucksDDGroup”に参加させるためには、addToGroupの手段で追加することが必要です。これでドロップターゲットが設定されました。乗用車やトラックを有効なドロップ要素上にドロップすると、どうなるか確認してみましょう。

Ext Drag and Drop

ドロップターゲットを試してみると、ドラッグ要素がドロップされた場所のままであることがわかります。つまり、画像はドロップターゲット上のどこの場所にでもドロップでき、その位置のままです。したがって、ドロップ実装はまだ完成していないことになります。完成するには、先ほど作成したDDのインスタンスの別のオーバーライドの手段によって、「完全ドロップ」動作のコードを実際に作成することが必要です。

ステップ 4:ドロップの完成

ドロップを完成するために、DOMツールを使用して、要素をその親要素から、ドロップターゲット要素へ実際にドラッグすることが必要です。このために、DDのonDragDropメソッドをオーバーライドします。以下のメソッドをオーバーライドオブジェクトに追加します。

var overrides = {
    ...
    // Called upon successful drop of an element on a DDTarget with the same
    onDragDrop : function(evtObj, targetElId) {
        // Wrap the drop target element with Ext.Element
        var dropEl = Ext.get(targetElId);

        // Perform the node move only if the drag element's
        // parent is not the same as the drop target
        if (this.el.dom.parentNode.id != targetElId) {

            // Move the element
            dropEl.appendChild(this.el);

            // Remove the drag invitation
            this.onDragOut(evtObj, targetElId);

            // Clear the styles
            this.el.dom.style.position ='';
            this.el.dom.style.top = '';
            this.el.dom.style.left = '';
        }
        else {
            // This was an invalid drop, initiate a repair
            this.onInvalidDrop();
        }
    },

上記のオーバーライドで、ドラッグ要素はドロップターゲット要素に移動しますが、それはドラッグ要素の親ノードと同じではない場合です。ドラッグ要素が移動した後、スタイルはクリアされます。ドロップ要素がドラッグ要素の親と同じである場合、this.onInvalidDropを呼び出して修正動作が必ず発生するようにします。

Ext Drag and Drop

ドロップに成功すると、ドラッグ要素は親要素からドロップターゲットへ移動することになります。ユーザーは有効なドロップターゲットの上にマウスがあることをどのように理解するのでしょうか?ドロップ誘引を設定することでビジュアルフィードバックをユーザーに提供します。

ステップ 5:ドロップ誘引の追加

ドラッグアンドドロップをもっと便利にするために、ドロップ操作が成功して実行できるかどうかユーザーにフィードバックを提供することが必要です。つまり、onDragEnterメソッドとonDragOutメソッドをオーバーライドすることが必要です。これらの2つのメソッドをオーバーライドオブジェクトに追加します。

var overrides = {
        ...
        // Only called when the drag element is dragged over the a drop target with the
same ddgroup
        onDragEnter : function(evtObj, targetElId) {
            // Colorize the drag target if the drag node's parent is not the same as the
drop target
            if (targetElId != this.el.dom.parentNode.id) {
                this.el.addCls('dropOK');
            }
            else {
                // Remove the invitation
                this.onDragOut();
            }
        },
        // Only called when element is dragged out of a dropzone with the same ddgroup
        onDragOut : function(evtObj, targetElId) {
            this.el.removeCls('dropOK');
        }
    };

上記のコードでは、onDragEnterメソッドとonDragOutメソッドをオーバーライドしました。どちらのメソッドもドラッグ要素が同じドラッグドロップグループに参加しているドロップターゲットとインタラクションする場合にのみ利用されます。onDragEnterメソッドは、ドラッグアイテムがドラッグモード中に、マウスカーソルがドロップターゲットの境界を初めて交差したときのみ呼び出されます。同様に、onDragOutは、ドラッグモード中に、マウスカーソルがドロップターゲットの境界外に始めてドラッグされると呼び出されます。

Ext Drag and Drop

onDragEnterメソッドとonDragOutメソッドにオーバーライドを追加することで、マウスカーソルが有効なドロップターゲットを初めて交差するとドラッグ要素の背景が緑になり、ドロップターゲットを離れる、またはドロップすると緑の背景が消えることを確認できます。これでDOM要素を使用するドラッグアンドドロップの実装が完成しました。

さらに

ドラッグアンドドロップは、Ext JSフレームワークのほとんどあらゆるものに適用できます。以下の例を使用すると、さまざまなウィジェットを使用してドラッグアンドドロップを実装する方法を学習できます。

要約

ここでは、最も簡単なドラッグアンドドロップ実装クラスを使用して、DOMノードのエンドツーエンドのドラッグアンドドロップを実装する方法を学習しました。簡単に、ドラッグアンドドロップを定義し、その内容やフレームワークの点からどのように考えるかを説明しました。また、ドラッグアンドドロップクラスはドラッグまたはドロップ動作、シングルまたはマルチドラッグまたはドロップ操作をサポートするかどうかによってグループ分けできることも学習しました。この動作を実装する中で、ddクラスは動作の決定に役立つこと、最終動作のコードに責任を持つことを示しました。DOMノードを使用する基本的なドラッグアンドドロップ操作を理解していただけたと思います。このトピックに関するガイドを今後追加する予定です。

著者

著者: Jay Garcia

Garcia 氏は『Ext JS in Action』と『Sencha Touch in Action』の著者です。2006年からSencha ベースの JavaScript フレームワークの普及に努めています。また、Modus Createの共同創業者兼CTOとして、高品質のSenchaベースのアプリケーションの優れた開発者を開拓することに努めています。Modus Create は Sencha Premier パートナーです。

Last updated