初心者のためのExtJS入門

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

suspendLayouts・resumeLayouts

今回はExt.suspendLayouts、Ext.resumeLayoutsでレンダリングのチューニングを試してみます。

これを使うとDOMのレンダリングをまとめられます。

Ext.suspendLayoutsを呼び出すと、フレームワーク内部で持っているレイアウト中断のカウンタがインクリメントされ、

Ext.resumeLayoutsを引数無しで呼び出すと、フレームワーク内部で持っているレイアウト中断のカウンタがデクリメントされます。

このカウンタが0になると、レンダリングが実行されるという仕組みです(そういえばCOMでこんなの(参照カウンタ)あったなー、と思いました)。

Ext.resumeLayoutsにtrueを渡して呼び出すと、カウンタが強制的に0になります。

試してみる

検証のため1000個のボタンを追加してみます。

A. Ext.suspendLayouts、Ext.resumeLayoutsを使わない場合

Ext.define('Sample.view.sample.PanelController', {
    extend: 'Ext.app.ViewController',
    alias: 'controller.sample_panel',

    onClickBulkAddButton: function () {
        var me = this,
            view = me.getView();

        console.time('normal');

        for (var n = 1; n <= 1000; n++) {
            view.add({
                xtype: 'button',
                text: '追加されたボタン' + n
            });
        }

        console.timeEnd('normal');
    }
});

B. Ext.suspendLayouts、Ext.resumeLayoutsを使う場合

Ext.define('Sample.view.sample.PanelController', {
    extend: 'Ext.app.ViewController',
    alias: 'controller.sample_panel',

    onClickBulkAddButton: function () {
        var me = this,
            view = me.getView();

        console.time('suspendAndResume');

        // MEMO: Ext.suspendLayoutsとExt.resumeLayoutsで挟む
        Ext.suspendLayouts();

        for (var n = 1; n <= 1000; n++) {
            view.add({
                xtype: 'button',
                text: '追加されたボタン' + n
            });
        }

        Ext.resumeLayouts(true);

        console.timeEnd('suspendAndResume');
    }
});

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

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

A.は約1.8秒, B.は約0.6秒という結果になり、Ext.suspendLayouts、Ext.resumeLayoutsを使った方が速いことがわかります。

アイコンフォントのパッケージ作成

前回はアイコンフォントの使い方を取り上げましたが、今回はアイコンフォントのパッケージを作成してみます。

標準のアイコンフォントだと思っているものが見つからないことがあります。

その場合、デザイナーに作ってもらったり、自分で適当なアイコン画像を探したりすることになるでしょう。

追加した画像を一つのフォントファイルにまとめてパッケージとして用意できれば、管理もしやすいし、アイコン画像の複数回読み込みがフォントファイル1回で済んだりと非常に便利になります。

あと、ベクタ画像になります。

フォントファイルの作成

フォントの作成には、IcoMoonのサービスを利用しました。

https://icomoon.io/app/

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

IcoMoonで標準で用意されているアイコンセットを使えたり、IcoMoonライブラリから有料でアイコンセットを購入して使えたり、自分でsvgファイルとして用意したファイルを使えたりと、

とにかく便利です。

自分でsvgファイルを用意した場合は、画面左上の「Import icons」ボタンからファイルをインポートすると、アイコンとして登録されます。

あとは、フォントファイルに含めたいアイコンを選択し、画面右下の「Generate Font」ボタンから作成し、ダウンロードできます。

ダウンロードしたファイルを展開すると、下記のようなセットになっています。

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

パッケージ作成

次にパッケージを作成します。

sencha generate package font-icomoon

パッケージ名は何でも良いですが、今回はfont-icomoonにしました。

フォントファイルとスタイルシートの設置

パッケージのresourcesにフォントを設置します。

既存のフォントパッケージに合わせて、resources/fontsに配置しました。

さらにstyle.css => style.scssとリネームして、sass/srcに配置します。

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

style.scssを一部修正し、フォントファイルをロードできるようにします。

@charset "UTF-8";

$icomoon-fonts-path: "font-icomoon/fonts";

@font-face {
  font-family: 'icomoon';
  src:  url('#{$icomoon-fonts-path}/icomoon.eot?hj34vj');
  src:  url('#{$icomoon-fonts-path}/icomoon.eot?hj34vj#iefix') format('embedded-opentype'),
    url('#{$icomoon-fonts-path}/icomoon.ttf?hj34vj') format('truetype'),
    url('#{$icomoon-fonts-path}/icomoon.woff?hj34vj') format('woff'),
    url('#{$icomoon-fonts-path}/icomoon.svg?hj34vj#icomoon') format('svg');
  font-weight: normal;
  font-style: normal;
}

最後にpackage.jsonを修正します。

"sass" : {
    "src": [
        "${package.dir}/sass/src",
        "${package.dir}/sass/src/style.scss"  // 追記
    ]
}

使ってみる

さっそく使ってみます。

まずはパッケージなので、app.jsonのrequiresにfont-icomoonを追記しましょう。

Ext.define('Sample.view.sample.Panel', {
    extend: 'Ext.panel.Panel',
    xtype: 'sample_panel',

    cls: 'sample-panel',

    padding: 50,

    items: {
        xtype: 'button',
        iconCls: 'icon-insomnia'
    },

    html: '<i class="icon-insomnia"></i>'
});

セレクタ名はstyle.scssに載っています。

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

追加したアイコンがうまく表示されました。

アイコンフォント

ExtJS6.5では、FontAwesomeとPictosがパッケージに含まれています。

今回はそれらの使い方を取り上げます。

まずはapp.jsonでアイコンフォントのパッケージをrequiresに追記します。

"requires": [
  "font-awesome",
  "font-pictos"
]

あとはclass属性に使用したいフォントのセレクタ名を正しく指定するだけです。

例えばHTMLタグであればiタグに、ボタンのアイコンならiconClsに指定します。

Ext.define('Sample.view.sample.Panel', {
    extend: 'Ext.panel.Panel',
    xtype: 'sample_panel',

    cls: 'sample-panel',

    padding: 50,
    bodyPadding: '15 0 0 0',

    tbar: {
        items: [
            {
                xtype: 'button',
                tooltip: '編集',
                iconCls: 'pictos pictos-pencil',
                scale: 'large'
            },
            {
                xtype: 'button',
                tooltip: '編集',
                iconCls: 'x-fa fa-pencil',
                scale: 'large'
            }
        ]
    },

    html: [
        '<i class="pictos pictos-pencil"></i>',
        '<i class="x-fa fa-pencil"></i>'
    ]
});

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

同じようなアイコンでもフォントで微妙に違いがでますね。

FontAwesomeの場合はx-fa fa-xxxx、Pictosの場合はpictos pictos-xxxxという形式になります。

FontAwesomeの場合は、http://fontawesome.io/icons/ を見るのが良いと思います。気を付けないといけないのは、ExtJSパッケージのフォントのバージョンが古い場合があるということです。

Pictosはいまいち一覧でセレクタ名までわかるページが無いんですよね。http://pictos.cc/classic/font を参照しつつ、セレクタ名は ext/packages/font-pictos/all.scss を直接みるのが早いかも。

 

 

 

ところでclassicの場合、ボタンではglyphコンフィグというものを使うこともできます(ExtJS5では結構使いました)。

が、modernには無いこととiconClsで同じUIで表示できることから今はほとんど使う必要がなくなっています。

Googleマップ

今回はExtJSからGoogleマップを表示してみます。

classicでの使い方

classicでは、Ext.ux.GMapPanelを使います(http://docs.sencha.com/extjs/6.5.1/classic/Ext.ux.GMapPanel.html)。

まずは、をロードしておきます。これはindex.htmlか、Ext.Loader.loadScriptで読み込んでおけば良いでしょう。

そのうえで、下記のようになります。

Ext.define('Sample.view.map.Panel', {
    extend: 'Ext.ux.GMapPanel',
    xtype: 'map_panel',

    gmapType: 'map',

    center: {
        geoCodeAddr: "鹿児島県庁",
        marker: {
            title: '鹿児島県庁'
        }
    },

    mapOptions: {
        mapTypeId: google.maps.MapTypeId.ROADMAP
    }
});

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

ドキュメントには定義されていませんが、centerコンフィグには中心位置に関する情報を指定します。

markersコンフィグでマーカーは複数設定できます。

マーカーの追加にはaddMakerメソッド(戻り値はgoogle.maps.Marker)が用意されているようですが、削除は自力でGoogle Map APIを呼ぶ必要があるようです。

modernでの使い方

modernでは、Ext.ux.google.Mapを使います(http://docs.sencha.com/extjs/6.5.1/modern/Ext.ux.google.Map.html)。

Google Map APIの使い方をあえてclassicに合わせると、まずapp.jsonマッシュアップの設定を追加します。

app.json

"mashup": {
    "map": {
        "options": "?v=3&sensor=false"
    }
}

Ext.ux.google.Mapは、下記のような定義があり、app.jsonのoptionsの値を設定したリクエストでGoogle Map APIを利用します。

requiredScripts: [
    '//maps.googleapis.com/maps/api/js{options}'
]

あとは下記のようになります。

/**
 * マップパネルクラス。
 *
 * @class Sample.view.map.Panel
 * @extend Ext.Panel
 */
Ext.define('Sample.view.map.Panel', {
    extend: 'Ext.Panel',
    xtype: 'map_panel',

    requires: [
        'Ext.ux.google.Map'
    ],

    layout: 'fit',

    items: {
        xtype: 'map',
        useCurrentLocation: false,
        mapOptions: {
            zoom: 16,
            center: {
                lat: 31.560236,
                lng: 130.557855
            }
        },
        markers: [
            {
                position: {
                    lat: 31.560236,
                    lng: 130.557855
                }
            }
        ]
    }
});

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

classicと異なり、geoCodeAddrが直接使えないみたいです。この点で少し使いにくい気がします。

最後に

ExtJSのクラスは結局のところ、Google Map APIの導入を簡単にしてくれているだけなので、必ずしも使う必要はありません。

最初に気にしておくべきは、APIのバージョンアップ時に対応できるかどうかという点だと思います。

ExtJSのマップ用クラスを使っていて、もしGoogle Map APIが大きく変わるような場合、どのように対処すれば良いか分かっている場合は採用しても問題ないでしょう。

そうでなければ、いっそ自分で組み込んだ方がその後の負担は少なくなるかもしれません。

スポットライト[classic]

今回はExt.ux.Spotlightの紹介です。

Exampleを眺めていたら見つけて試したくなりました(http://examples.sencha.com/extjs/6.5.1/examples/classic/core/spotlight.html)。

これを使うと、特定の要素を目立たせることができます。

使い方

Ext.ux.Spotlightのインスタンスで目立たせたい要素を引数にしてshowメソッドを実行します。

var button1 = panel.down('#button1'),
    spot = Ext.create('Ext.ux.Spotlight');

spot.show(button1.getEl());

すると、下記のようにボタン1をマスク表示で囲うような表現となります。

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

スポットライト用のスタイルが見当たらなかったので、自分で定義する必要があるようです。

ここでは下記のようにしています。

.#{$prefix}spotlight {
  background-color: #999;
  z-index: 8999;
  position: absolute;
  top: 0;
  left: 0;
  @include opacity(.5);
  width: 0;
  height: 0;
  zoom: 1;
  font-size: 0;
}

応用

サービスへの初回ログイン時のナビゲーションに使えそうなので、それっぽいものを試してみました。

f:id:sham-memo:20170903142205g:plain

gifにしたので画質が悪くなっています。。。

しかし、それなりに形になりました。これから作るアプリケーションでは組み込んでいこうと思います。

メインのビュー

Ext.define('Sample.view.sample.Panel', {
    extend: 'Ext.panel.Panel',
    xtype: 'sample_panel',

    controller: 'sample_panel',

    cls: 'sample-panel',

    layout: 'fit',

    listeners: {
        afterlayout: 'onAfterLayout'
    },

    dockedItems: {
        xtype: 'toolbar',
        cls: 'nav-toolbar',
        items: [
            {
                xtype: 'label',
                cls: 'brand',
                text: 'ロゴ'
            },
            '->',
            {
                reference: 'envelope_button',
                iconCls: 'x-fa fa-envelope',
                scale: 'large'
            },
            {
                reference: 'comment_button',
                iconCls: 'x-fa fa-comment',
                scale: 'large'
            },
            {
                reference: 'info_button',
                iconCls: 'x-fa fa-info-circle',
                scale: 'large'
            }
        ]
    },

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

Ext.define('Sample.view.sample.PanelController', {
    extend: 'Ext.app.ViewController',
    alias: 'controller.sample_panel',

    requires: [
        'Ext.ux.Spotlight'
    ],

    helpRefs: [
        'envelope_button',
        'comment_button',
        'info_button'
    ],

    helpInfo: {
        'envelope_button': {
            text: 'メンテナンスや新機能追加のお知らせは、こちらからご確認いただけます。',
            align: 'tr-br',
            offset: [0, 10]
        },
        'comment_button': {
            text: 'ユーザからのコメントは、こちらからご確認いただけます。',
            align: 'tr-br',
            offset: [0, 10]
        },
        'info_button': {
            text: '分からない機能があれば、このボタンからヘルプをご確認いただけます。',
            align: 'tr-br',
            offset: [0, 10]
        }
    },

    config: {
        /**
         * @cfg {Ext.ux.Spotlight} スポットライトオブジェクト。
         */
        spot: null,

        /**
         * @cfg {Number} 現在表示しているヘルプ参照のインデックス。
         */
        currentHelpIndex: null,

        /**
         * @cfg {Sample.view.balloon.Panel} 吹き出しパネル。
         */
        balloon: null
    },

    /**
     * afterlayoutイベント時の処理。
     */
    onAfterLayout: function () {
        this.runHelp();
    },

    /**
     * ヘルプの表示を開始する。
     */
    runHelp: function () {
        var me = this,
            refInfo = me.currentHelpRefInfo();

        me.showHelp(refInfo);
    },

    /**
     * 前のヘルプを表示する。
     */
    showPrevHelp: function () {
        var me = this,
            refInfo = me.prevHelpRefInfo();

        me.showHelp(refInfo);
    },

    /**
     * 次のヘルプを表示する。
     */
    showNextHelp: function () {
        var me = this,
            refInfo = me.nextHelpRefInfo();

        me.showHelp(refInfo);
    },

    /**
     * ヘルプを表示する。
     * @param {object} helpRefInfo ヘルプ参照情報
     */
    showHelp: function (helpRefInfo) {
        var me = this,
            spot = me.getSpot(),
            ref = helpRefInfo.ref,
            target = me.lookupReference(ref),
            helpInfo = me.helpInfo[ref];

        me.setCurrentHelpIndex(helpRefInfo.index);

        // 前の吹き出しが残っていたら破棄する
        me.destroyPreBalloonPanel();

        // スポットライトを当てる
        spot.show(target.getEl());

        // 吹き出し表示
        me.showBallonPanel({
            target: target,
            text: helpInfo.text,
            first: helpRefInfo.first,
            last: helpRefInfo.last,
            align: helpInfo.align,
            offset: helpInfo.offset
        });
    },

    /**
     * 吹き出しを表示する。
     * @param {object} params パラメータ
     */
    showBallonPanel: function (params) {
        var me = this;

        Ext.defer(function () {
            var balloonPanel = Ext.widget('sample_balloon_panel', {
                html: params.text,
                first: params.first,
                last: params.last,
                listeners: {
                    prev: 'onPrevBalloonPanel',
                    next: 'onNextBalloonPanel',
                    end: 'onEndBalloonPanel',
                    destroy: 'onDestroyBalloonPanel',
                    scope: me
                }
            });

            me.setBalloon(balloonPanel);

            balloonPanel.showBy(params.target, params.align, params.offset);
        }, params.first ? 500 : 100);
    },

    /**
     * 前の吹き出しを破棄する。
     */
    destroyPreBalloonPanel: function () {
        var me = this,
            balloon = me.getBalloon();

        if (balloon) {
            balloon.destroy();
        }
    },

    /**
     * 吹き出しパネルprevイベント時の処理。
     * @param {Sample.view.balloon.Panel} panel 吹き出しパネル
     */
    onPrevBalloonPanel: function (panel) {
        var me = this;

        panel.destroy();

        me.showPrevHelp();
    },

    /**
     * 吹き出しパネルnextイベント時の処理。
     * @param {Sample.view.balloon.Panel} panel 吹き出しパネル
     */
    onNextBalloonPanel: function (panel) {
        var me = this;

        panel.destroy();

        me.showNextHelp();
    },

    /**
     * 吹き出しパネルendイベント時の処理。
     * @param {Sample.view.balloon.Panel} panel 吹き出しパネル
     */
    onEndBalloonPanel: function (panel) {
        var me = this,
            spot = me.getSpot();

        panel.destroy();

        me.setCurrentHelpIndex(null);

        spot.hide();
    },

    /**
     * 吹き出しパネルdestroyイベント時の処理。
     */
    onDestroyBalloonPanel: function () {
        this.setBalloon(null);
    },

    /**
     * 現在の参照情報を返す。
     * @return {object} 現在の参照情報
     */
    currentHelpRefInfo: function () {
        var me = this,
            currentHelpIndex = me.getCurrentHelpIndex();

        if (currentHelpIndex === null) {
            currentHelpIndex = 0;
        }

        return me.getHelpRefInfo(currentHelpIndex);
    },

    /**
     * 前の参照情報を返す。
     * @return {object} 前の参照情報
     */
    prevHelpRefInfo: function () {
        var me = this,
            currentHelpIndex = me.getCurrentHelpIndex(),
            index;

        if (currentHelpIndex > 0) {
            index = currentHelpIndex - 1;
        } else {
            index = 0;
        }

        return me.getHelpRefInfo(index);
    },

    /**
     * 次の参照情報を返す。
     * @return {object} 次の参照情報
     */
    nextHelpRefInfo: function () {
        var me = this,
            currentHelpIndex = me.getCurrentHelpIndex(),
            index;

        if (currentHelpIndex === null) {
            index = 0;
        } else if (currentHelpIndex >= 0) {
            index = currentHelpIndex + 1;
        }

        return me.getHelpRefInfo(index);
    },

    /**
     * 指定されたインデックスのヘルプ参照情報を返す。
     *
     * @param {number} index インデックス
     * @return {object} ヘルプ参照情報
     */
    getHelpRefInfo: function (index) {
        var me = this,
            helpRefs = me.helpRefs,
            refInfo = {
                ref: null,
                first: false,
                last: false,
                index: index
            };

        if (index === 0) {
            refInfo.first = true;
        } else if (index === helpRefs.length - 1) {
            refInfo.last = true;
        }

        refInfo.ref = helpRefs[index];

        return refInfo;
    },

    // @override
    getSpot: function () {
        var me = this,
            spot = me.callParent(arguments);

        if (!spot) {
            spot = Ext.create('Ext.ux.Spotlight', {
                easing: 'easeOut',
                duration: 300
            });
            me.setSpot(spot);
        }

        return spot;
    }
});
@charset "UTF-8";

.sample-panel {
  .nav-toolbar {
    background-color: $base-color;

    .brand {
      color: #fff;
    }
  }
}

.#{$prefix}spotlight {
  background-color: #999;
  z-index: 8999;
  position: absolute;
  top: 0;
  left: 0;
  @include opacity(.5);
  width: 0;
  height: 0;
  zoom: 1;
  font-size: 0;
}

吹き出し部分

ビューモデルやビューコントローラも使わず、スタイルも吹き出しっぽくしてなくて、結構手抜きです。

Ext.define('Sample.view.sample.balloon.Panel', {
    extend: 'Ext.panel.Panel',
    xtype: 'sample_balloon_panel',

    cls: 'sample-balloon-panel',

    floating: true,
    bodyPadding: 15,
    maxWidth: 300,

    // @override
    initComponent: function () {
        var me = this;

        Ext.apply(me, {
            buttons: [
                {
                    text: 'Prev',
                    hidden: me.first,
                    handler: function () {
                        var p = this.up('sample_balloon_panel');
                        p.fireEvent('prev', p);
                    },
                    scope: 'this'
                },
                {
                    text: 'Next',
                    hidden: me.last,
                    handler: function () {
                        var p = this.up('sample_balloon_panel');
                        p.fireEvent('next', p);
                    },
                    scope: 'this'
                },
                {
                    text: 'End',
                    hidden: !me.last,
                    handler: function () {
                        var p = this.up('sample_balloon_panel');
                        p.fireEvent('end', p);
                    },
                    scope: 'this'
                }
            ]
        });

        me.callParent(arguments);
    }
});
@charset "UTF-8";

.sample-balloon-panel {
  background-color: #fff;

  .#{$prefix}panel-body {
    color: #666;
    font-size: 16px;
    line-height: 1.2;
  }
}

任意のコンフィグを双方向バインディングに対応する

ビュー作成時に独自のコンフィグを追加することがあります。

ここでは追加したコンフィグをデータバインディングしてみます。

Ext.define('Sample.view.sample.Panel', {
    extend: 'Ext.panel.Panel',
    xtype: 'sample_panel',

    config: {
        text: null
    },

    applyText: function (text) {
        var me = this;

        me.setHtml(Ext.String.htmlEncode(text));

        return text;
    }
});

まずはtextコンフィグを用意しました。

textコンフィグに値を設定すると、HTMLエンコードして画面に表示するようにしています。

Ext.define('Sample.view.sample.PanelModel', {
    extend: 'Ext.app.ViewModel',
    alias: 'viewmodel.sample_panel',

    data: {
        text: '<b>Hello, World!!</b>'
    }
});

textという値を持ったビューモデルを用意しました。

これをビューにデータバインディングします。

Ext.define('Sample.view.sample.Panel', {
    extend: 'Ext.panel.Panel',
    xtype: 'sample_panel',

    viewModel: 'sample_panel',

    config: {
        text: null
    },

    bind: {
        text: '{text}'
    },

    applyText: function (text) {
        var me = this;

        me.setHtml(Ext.String.htmlEncode(text));

        return text;
    }
});

これでビューモデル => ビューの一方向のデータバインディングとなります。

ビュー => ビューモデルについては、publishesコンフィグを指定する必要があります。

Ext.define('Sample.view.sample.Panel', {
    extend: 'Ext.panel.Panel',
    xtype: 'sample_panel',

    viewModel: 'sample_panel',

    config: {
        text: null
    },

    publishes: {
        text: true
    },

    bind: {
        text: '{text}'
    },

    applyText: function (text) {
        var me = this;

        me.setHtml(Ext.String.htmlEncode(text));

        return text;
    }
});

これで、ビュー側でtextの値が変更されるとビューモデルの値に反映されるようになります。

ツリーパネル[modern]

今回はmodernのツリーパネルです。

できることはclassicのツリーパネルとほぼ同じです。

基本的な使い方

モデルとストアはclassicのときと同じものを使っています。

ビューにはExt.grid.TreeとExt.list.Treeを使えます。

ビュー

まずはExt.grid.Treeを使った場合です。

Ext.define('Sample.view.sample.Panel', {
    extend: 'Ext.grid.Tree',
    xtype: 'sample_panel',

    store: 'Earnings',

    rootVisible: false
});

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

次にExt.list.Treeを使った場合です。

Ext.define('Sample.view.sample.Panel', {
    extend: 'Ext.list.Tree',
    xtype: 'sample_panel',

    store: 'Earnings',

    ui: 'nav',

    expanderFirst: false
});

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

Ext.list.Treeはuiが少し用意されてるみたいです。expanderFirstコンフィグはExt.grid.Treeでも使えます。

Ext.list.TreeはExt.Gadgetを継承しているみたいで、Ext.grid.Treeとは根本的に違うようです。

編集などの機能はExt.grid.Treeでしか実装できません。

スマフォサイズで単純なツリーパネルを使う場合、expanderFirst: falseのほうが個人的に好きなUIです。

ツリーグリッド

最後に、ツリーグリッドにしてさらに編集可能にしてみました。

Ext.define('Sample.view.sample.Panel', {
    extend: 'Ext.grid.Tree',
    xtype: 'sample_panel',

    requires: [
        'Ext.grid.column.Tree',
        'Ext.grid.column.Number',
        'Ext.grid.plugin.CellEditing'
    ],

    store: 'Earnings',

    rootVisible: false,

    rowLines: true,
    columnLines: true,

    columns: [
        {
            text: 'グループ名',
            xtype: 'treecolumn',
            width: 300,
            flex: 1,
            dataIndex: 'text',
            editable: true
        },
        {
            text: '第1四半期',
            xtype: 'numbercolumn',
            format: '0,000',
            dataIndex: 'quarter1',
            align: 'right',
            editable: true
        },
        {
            text: '第2四半期',
            xtype: 'numbercolumn',
            format: '0,000',
            dataIndex: 'quarter2',
            align: 'right',
            editable: true
        },
        {
            text: '第3四半期',
            xtype: 'numbercolumn',
            format: '0,000',
            dataIndex: 'quarter3',
            align: 'right',
            editable: true
        },
        {
            text: '第4四半期',
            xtype: 'numbercolumn',
            format: '0,000',
            dataIndex: 'quarter4',
            align: 'right',
            editable: true
        }
    ],

    plugins: {
        gridcellediting: true
    }
});

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

ドラッグ&ドロップで入れ替えてみようと思いましたが、API見ても分かりませんでした。

知ってる人がいたらぜひ教えてください( )