読者です 読者をやめる 読者になる 読者になる

初心者のためのExtJS入門

ExtJSを使うので、ついでにまとめていきます

グリッドの機能:ドラッグ&ドロップ[classic]

ExtJS

Ext.grid.plugin.DragDropプラグインを使うと、グリッドの行をドラッグ&ドロップで操作できます。

プラグインを組み込む

/**
 * メモ一覧グリッドクラス。
 *
 * @class Memo.view.list.Grid
 * @extend Ext.grid.Panel
 */
Ext.define('Memo.view.list.Grid', {
    extend: 'Ext.grid.Panel',
    xtype: 'list_grid',

    requires: [
        'Ext.grid.column.Date',
        'Ext.grid.plugin.DragDrop'
    ],

    store: 'Memo',

    viewConfig: {
        plugins: [
            {
                ptype: 'gridviewdragdrop'
            }
        ]
    },

    columns: [
        {
            dataIndex: 'id',
            text: 'ID',
            align: 'right'
        },
        {
            dataIndex: 'title',
            text: 'タイトル',
            width: 250,
            renderer: function (value) {
                return Ext.String.htmlEncode(value);
            }
        },
        {
            dataIndex: 'body',
            text: '本文',
            flex: 1,
            renderer: function (value) {
                value = Ext.String.htmlEncode(value);
                return value.replace(/\r?\n/g, '<br/>');
            }
        },
        {
            xtype: 'datecolumn',
            format: 'Y/m/d H:i:s',
            dataIndex: 'created',
            text: '登録日時',
            width: 200
        }
    ]
});

f:id:sham-memo:20170208022249p:plain

編集用のプラグインと違い、viewConfigにプラグインを設定します。viewConfigは、グリッドが内部で生成するExt.view.Tableに渡されるコンフィグです。

とりあえず、これだけでグリッドの行をドラッグ&ドロップで移動できるようになりました。

次に、2つのグリッドを作成して、グリッド間で行を移動させてみました。グリッドの実装はほとんど変わりません。

/**
 * メモストアクラス。
 *
 * @class Memo.store.Memo
 * @extend Ext.data.Store
 */
Ext.define('Memo.store.Memo', {
    extend: 'Ext.data.Store',

    requires: [
        'Ext.data.proxy.LocalStorage',
        'Memo.model.Memo'
    ],

    model: 'Memo.model.Memo',

    proxy: {
        type: 'localstorage',
        id: 'memo'
    }
});

/**
 * 選択メモストアクラス。
 *
 * @class Memo.store.SelectMemo
 * @extend Ext.data.Store
 */
Ext.define('Memo.store.SelectMemo', {
    extend: 'Ext.data.Store',

    requires: [
        'Memo.model.Memo'
    ],

    model: 'Memo.model.Memo',

    proxy: 'memory'
});

/**
 * メモ一覧グリッドクラス。
 *
 * @class Memo.view.list.Grid
 * @extend Ext.grid.Panel
 */
Ext.define('Memo.view.list.Grid', {
    extend: 'Ext.grid.Panel',
    xtype: 'list_grid',

    requires: [
        'Ext.grid.plugin.DragDrop'
    ],

    cls: 'list-grid',

    store: 'Memo',

    viewConfig: {
        plugins: [
            {
                ptype: 'gridviewdragdrop'
            }
        ]
    },

    columns: [
        {
            dataIndex: 'id',
            text: 'ID',
            align: 'right'
        },
        {
            dataIndex: 'title',
            text: 'タイトル',
            flex: 1,
            renderer: function (value) {
                return Ext.String.htmlEncode(value);
            }
        }
    ]

});

/**
 * 選択メモ一覧グリッドクラス。
 *
 * @class Memo.view.list.SelectGrid
 * @extend Ext.grid.Panel
 */
Ext.define('Memo.view.list.SelectGrid', {
    extend: 'Ext.grid.Panel',
    xtype: 'list_selectgrid',

    requires: [
        'Ext.grid.plugin.DragDrop'
    ],

    cls: 'list-selectgrid',

    store: 'SelectMemo',

    viewConfig: {
        plugins: [
            {
                ptype: 'gridviewdragdrop'
            }
        ]
    },

    columns: [
        {
            dataIndex: 'id',
            text: 'ID',
            align: 'right'
        },
        {
            dataIndex: 'title',
            text: 'タイトル',
            flex: 1,
            renderer: function (value) {
                return Ext.String.htmlEncode(value);
            }
        }
    ]

});

/**
 * メモ一覧パネルクラス。
 *
 * @class Memo.view.list.Panel
 * @extend Ext.Panel
 */
Ext.define('Memo.view.list.Panel', {
    extend: 'Ext.Panel',
    xtype: 'list_panel',

    requires: [
        'Memo.view.list.Grid',
        'Memo.view.list.SelectGrid',
        'Memo.view.list.ViewController'
    ],

    controller: 'list',

    layout: {
        type: 'hbox',
        align: 'stretch'
    },

    listeners: {
        showscreen: 'onShowScreen'
    },

    defaults: {
        flex: 1
    },

    items: [
        {
            xtype: 'list_grid',
            padding: '30 15 30 30'
        },
        {
            xtype: 'list_selectgrid',
            padding: '30 30 30 15'
        }
    ]
});

f:id:sham-memo:20170208131534p:plain

↑では、左のグリッドから右のグリッドへ移動させています。逆向きの移動も可能です。

プラグインは最初の例と同じく単純に設定した状態です。特に指定がなければ、別のグリッドでも移動が可能であることがわかります。

移動を制限する

Ext.grid.plugin.DragDropのddGroupコンフィグが一致しない場合は移動できません。特に指定しない場合は、デフォルト値GridDDとなっています。

/**
 * メモ一覧グリッドクラス。
 *
 * @class Memo.view.list.Grid
 * @extend Ext.grid.Panel
 */
Ext.define('Memo.view.list.Grid', {
    extend: 'Ext.grid.Panel',
    xtype: 'list_grid',

    requires: [
        'Ext.grid.plugin.DragDrop'
    ],

    cls: 'list-grid',

    store: 'Memo',

    viewConfig: {
        plugins: [
            {
                ptype: 'gridviewdragdrop',
                ddGroup: 'memo1'
            }
        ]
    },

    columns: [
        {
            dataIndex: 'id',
            text: 'ID',
            align: 'right'
        },
        {
            dataIndex: 'title',
            text: 'タイトル',
            flex: 1,
            renderer: function (value) {
                return Ext.String.htmlEncode(value);
            }
        }
    ]

});

/**
 * 選択メモ一覧グリッドクラス。
 *
 * @class Memo.view.list.SelectGrid
 * @extend Ext.grid.Panel
 */
Ext.define('Memo.view.list.SelectGrid', {
    extend: 'Ext.grid.Panel',
    xtype: 'list_selectgrid',

    requires: [
        'Ext.grid.plugin.DragDrop'
    ],

    cls: 'list-selectgrid',

    store: 'SelectMemo',

    viewConfig: {
        plugins: [
            {
                ptype: 'gridviewdragdrop',
                ddGroup: 'memo2'
            }
        ]
    },

    columns: [
        {
            dataIndex: 'id',
            text: 'ID',
            align: 'right'
        },
        {
            dataIndex: 'title',
            text: 'タイトル',
            flex: 1,
            renderer: function (value) {
                return Ext.String.htmlEncode(value);
            }
        }
    ]

});

f:id:sham-memo:20170208134430p:plain

↑のように、移動させようとすると×アイコンが表示されました。

他にもenableDragやenableDropコンフィグを使うと、ドラッグだけ許可したり、ドロップだけ許可したりといった制限も付けられます。

データによって移動を制限したいのであれば、イベントハンドラで処理することもできます。

/**
 * 選択メモ一覧グリッドクラス。
 *
 * @class Memo.view.list.SelectGrid
 * @extend Ext.grid.Panel
 */
Ext.define('Memo.view.list.SelectGrid', {
    extend: 'Ext.grid.Panel',
    xtype: 'list_selectgrid',

    requires: [
        'Ext.grid.plugin.DragDrop'
    ],

    cls: 'list-selectgrid',

    store: 'SelectMemo',

    viewConfig: {
        plugins: [
            {
                ptype: 'gridviewdragdrop'
            }
        ]
    },

    columns: [
        {
            dataIndex: 'id',
            text: 'ID',
            align: 'right'
        },
        {
            dataIndex: 'title',
            text: 'タイトル',
            flex: 1,
            renderer: function (value) {
                return Ext.String.htmlEncode(value);
            }
        }
    ],

    listeners: {
        beforedrop: function (node, data, overModel, dropPosition, dropHandlers) {
            var records = data.records;  // recordsに選択行のモデルが配列でセットされている(複数行移動の場合があるので配列)
            return false;
        }
    }
});

例えば↑のように、beforedropイベントでfalseを返すとドロップがキャンセルされます。dataに設定されているモデルの内容によって、falseを返すことで移動を制限することができます。

ちなみに、falseを返す代わりに、dropHandlers.cancelDrop()を実行してもキャンセルできます。違いは元の位置に戻るアクションが付くかどうかです。

そもそも特定の行をドラッグできないようにしたいんだけど、という場合は一応ドラッグする側のグリッドのbeforeitemmousedownイベントでfalseを返すと止めることはできます。

/**
 * メモ一覧グリッドクラス。
 *
 * @class Memo.view.list.Grid
 * @extend Ext.grid.Panel
 */
Ext.define('Memo.view.list.Grid', {
    extend: 'Ext.grid.Panel',
    xtype: 'list_grid',

    requires: [
        'Ext.grid.plugin.DragDrop'
    ],

    cls: 'list-grid',

    store: 'Memo',

    viewConfig: {
        plugins: [
            {
                ptype: 'gridviewdragdrop',
                containerScroll: true
            }
        ]
    },

    columns: [
        {
            dataIndex: 'id',
            text: 'ID',
            align: 'right'
        },
        {
            dataIndex: 'title',
            text: 'タイトル',
            flex: 1,
            renderer: function (value) {
                return Ext.String.htmlEncode(value);
            }
        }
    ],

    listeners: {
        beforeitemmousedown: function (grid, record, item, index, e) {
            return false;
        }
    }
});

↑は仕様によっては問題が発生するかもしれません。編集できるようにしている場合に処理が中断されることがありそうですし、複数行の場合にどうするかといった問題も発生しそうです。ここでは、ドラッグ操作以前のイベントで禁止にできなくはないということで一応紹介しました。

移動ではなくコピーする

Ext.grid.plugin.DragDropのcopyコンフィグをtrueにするとコピーになります。

/**
 * メモ一覧グリッドクラス。
 *
 * @class Memo.view.list.Grid
 * @extend Ext.grid.Panel
 */
Ext.define('Memo.view.list.Grid', {
    extend: 'Ext.grid.Panel',
    xtype: 'list_grid',

    requires: [
        'Ext.grid.plugin.DragDrop'
    ],

    cls: 'list-grid',

    store: 'Memo',

    viewConfig: {
        plugins: [
            {
                ptype: 'gridviewdragdrop',
                copy: true
            }
        ]
    },

    columns: [
        {
            dataIndex: 'id',
            text: 'ID',
            align: 'right'
        },
        {
            dataIndex: 'title',
            text: 'タイトル',
            flex: 1,
            renderer: function (value) {
                return Ext.String.htmlEncode(value);
            }
        }
    ]

});

f:id:sham-memo:20170208141954p:plain

データに応じてコピーしたい場合は、イベントハンドラで処理します。

/**
 * 選択メモ一覧グリッドクラス。
 *
 * @class Memo.view.list.SelectGrid
 * @extend Ext.grid.Panel
 */
Ext.define('Memo.view.list.SelectGrid', {
    extend: 'Ext.grid.Panel',
    xtype: 'list_selectgrid',

    requires: [
        'Ext.grid.plugin.DragDrop'
    ],

    cls: 'list-selectgrid',

    store: 'SelectMemo',

    viewConfig: {
        plugins: [
            {
                ptype: 'gridviewdragdrop'
            }
        ]
    },

    columns: [
        {
            dataIndex: 'id',
            text: 'ID',
            align: 'right'
        },
        {
            dataIndex: 'title',
            text: 'タイトル',
            flex: 1,
            renderer: function (value) {
                return Ext.String.htmlEncode(value);
            }
        }
    ],

    listeners: {
        beforedrop: function (node, data, overModel, dropPosition, dropHandlers) {
            data.copy = true;
        }
    }
});

↑のようにbeforedropイベントの第2引数dataのcopyプロパティをtrueにするとコピーになるので、data.recordsの値に応じた対応が可能です。

ドラッグに合わせてスクロールしたい

Ext.grid.plugin.DragDropプラグインのcontainerScrollコンフィグをtrueにすると、移動のドラッグ中にグリッドのスクロール位置を調整してくれるようになります。

移動後に行の位置を保存する

ブラウザのリロード後も、移動した行の位置を維持したいのであれば、位置をデータとして残す必要があります。そのためには、位置を表す項目をモデルが持っておくべきです。

ここでは、位置を表す項目としてsortフィールドを追加してみます。

/**
 * メモモデルクラス。
 *
 * @class Memo.model.Memo
 * @extend Ext.data.Model
 */
Ext.define('Memo.model.Memo', {
    extend: 'Ext.data.Model',

    fields: [
        {
            name: 'id',
            type: 'int'
        },
        {
            name: 'title',
            type: 'string'
        },
        {
            name: 'body',
            type: 'string'
        },
        {
            name: 'created',
            type: 'date',
            dateFormat: 'Y/m/d H:i:s'
        },
        {
            name: 'sort',
            type: 'int'
        }
    ],

    proxy: {
        type: 'localstorage',
        id: 'memo'
    }
});

次に、ストアの並び順を指定します。

/**
 * メモストアクラス。
 *
 * @class Memo.store.Memo
 * @extend Ext.data.Store
 */
Ext.define('Memo.store.Memo', {
    extend: 'Ext.data.Store',

    requires: [
        'Ext.data.proxy.LocalStorage',
        'Memo.model.Memo'
    ],

    model: 'Memo.model.Memo',

    proxy: {
        type: 'localstorage',
        id: 'memo'
    },

    remoteSort: true,

    sorters: [
        {
            property: 'sort',
            direction: 'ASC'
        }
    ]
});

sortersコンフィグを指定することで、ストア内のデータの並び順を設定できます。

remoteSortをtrueにすると、サーバ側に並べ替えを委ねることができます(ただ、今はプロキシをローカルストレージにしているので、実際にはプロキシクラスの内部でソートしてるんですが)。

今回remoteSortをtrueにしているのは、ストアのeachで並び順をまとめて変更するためです。でなければ、eachのループ順が常にsortersの影響を受けてしまいます。

最後に、dropイベントでsortを変更し、ローカルストレージに反映します。(ビューコントローラはもう省略してますよ。まあストアをロードしてるだけです)

/**
 * メモ一覧グリッドクラス。
 *
 * @class Memo.view.list.Grid
 * @extend Ext.grid.Panel
 */
Ext.define('Memo.view.list.Grid', {
    extend: 'Ext.grid.Panel',
    xtype: 'list_grid',

    requires: [
        'Ext.grid.plugin.DragDrop'
    ],

    cls: 'list-grid',

    store: 'Memo',

    viewConfig: {
        plugins: [
            {
                ptype: 'gridviewdragdrop'
            }
        ]
    },

    columns: [
        {
            dataIndex: 'id',
            text: 'ID',
            align: 'right'
        },
        {
            dataIndex: 'title',
            text: 'タイトル',
            flex: 1,
            renderer: function (value) {
                return Ext.String.htmlEncode(value);
            }
        }
    ],

    listeners: {
        drop: function () {
            var store = Ext.getStore('Memo');

            store.each(function (record, index) {
                record.set('sort', index + 1);
            });

            store.sync();
        }
    }
});

/**
 * メモ一覧パネルクラス。
 *
 * @class Memo.view.list.Panel
 * @extend Ext.Panel
 */
Ext.define('Memo.view.list.Panel', {
    extend: 'Ext.Panel',
    xtype: 'list_panel',

    requires: [
        'Memo.view.list.Grid',
        'Memo.view.list.ViewController'
    ],

    controller: 'list',

    layout: 'fit',

    listeners: {
        showscreen: 'onShowScreen'
    },

    items: {
        xtype: 'list_grid'
    }
});

これでうまくいきました。