カスタムツリービューの基本的な使い方(その5~ドラッグ&ドロップ)

ツリーのアイテムをドラッグ&ドロップして移動できるようにする。ただし一度にドラッグ&ドロップ可能な行数は1とする。つまり、複数選択してのドラッグ&ドロップは不許可とする。
ベースとなるソースコードはその1~表示を参照。

fruits.xul

tree か treechildren 要素に対して、ドラッグ開始時に発生する ondragstart イベントハンドラを追加する。 handleDragStart 関数については後述。

    <tree id="fruitsTree"
          ondragstart="handleDragStart(event);"
          flex="1">

fruits.js

はじめに handleDragStart 関数を実装する。この関数では、まず dragstart イベント発生元が treechildren 要素であることを確認し、それ以外の場合のイベント(ツリーのスクロールバーのドラッグ)を無視する。ただし、 treechildren 要素に ondragstart イベントハンドラを付加した場合、この処理は不要となる。次に、現在選択している行数が1である場合のみ、現在の行番号(つまりドラッグ元の行番号)を取得し、その値を text/x-moz-tree-index という固有のデータ型で、ドラッグ&ドロップの転送データとしてセットする。この処理は Firefox 3.5 で導入された新しいドラッグ&ドロップAPIを使って実装されている。 Firefox 3.0 以下に対応させる場合、 nsDragAndDrop.js を使ったレガシーな実装方式が必要となる。

function handleDragStart(event) {
    // ignore when dragging scrollbar
    if (event.target.localName != "treechildren")
        return;
    // disallow dragging multiple rows
    if (gFruitsTreeView.selection.count != 1)
        return;
    // set current row index to transfer data
    var sourceIndex = gFruitsTreeView.selection.currentIndex;
    event.dataTransfer.setData("text/x-moz-tree-index", sourceIndex);
    event.dataTransfer.dropEffect = "move";
}

前述のドラッグ&ドロップAPIでは、ドラッグ開始時に発生する dragstart イベントの他に、ドラッグオーバー時に発生する dragover イベントやドロップ時に発生する drop イベントなどもある。しかし、ツリー内の行のドラッグ&ドロップを実装する場合はドラッグ時の処理だけを tree 要素側に実装し、ドラッグオーバー時とドロップ時の処理は nsITreeView 側に実装する。

ツリーへのドラッグオーバー時は、 nsITreeView#canDrop メソッドが呼び出され、このメソッドで true を返すと現在の位置に対するドロップが可能であることを示すアンダーラインが表示され、 false を返すとマウスポインタが駐車禁止の標識のようなアイコンへと変わり、ドロップが不可であることが示される。 canDrop メソッドへ渡される第1引数 targetIndex はドロップしようとしている位置の行番号、第2引数 orientation はその行の前後どちらに対してドロップしようとしているかを示す値で、 nsITreeView で定義されている3つの定数、 DROP_BEFORE (-1) 、 DROP_ON (0) 、 DROP_AFTER (1) のうちのいずれかである。ただし、 DROP_ON は階層構造があるツリーのコンテナ(いわゆるフォルダ)へのドラッグオーバー時にセットされるので、今回は考慮する必要はない。第3引数 dataTransfer は nsIDOMDataTransfer オブジェクト(dragstart イベントなど発生時の event.dataTransfer に相当)である。ただし、第3引数は Firefox 3.5 以前では渡されないので、ここからは Firefox 3.6 以上を前提として話を進める。なお、 Firefox 3.5 に対応させるためには nsIDOMDataTransfer の代わりに nsIDragSession から転送データの値を取得するやや面倒な手順が必要となる

canDrop メソッドではまず dataTransfer からドラッグされた転送データ内に text/x-moz-tree-index データ型のデータが存在するかを確認する。存在しない場合、ツリーアイテム以外の何らかのデータがドラッグオーバーされたということなので、 false を返してドロップを不許可にする。さらに、複数のツリーアイテムのドロップを不許可、現在の行と同一の行前後へのドロップは無意味なため不許可にする。

下記のコード中にデバッグ用の dump 関数を仕込んだので、コンソールをみながらドラッグして具体的な引数の値を調べるとわかりやすい。

    canDrop: function(targetIndex, orientation, dataTransfer) {
        dump("canDrop(" + targetIndex + ", " + orientation + ")
");
        if (!dataTransfer.types.contains("text/x-moz-tree-index"))
            return false;
        if (this.selection.count != 1)
            return false;
        var sourceIndex = this.selection.currentIndex;
        if (sourceIndex == -1)
            return false;
        if (sourceIndex == targetIndex)
            return false;
        if (sourceIndex == (targetIndex + orientation))
            return false;
        return true;
    },

ツリーへのドロップ時は、 nsITreeView#drop メソッドが呼び出される。引数は canDrop メソッドと同様である。下記コードでは、まず前述の canDrop メソッドを使ってドロップが可能であることをチェックする。次に、ドロップ先の行番号 targetIndex の値をドロップした後の状態でのそのアイテムの行番号へと補正している。この補正は以下のような条件判断で行われる。
(1) ドラッグ元の行よりも下にある行の前へドロップした場合、アイテム移動によって行番号が1減ることを考慮する
(2) ドラッグ元の行よりも上にある行の後へドロップした場合、その行のすぐ下の位置へ移動する
この条件判断は頭で考えるだけでは難しいので、 canDrop メソッド同様に dump 関数を使ってデバッグを行うと良い。特に階層構造を持ったツリーでは条件判断がかなり複雑になる。
ドロップ後のツリーアイテムの行番号を求めたあとは、新たに追加する自前の moveItem メソッドによって実際のデータ内のアイテム移動およびツリーの表示更新を行う。

    drop: function(targetIndex, orientation, dataTransfer) {
        if (!this.canDrop(targetIndex, orientation, dataTransfer))
            return;
        var sourceIndex = this.selection.currentIndex;
        if (sourceIndex < targetIndex) {
            if (orientation == Components.interfaces.nsITreeView.DROP_BEFORE)
                targetIndex--;
        }
        else {
            if (orientation == Components.interfaces.nsITreeView.DROP_AFTER)
                targetIndex++;
        }
        this.moveItem(sourceIndex, targetIndex);
    },

ドロップ時に drop メソッドから呼び出す moveItem メソッドでは、データ _data 配列の位置 aSourceIndex の要素を、位置 aTargetIndex へと移動させ、ツリーの表示を更新して移動した要素に対応する行を再度選択状態にする。

    /**
     * @param Number aSourceIndex The array index wherefrom move.
     * @param Number aTargetIndex The array index whereto move.
     */
    moveItem: function(aSourceIndex, aTargetIndex) {
        if (aTargetIndex < 0 || aTargetIndex > this.rowCount - 1)
            return;
        var removedItems = this._data.splice(aSourceIndex, 1);
        this._data.splice(aTargetIndex, 0, removedItems[0]);
        this._treeBoxObject.invalidate();
        // select moved item again
        this.selection.clearSelection();
        this.selection.select(aTargetIndex);
        this._treeBoxObject.ensureRowIsVisible(aTargetIndex);
        this._treeBoxObject.treeBody.parentNode.focus();
    },

応用例1~ボタンによる移動~

Firefox の「検索バーの管理」のように、「上へ」「下へ」のボタンによって移動できるようにする。
まずは fruits.xul へ以下のように Up / Down ボタンを追加する。

    <vbox>
        <button label="Up" oncommand="bumpFruit(-1);" />
        <button label="Down" oncommand="bumpFruit(1);" />
    </vbox>

次に、ボタンクリック時に呼び出される bumpFruit 関数を実装する。

function bumpFruit(aUpDown) {
    if (gFruitsTreeView.selection.count != 1)
        return;
    var sourceIndex = gFruitsTreeView.selection.currentIndex;
    var targetIndex = sourceIndex + aUpDown;
    gFruitsTreeView.moveItem(sourceIndex, targetIndex);
}

応用例2~ドラッグ&ドロップの転送データを利用する~

ここまでは「ドラッグ元のツリー行=ドロップ時に選択されたツリー行」という前提で nsITreeView#canDrop および drop メソッドを実装したが、ドラッグ開始時にドラッグ元の行番号を転送データとしてセットしているので、これを利用する手もある。 nsITreeView#canDrop および nsITreeView#drop は以下のように修正される。

-        var sourceIndex = this.selection.currentIndex;
+        var sourceIndex = parseInt(dataTransfer.getData("text/x-moz-tree-index"));

今回のような階層構造の無いツリーでは応用例2の方式で問題ないが、階層構造のあるツリーではドラッグ中にフォルダ上にマウスオーバーするとフォルダが自動的に開いてツリーの行番号に狂いが生じる場合があるため、応用例2の方式が使えない。

関連記事

TOP

TOP