/home/by-natures/dev*

データ界隈で働くエンジニアとしての技術的なメモと、たまに普通の日記。

ExtJS: CheckboxModelのselectメソッドで指定したデータにフォーカスするのを避ける方法

ExtJS を使ったプロジェクトに携わっていたのですが、ようやくリリースを迎えられて一区切り付きました。

何でも揃った JavaScript フレームワークExtJS ですが、何でも揃いすぎて利用用途が大型のブラウザアプリケーション向けになっているようで、技術情報が少ないです。いわんや日本語をや… ということで、ExtJS の tips を少しご紹介できればと思います。

以下、ExtJS 4.2.0 での動作を仮定しています。

[toc]

CheckboxModel の使い方

grid_checkboxModel grid に設定できる selModel として CheckboxModel が用意されています。grid 左端にチェックボックスを付けられる機能です。

使い方は簡単で、grid の selModel プロパティに CheckboxModel のオブジェクトを設定すれば OK です。

var checkBoxModel = Ext.create('Ext.selection.CheckboxModel');
var employeeList  = [
            { name : 'Ando',     job : 'Designer',    age : 25 },
            { name : 'Baba',     job : 'Programmer',  age : 28 },
            { name : 'Chaya',    job : 'Director',    age : 34 },
            { name : 'Deguchi',  job : 'Programmer',  age : 30 },
            { name : 'Endo',     job : 'Programmer',  age : 21 },
            { name : 'Fuji',     job : 'Programmer',  age : 24 },
            { name : 'Genji',    job : 'Designer',    age : 28 },
            { name : 'Hourai',   job : 'Sales Staff', age : 29 },
            { name : 'Itou',     job : 'Director',    age : 35 },
            { name : 'Jonouchi', job : 'Programmer',  age : 27 },
            { name : 'Kiritani', job : 'Sales Staff', age : 38 }
        ];

Ext.application({
  name: 'Grid checkbox sample',
  requires: [
    'Ext.Viewport'
  ],

  launch: function() {
    Ext.create('Ext.Viewport', {
      layout: {
         type  : 'vbox',
         align : 'stretch'
      },
      items: [{
        xtype   : 'grid',
        height  : 100,
        columns : [
          { text : 'Name', dataIndex : 'name', flex : 1 },
          { text : 'Job',  dataIndex : 'job',  flex : 1 },
          { text : 'Age',  dataIndex : 'age',  flex : 1 }
        ],
        selModel : checkBoxModel,
        store : {
          fields : [
            { name : 'name', type : 'String' },
            { name : 'job',  type : 'String' },
            { name : 'age',  type : 'int'    }
          ],
          data : employeeList
        }
      }]
    });
  }
});

(データは適当です、すみません。。)

この Ext JS のサンプルを動かすための HTML も記載します:



    Grid checkbox sample

    

    

    



CheckboxModel で select したら、勝手に focus される?

この CheckboxModel ですが、データの内容によってはあらかじめチェックボックスを付けておきたいとしましょう。

ここではデータの最終行に必ずチェックを入れたい場合を考えてみます。実装方法は色々あると思いますが、grid の afterrender でやると次のようになります:

        ...
        selModel : checkBoxModel,
        store : {
          fields : [
            { name : 'name', type : 'String' },
            { name : 'job',  type : 'String' },
            { name : 'age',  type : 'int'    }
          ],
          data : employeeList
        },
        listeners : {
          afterrender: function(thisGrid) {
            var store     = thisGrid.getStore();
            var lastIndex = store.getCount() - 1;
            checkBoxModel.select(lastIndex);
          }
        }
        ...

listeners プロパティを新たに登録して、afterrender イベントの内容を記述しました。

これを実際に動作させるとデータの最終行にチェックが入りますが、grid が画面に表示しきれていない場合は、最終行までスクロールが進みます(スクロールする様子が分かりやすいよう、grid の height を 100px に指定しています):

grid_checkboxModel2

ソースコードを見てみる

grid だけでスクロールする場合はこれで問題ありませんが、grid がスクロールせずに panel に張り付いている場合などは、panel ごと画面がスクロールしてしまうため都合が悪い場合があります。

この処理を行っているソースコードを追ってみます。

クラスの継承関係

CheckboxModel クラスの継承関係は次の通りです:

Ext.selection.CheckboxModel -> Ext.selection.RowModel -> Ext.selection.Model

select メソッドから辿る

ソース2 で呼んだ select メソッドは、CheckboxModel クラスにも RowModel クラスにも定義されていないため、さらに上位の Model クラスでの定義が呼ばれます:

Ext.define('Ext.selection.Model', {
    ...
    select: function(records, keepExisting, suppressEvent) {
        // Automatically selecting eg store.first() or store.last() will pass undefined, so that must just return;
        if (Ext.isDefined(records)) {
            this.doSelect(records, keepExisting, suppressEvent);
        }
    },
    ...

doSelect メソッドを見てみます:

Ext.define('Ext.selection.Model', {
    ...
    doSelect: function(records, keepExisting, suppressEvent) {
        ...
        if (me.selectionMode == "SINGLE" && records) {
            record = records.length ? records[0] : records;
            me.doSingleSelect(record, suppressEvent);
        } else {
            me.doMultiSelect(records, keepExisting, suppressEvent);
        }
    },
    ...

me.selectionMode は CheckboxModel クラスにて 'MULTI' に設定されているため、次は doMultiSelect メソッドが呼ばれます:

Ext.define('Ext.selection.Model', {
    ...
    doMultiSelect: function(records, keepExisting, suppressEvent) {
        ...
        for (; i < len; i++) {
            record = records[i];
            if (keepExisting && me.isSelected(record)) {
                continue;
            }
            me.lastSelected = record;

            me.onSelectChange(record, true, suppressEvent, commit);
        }
        if (!me.preventFocus) {
            me.setLastFocused(record, suppressEvent);
        }
        // fire selchange if there was a change and there is no suppressEvent flag
        me.maybeFireSelectionChange(change && !suppressEvent);
    },
    ...

setLastFocused というメソッドが怪しそうです。

Ext.define('Ext.selection.Model', {
    ...
    setLastFocused: function(record, supressFocus) {
        var me = this,
            recordBeforeLast = me.lastFocused;

        me.lastFocused = record;

        // Only call the changed method if in fact the selected record *has* changed.
        if (record !== recordBeforeLast) {
            me.onLastFocusChanged(recordBeforeLast, record, supressFocus);
        }
    },
    ...

ここで onLastFocusChanged メソッドが登場しますが、このメソッドは Model クラスと子クラスの RowModel クラスで定義されています。つまり子クラスである RowModel の onLastFocusChanged が呼ばれるのですが、その中で callParent() することで Model クラスの onLastFocusChanged へ戻る流れになっています。

RowModel 内の onLastFocusChanged メソッドから続く一連の処理では対象行にスクロールさせる処理が含まれているため、ここを回避することができれば目的達成です。

解決策

preventFocus プロパティを使う

ソース5 に、preventFocus プロパティを利用した条件分岐がありました:

Ext.define('Ext.selection.Model', {
    ...
    doMultiSelect: function(records, keepExisting, suppressEvent) {
        ...
        if (!me.preventFocus) {
            me.setLastFocused(record, suppressEvent);
        }
        ...

この条件分岐に入らなければ、対象行にフォーカスが当たることはありません。

しかし preventFocus プロパティは ExtJS のマニュアルには載っていないため、プライベートなプロパティとして利用されています。下記プログラムで目的は達成できましたが、CheckboxModel で複雑な処理を行おうとしている場合は注意が必要かもしれません(確認したところ、CheckboxModel クラスの onHeaderClick メソッド内では delete 演算で preventFocus が削除されるため、プライベートなプロパティであることは間違いありません)。

var checkBoxModel = Ext.create('Ext.selection.CheckboxModel', {
  preventFocus : true
});

onLastFocusChanged メソッドを上書きして、callParent() しない

Model, RowModel クラスで実装されていた onLastFocusChanged メソッドを、CheckboxModel クラスで定義し、処理を奪う方法もあります:

var checkBoxModel = Ext.create('Ext.selection.CheckboxModel', {
  onLastFocusChanged: function() { return; }
});

上位クラスの同メソッドが実行されないため、必要な処理をこのメソッド内に移植する必要があります。

例えば、Model クラスでは同メソッド内で focuschange イベントを発火しているため、このイベントが必要な場合は上記メソッド内で発火する必要があります。

注:suppressEvent, suppressFocus は afterrender / viewready イベント内では効かない

ソースコードを眺めている中で、メソッドの引数に suppressEvent や suppressFocus という変数があったと思います。

select メソッドの引数に suppressEvent を false として与えれば解決…となれば綺麗なのですが、上手くいきません。せっかく focus を回避しても、store が紐づいた view が初期化 されるタイミングで onLastFocusChanged メソッドが再度呼ばれてしまいます。こちらは suppressEvent を渡さないで setLastFocused を呼ぶため、focus が回避できません。これは afterrender より後のイベント viewready でも同様です。

実験として、afterrender で setTimeout を入れ、コールバックとして最終行を select させると、suppressEvent が効いた状態で select させることができます:

        ...
        listeners : {
          afterrender: function(thisGrid) {
            var store     = thisGrid.getStore();
            var lastIndex = store.getCount() - 1;
            // 10秒後に select を実行
            setTimeout( function() {
              checkBoxModel.select(lastIndex, false, true);
            }, 10000);
          }
        }
        ...

…ということで、あまりスッキリした解決策が見当たりません。プロパティの見落としや、afterrender / viewready 以外のタイミングで処理を行えば回避できる可能性があればぜひ教えてください。