初心者のためのExtJS入門

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

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

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'
    }
});

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

クラスのオーバーライド

今回はクラスのオーバーライドです。

クラスをオーバーライドすると、対象クラスのメソッドを上書きしたり、プロパティやメソッドを追加したりできます。

私がオーバーライドを使うのは↓のような場合です。

  • フレームワークが提供しているクラスに不具合があり修正が必要・・・
  • 良く使う機能を追加したい・・・
  • 必ず含めないといけないリクエストパラメータを追加したい・・・

オーバーライドしてみる

試しにExt.form.field.Textをオーバーライドしてみましょう。

app.jsonには↓の定義があり、オーバーライド用のファイルはoverrides、classic/overrides、modern/overridesディレクトリに作成していきます。

"overrides": [
    "overrides",
    "${toolkit.name}/overrides"
]

classicだけに適用したいので、classic/overridesにform/field/Text.jsを作成してみます。

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

最低限の記述は↓のようになります。overrideプロパティにオーバーライドしたいクラスを指定します。クラス名は何でも良いですが、私は大体、アプリケーション名.overrides.(オーバーライドするクラスのExtを除いた部分)とします。重複しないから。

/**
 * Ext.form.field.Textのオーバーライドクラス。
 *
 * @class Memo.overrides.form.field.Text
 * @override Ext.form.field.Text
 */
Ext.define('Memo.overrides.form.field.Text', {
    override: 'Ext.form.field.Text'
});

試しにpaddingを設定してみます。

/**
 * Ext.form.field.Textのオーバーライドクラス。
 *
 * @class Memo.overrides.form.field.Text
 * @override Ext.form.field.Text
 */
Ext.define('Memo.overrides.form.field.Text', {
    override: 'Ext.form.field.Text',

    padding: 20

});

↓こんな感じにpaddingが反映されてますね。テキストエリアはExt.form.field.Textを継承しているので、そちらも影響を受けているのが分かります。

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

特定の機能を追加してみる

私がオーバーライドで良く作成する処理を1つ紹介します。入力フィールドをclearable: trueにすると、値が入力されるとクリアボタンが表示されるようにしています。

/**
 * Ext.form.field.Textのオーバーライドクラス。
 *
 * @class Memo.overrides.form.field.Text
 * @override Ext.form.field.Text
 */
Ext.define('Memo.overrides.form.field.Text', {
    override: 'Ext.form.field.Text',

    /**
     * @cfg {Boolean} trueを設定するとクリア機能が利用できる。
     */
    clearable: false,

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

        config = config || {};

        if (config.clearable) {
            config.triggers = Ext.apply(config.triggers || {}, {
                clear: {
                    weight: 0,
                    cls: Ext.baseCSSPrefix + 'form-clear-trigger',
                    hidden: true,
                    handler: 'onClearClick',
                    scope: 'this'
                }
            })
        }

        me.callParent(arguments);

        if (config.clearable) {
            me.on('change', 'onChange', me);
        }
    },

    // @override
    afterRender: function(){
        var me = this;
        me.callParent();

        me.onChange();
    },

    /**
     * クリアボタンクリック時の処理。
     */
    onClearClick : function(){
        this.setValue('');
    },

    /**
     * changeイベント発火時の処理。
     */
    onChange: function() {
        var me = this,
            value = String(me.getValue() || '');

        value.length > 0 ? me.showClearButton() : me.hideClearButton();
    },

    /**
     * クリアボタンを表示する
     */
    showClearButton: function() {
        var me = this;

        if(me.triggers.clear) {
            me.triggers.clear.show();
        }
    },

    /**
     * クリアボタンを非表示にする
     */
    hideClearButton: function() {
        var me = this;

        if(me.triggers.clear) {
            me.triggers.clear.hide();
        }
    }
});

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

便利な機能の1つだと思いますが、継承しているクラスにも影響するので、オーバーライドしても大丈夫か注意して使うようにしましょう。

グリッドの機能:選択方法(セレクションモデル)[classic] (1)

Ext.grid.PanelのselModelコンフィグを指定すると、グリッドの行選択方法を変更できます。

ここにはセレクションモデル(Ext.selectionにあるクラス)を指定でき、主に下記3種類を使うと思います。

  • セル選択(Ext.selection.CellModel)
  • 行選択(Ext.selection.RowModel)
  • チェックボックス選択(Ext.selection.CheckboxModel)

selModelは↓のように指定します。

selModel: 'cellmodel'

selModel: {
  type: 'cellmodel',
  allowDeselect: true
}

selModel: Ext.create('Ext.selection.CellModel')

チェックボックス選択を試してみます。

/**
 * メモ一覧グリッドクラス。
 *
 * @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.selection.CheckboxModel'
    ],

    store: 'Memo',

    selModel: {
        type: 'checkboxmodel',
        ignoreRightMouseSelection: true,  // マウスの右クリック選択を禁止
        checkOnly: true  // チェックボックスのクリックだけで選択を有効にする
    },

    columns: [
        {
            dataIndex: 'id',
            text: 'ID',
            align: 'right',
            locked: true
        },
        {
            dataIndex: 'title',
            text: 'タイトル',
            width: 250
        },
        {
            dataIndex: 'body',
            text: '本文',
            flex: 1
        },
        {
            xtype: 'datecolumn',
            format: 'Y/m/d H:i:s',
            dataIndex: 'created',
            text: '登録日時',
            width: 200
        }
    ]
});

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

セレクションモデルの参照

現在選択している行や最後に選択した行など、グリッドの選択に関する情報はセレクションモデルから取得します。

セレクションモデルはgetSelectionModelメソッドで取得できます。

var grid = this.lookupRefreence('grid');

var selModel = grid.getSelectionModel();

// 選択行のモデル
var records = selModel.getSelection();

// 現在の選択件数
var count = selModel.getCount();

// 全選択
selModel.selectAll();

// 全選択解除
selModel.deselectAll();

特定データを選択できないようにする

beforeselectイベント時にイベントハンドラで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.column.Date',
        'Ext.selection.CellModel'
    ],

    store: 'Memo',

    selModel: 'cellmodel',

    columns: [
        {
            dataIndex: 'id',
            text: 'ID',
            align: 'right',
            locked: true
        },
        {
            dataIndex: 'title',
            text: 'タイトル',
            width: 250
        },
        {
            dataIndex: 'body',
            text: '本文',
            flex: 1
        },
        {
            xtype: 'datecolumn',
            format: 'Y/m/d H:i:s',
            dataIndex: 'created',
            text: '登録日時',
            width: 200
        }
    ],

    listeners: {
        beforeselect: function (selModel, record, row, col) {
            var position = selModel.getPosition();

            if (position.column.dataIndex === 'id') {
                // ID列は選択不可
                return false;
            }

        }
    }

});

colの値で判定しても良さそうですが、列をロックしている場合は注意が必要です。

ID列とタイトル列のセルを選択した場合、どちらもcolは0になるためです。

確実に列の情報を見るためには、セレクションモデルのgetPositionメソッドから選択している列情報を取得し、その内容を使って判定する必要があります。

グリッドの機能:編集[classic]

今回はグリッド上での編集機能です。

編集機能はプラグイン(のクラス)で提供されています。グリッド用のプラグインはExt.grid.pluginにあり、編集可能にするにはCellEditing(セル編集)とRowEditing(行編集)を使います。

セル編集

グリッドの1マスずつ編集する場合は、Ext.grid.plugin.CellEditing(http://docs.sencha.com/extjs/6.2.1/classic/Ext.grid.plugin.CellEditing.html)を使います。

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

    requires: [
        'Ext.form.field.Text',
        'Ext.grid.column.Date',
        'Ext.grid.plugin.CellEditing'
    ],

    store: 'Memo',

    plugins: [
        {
            ptype: 'cellediting'
        }
    ],

    columns: [
        {
            dataIndex: 'id',
            text: 'ID',
            align: 'right',
            locked: true
        },
        {
            dataIndex: 'title',
            text: 'タイトル',
            width: 250,
            editor: 'textfield',
            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
        }
    ],

    listeners: {
        edit: function (editor, e) {
            e.record.save();
        }
    }

});

タイトル列のセルをダブルクリックすると、テキストフィールドが表れて編集できます。

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

まずはプラグインの設定です。↑では、ptypeでエイリアス名を使って設定しています。clicksToEditなどのコンフィグもpluginで指定できます。この時点で画面上での編集操作が有効になります。

次にcolumnsで編集したい列にeditorを設定します。↑では、editor: 'textfield'としており、編集エディタにテキストフィールドを使うようにしています。

最後に保存処理です。編集操作の過程でbeforeedit,canceledit,edit,validateeditイベントが発生するので、編集操作完了時に発生するeditイベントで保存するようにしています。プロキシをローカルストレージにしているので、モデルのsaveメソッドで保存できます。

editorはオブジェクトリテラルにすることもでき、↓のようにすれば入力が必須になります。これだと、確定したときに入力エラーがある場合は、編集がキャンセルされて編集前の値に戻ります。

editor: {
    xtype: 'textfield',
    allowBlank: false
}

↓だと、確定したときに入力エラーがある場合にも編集エディタは閉じません。こっちのほうが多用する気がします。

editor: {
    field: {
        xtype: 'textfield',
        allowBlank: false
    },
    revertInvalid: false
}

1行編集

1行を編集する場合は、Ext.grid.plugin.RowEditing(http://docs.sencha.com/extjs/6.2.1/classic/Ext.grid.plugin.RowEditing.html)を使います。

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

    requires: [
        'Ext.form.field.Text',
        'Ext.form.field.TextArea',
        'Ext.grid.column.Date',
        'Ext.grid.plugin.RowEditing'
    ],

    store: 'Memo',

    plugins: [
        {
            ptype: 'rowediting'
        }
    ],

    columns: [
        {
            dataIndex: 'id',
            text: 'ID',
            align: 'right',
            locked: true
        },
        {
            dataIndex: 'title',
            text: 'タイトル',
            width: 250,
            editor: {
                field: {
                    xtype: 'textfield',
                    allowBlank: false
                },
                revertInvalid: false
            },
            renderer: function (value) {
                return Ext.String.htmlEncode(value);
            }
        },
        {
            dataIndex: 'body',
            text: '本文',
            flex: 1,
            editor: 'textarea',
            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
        }
    ],

    listeners: {
        edit: function (editor, e) {
            e.record.save();

            this.getView().refresh();
        },
        scope: 'this'
    }
});

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

今度は本文にもeditorを設定しています。テキストエリアで。

保存後にthis.getView().refresh()を実行していますが、テキストエリアで広がった高さが編集完了後にちゃんと調整されなかったので、手動で対応しています(ちなみにExtJS6.2.1です)。こういうのはExtJSで良くある残念なパターンです。。。

Ext.grid.column.Widgetを使って入力フィールドを表示したままにする

頻度は少ないかもしれないですが、こんな感じにもできます。これはプラグイン関係ないです。

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

    requires: [
        'Ext.form.field.Text',
        'Ext.form.field.TextArea',
        'Ext.grid.column.Date',
        'Ext.grid.column.Widget'
    ],

    store: 'Memo',

    columns: [
        {
            dataIndex: 'id',
            text: 'ID',
            align: 'right',
            locked: true
        },
        {
            dataIndex: 'title',
            text: 'タイトル',
            width: 250,
            xtype: 'widgetcolumn',
            widget: {
                xtype: 'textfield',
                allowBlank: false,
                listeners: {
                    change: function (field, value) {
                        var record = field.getWidgetRecord();

                        if (record && field.isValid()) {
                            record.set('title', value);
                            record.save();
                        }
                    }
                }
            }
        },
        {
            dataIndex: 'body',
            text: '本文',
            flex: 1,
            xtype: 'widgetcolumn',
            widget: {
                xtype: 'textarea',
                listeners: {
                    change: function (field, value) {
                        var record = field.getWidgetRecord();

                        if (record && field.isValid()) {
                            record.set('body', value);
                            record.save();
                        }
                    }
                }
            }
        },
        {
            xtype: 'datecolumn',
            format: 'Y/m/d H:i:s',
            dataIndex: 'created',
            text: '登録日時',
            width: 200
        }
    ]
});

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

ウィジェットとして、テキストフィールドとテキストエリアを表示させ、changeイベント時に保存しています。ちょっと無理やりな感じもしますが、まあ、こういうことも可能です(今はlistenersプロパティに直接イベントハンドラを設定していますが、きれいに整理するならビューコントローラに書くほうが良さげ)。

メソッドで編集を開始する

CellEditingプラグインの場合、startEditByPositionメソッドで編集を開始できます。そのためには、プラグインの参照を取得する必要があります。(ちなみにRowEditingプラグインの場合は、編集開始するメソッドはstartEditです)

プラグインは、グリッドのgetPluginメソッドで取得できます。その際、引数にプラグインのIDを指定するので、↓のようにIDを振っておきます。

plugins: [
    {
        ptype: 'cellediting',
        id: 'editing'
    }
]

あとはプラグインを取得して、startEditByPositionを実行するだけです。

var grid = this.lookupReference('grid');

var editing = grid.getPlugin('editing');

editing.startEditByPosition({
    row: 3,
    column: 2
});

グリッドの機能:カラム(列)[classic]

チュートリアルの登録画面あたりに不具合があったので一部訂正しました。あまりテストしていなかったので・・・。

さて、今回はグリッド(Ext.grid.Panel)の使い方です(チュートリアルで作ったメモアプリを流用しています)。

何回かに分けてグリッドの機能を取り上げてみようと思っています。まずはカラムについてです。あと、classicに絞っています。modernはまた改めて・・・(ところでスマフォ表示でグリッドってそんなに使うのかな?)。

グリッドは、http://examples.sencha.com/extjs/6.2.0/examples/kitchensink/#array-gridにあるように表形式を表現するのに便利なビューコンポーネントです。

非常に多機能で、ソートや並べ替え、ページングなど色々なことができるので利用頻度が高いです。

とりあえず表示

グリッドパネルを表示させる最低限のコードは、↓になります。

storeとcolumnsを指定すれば、ひとまず表示はできます(データを出さなくても良いならcolumnsだけで表示できます)。

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

    store: 'Memo',

    columns: [
        {
            dataIndex: 'title',
            text: 'タイトル',
            width: 250
        },
        {
            dataIndex: 'body',
            text: '本文',
            flex: 1
        }
    ]

});

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

データビューと同じようにstoreに使用するストアを設定します。

そして、columnsに列を定義します。上記コードだと、タイトルと本文の列が表示されます。タイトルは、width:250で幅を250ピクセルにしています。そして、本文に設定しているflex:1で、タイトル部以外に余っている横幅を埋めるように目一杯広げるようになります。パネルの幅が狭いときには、かなりつぶれて表示されることもあるので、最低限の幅を持たせたいならminWidthを付けておくと良いかもしれません。

これで表示はできました。このような最低限の状態でも、列見出し部分をクリックすれば並べ替えができたり、列の位置をドラッグ&ドロップで入れ替えたりできるようになっています。

カラム(列)

さきほどのコードにもありましたが、カラムの定義はcolumnsに設定します。カラムの機能を使って、さらに改良してみます。

/**
 * メモ一覧グリッドクラス。
 *
 * @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'
    ],

    store: 'Memo',

    columns: [
        {
            dataIndex: 'id',
            text: 'ID',
            align: 'right',
            locked: true
        },
        {
            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:20170201140712p:plain

align、locked、rendererというプロパティと、Ext.grid.column.Dateというカラムの派生クラスを使ってみました。

alignを使うと表示位置を設定できます。これでIDを右寄せにしました。

lockedをtrueにすると、横スクロールがある場合に、列を固定することができます。Excelで列をロックするような感じになります。

rendererには表示内容を変換するための関数を設定できます。これで値に応じて表示を柔軟に変更できます。上記コードでは、第1引数の値しか使っていませんが、引数の数はもう少しあります(http://docs.sencha.com/extjs/6.2.1/classic/Ext.grid.column.Column.html#cfg-renderer)。よく使うのは第3引数のrecordぐらいまでです。

よく使うような機能はカラムの派生クラスとして用意されている場合があります。その一つがExt.grid.column.Dateです。これは日付項目のフォーマットに使えます。他にもExt.grid.column.NumberやExt.grid.column.Widgetなどがあります。

Ext.grid.column.Widgetを使ってみる

次はExt.grid.column.Widgetを試してみます。

このカラムを使うとグリッド内にいろいろなビューを埋め込むことができます(http://examples.sencha.com/extjs/6.2.0/examples/kitchensink/#widget-grid)。

ここではExt.grid.column.Widgetを使って削除ボタンを設置して1行のデータ削除をやってみました。

/**
 * メモ一覧グリッドクラス。
 *
 * @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'
    ],

    store: 'Memo',

    columns: [
        {
            dataIndex: 'id',
            text: 'ID',
            align: 'right',
            locked: true
        },
        {
            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
        },
        {
            xtype: 'widgetcolumn',
            width: 60,
            widget: {
                xtype: 'button',
                iconCls: 'x-fa fa-times',
                tooltip: '削除',
                handler: function (btn) {
                    var record = btn.getWidgetRecord(),
                        store = record.store;

                    Ext.Msg.confirm('確認', 'メモを1件削除します。よろしいですか?', function (btn) {
                        if (btn === 'yes') {
                            store.remove(record);
                            store.sync();
                        }
                    });
                }
            }
        }
    ]

});

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

widgetプロパティに表示したいコンポーネント設定します。ボタンを設置するのでxtype: 'button'となっています。

今回は例なので、handlerプロパティで直接ストアからデータを消すようにしています。Ext.grid.column.Widgetを使った場合、getWidgetRecordメソッドを使って対象行のモデルを取得できます。

おまけにExt.grid.column.Templateを使ってみる

このクラスは知らなかったので試してみました。たまに調べてみると見つけるんですよねw

/**
 * メモ一覧グリッドクラス。
 *
 * @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.Template',
        'Ext.grid.column.Date'
    ],

    store: 'Memo',

    columns: [
        {
            xtype: 'templatecolumn',
            tpl: '{id}:{[Ext.String.htmlEncode(values.title)]}',
            text: 'ID:タイトル',
            width: 250
        },
        {
            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
        }
    ]
});

意味はないですが、IDとタイトルをExt.grid.column.Templateで出力するように変更してみました。複数項目をシンプルな形式で表示するのであれば、このクラスを使ったほうがスマートに書けそうです(今後は実務でも使っていこう)。

カラムの段組

columnsをネストすると、段組みできます。

/**
 * メモ一覧グリッドクラス。
 *
 * @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'
    ],

    store: 'Memo',

    columns: [
        {
            dataIndex: 'id',
            text: 'ID',
            align: 'right',
            locked: true
        },
        {
            text: 'メモ',
            flex: 1,
            columns: [
                {
                    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
                },
                {
                    xtype: 'widgetcolumn',
                    width: 60,
                    widget: {
                        xtype: 'button',
                        iconCls: 'x-fa fa-times',
                        tooltip: '削除',
                        handler: function (btn) {
                            var record = btn.getWidgetRecord(),
                                store = record.store;

                            Ext.Msg.confirm('確認', 'メモを1件削除します。よろしいですか?', function (btn) {
                                if (btn === 'yes') {
                                    store.remove(record);
                                    store.sync();
                                }
                            });
                        }
                    }
                }
            ]
        }
    ]

});

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

カラムの標準機能のON/OFFを変更する

グリッドのカラムは最初から並べ替えや移動ができるのですが、列によっては使えないようにしたい場合があります。それには下記のプロパティを使います。

sortable ・・・ 並べ替えの設定

hideable ・・・ 非表示の設定

menuDisabled ・・・ カラム右側のメニューを無効にする設定

draggable ・・・ 移動の設定

 

 

 

 

 

しばらくは、このような感じでグリッドの機能を取り上げていく予定です。

テーマの変更

テーマは、アプリケーション全体のデザインのことを指します。

テーマの設定

app.jsonの↓にあるthemeプロパティで使用するテーマを設定します。

"builds": {
    "classic": {
        "toolkit": "classic",
        
        "theme": "theme-triton",
        
        "sass": {
            "generated": {
                "var": "classic/sass/save.scss",
                "src": "classic/sass/save"
            }
        }
    },

    "modern": {
        "toolkit": "modern",
        
        "theme": "theme-triton",
        
        "sass": {
            "generated": {
                "var": "modern/sass/save.scss",
                "src": "modern/sass/save"
            }
        }
    }
}

予め、ExtJS標準でいくつかのテーマが用意されており、theme-tritonはその一つです。extディレクトリのclassic、modernにあるthemeで始まるディレクトリがテーマ用のパッケージです。

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

ちなみにmodernのおすすめはtheme-materialです。マテリアルデザインになり、https://material.io/icons/のアイコンが使えるようになります。

ベースカラーの変更

↓は自動生成したアプリケーションの初期画面です。この中の青色部分はベースカラーとして設定されており、sass変数base-colorを自分で定義することで変更できます。

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

一番簡単に設定するのであれば、アプリケーション直下にあるsassディレクトリのvar/all.scssに設定できます。

$enable-font-awesome: dynamic(true);

$base-color: #E57373;  // <-- 追記

すると↓のように、反映されます。

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

sass変数で指定できるスタイルはAPIドキュメントで分かるので、いろいろ試してみたら良いと思います。

classicだけとかmodernだけに適用させたいグローバルなsass変数がある場合は、例えばclassic/sass/var/Application.scssを作成して、そこに定義すれば良いです。

アプリケーションを本番用にビルドする

今回の話題はビルドです。

ビルドコマンド

開発途中は↓のコマンドを使います。チュートリアルでも、このコマンドを使っていました。このコマンドは簡易的なビルドとサーバ起動を行ってくれます。

sencha app watch [classic|modern]

本番環境で稼働させる場合は、↓のコマンドでビルドします。

sencha app build

このコマンドを実行すると、build/productionディレクトリが作成されます。

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

上記キャプチャだと、productionの下にSampleディレクトリが作成されています。これはアプリケーション名をSampleとしているためです。

このディレクトリの中には、圧縮されたjsやcss、画像ファイルやフォントファイルなどが作成されています。

classicとmodernディレクトリの下に、それぞれアプリケーションで作成したclassicとmodernのソース一式が出力されます。

この作成されたファイル一式を本番環境に適用すれば、基本的にはOKです。

実際、Apacheでproduction/Sampleをドキュメントルートに設定して動かすと、PCブラウザでアクセスするとclassicの画面が、スマフォなどのブラウザでアクセスするとmodernの画面が表示されます。

タグで単純に埋め込むなら

実務ではWebアプリケーションフレームワークを使っていることが多いことでしょう。そういう場面では「出力されたindex.htmlは使えないよ。既存のHTMLファイルにscriptタグで埋め込めないの?」という需要もあります。

その場合は、直接classic、modern以下のjs、cssを使うこともできます。modernのリソースを使う場合は、例えば↓のようになるでしょう。

<link rel="stylesheet" href="modern/resources/Sample-all.css"/>
<script type="text/javascript" src="modern/app.js"></script>

ただし、classicとmodernのどちらを使用するかの判定は自分でやる必要があります。最近はデバイス判定のライブラリも存在するので、そちらを使うのであれば直接タグを指定する記述でも十分でしょう。

私の場合は、PlayFrameworkをよく使っており、デバイスの判定はhttps://github.com/HaraldWalker/user-agent-utilsにまかせています。

app.json

app.jsonファイルには、アプリケーションの設定を記述します。

http://docs.sencha.com/cmd/guides/microloader.htmlが参考になります。

ビルドの出力先

ビルドの出力先は、app.json内のoutput.baseに定義されています。

"output": {
    "base": "${workspace.build.dir}/${build.environment}/${app.name}"
}

Webアプリケーションフレームワークの都合の良い場所に出力したい場合は、この値を変更するという方法があります。

もしくは、gulpやgruntなどのタスクツールで移動やコピーするのでも十分だと思います。

この辺りは色々やり方があると思うので、都合の良い方法を検討・選択しましょう。

キャッシュの設定

特に変更していない場合、productionはキャッシュが有効になっていると思います。

これは↓の記述に依ります。

"production": {
    "output": {
        "appCache": {
            "enable": true,
            "path": "cache.appcache"
        }
    },
    "loader": {
        "cache": "${build.timestamp}"
    },
    "cache": {
        "enable": true
    },
    "compressor": {
        "type": "yui"
    }
}

appCacheは、HTML5のアプリケーションキャッシュを使ったキャッシュです。

別途appCacheの定義がapp.json内に記載されており、そこに記述されているファイルがキャッシュされます。

cacheは、ローカルストレージを使ったキャッシュです。こちらはapp.jsをキャッシュします。

ちなみに、このキャッシュはindex.htmlを使う場合は影響しますが、タグを直接埋め込む場合は使えません。

おまけ

productionに出力されたindex.htmlには、classicとmodernのどちらを使用するかを判定するjavascriptの処理が存在します(正確にはMicroloaderの機能になるのでしょうが)。User Agentの内容で自動的に切り替わっていたのは、この処理のおかげです。将来、ブラウザの種類が増えた場合には、この判定処理も更新しないといけないので注意が必要です。ExtJSの更新に頼る、別ライブラリでデバイス判定する等、どのような準備をしておくか、あらかじめ検討しておきましょう。

<!DOCTYPE HTML>
<html>
<head>
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=10, user-scalable=yes">

    <title>Sample</title>

    
    <script type="text/javascript">
        var Ext = Ext || {}; // Ext namespace won't be defined yet...

        // This function is called by the Microloader after it has performed basic
        // device detection. The results are provided in the "tags" object. You can
        // use these tags here or even add custom tags. These can be used by platform
        // filters in your manifest or by platformConfig expressions in your app.
        //
        Ext.beforeLoad = function (tags) {
            var s = location.search,  // the query string (ex "?foo=1&bar")
                profile;

            // For testing look for "?classic" or "?modern" in the URL to override
            // device detection default.
            //
            if (s.match(/\bclassic\b/)) {
                profile = 'classic';
            }
            else if (s.match(/\bmodern\b/)) {
                profile = 'modern';
            }
            else {
                profile = tags.desktop ? 'classic' : 'modern';
                //profile = tags.phone ? 'modern' : 'classic';
            }

            Ext.manifest = profile; // this name must match a build profile name

            // This function is called once the manifest is available but before
            // any data is pulled from it.
            //
            //return function (manifest) {
                // peek at / modify the manifest object
            //};
        };
    </script>
    
    
    <!-- The line below must be kept intact for Sencha Cmd to build your application -->
    <script id="microloader" data-app="a8f635c1-23d2-4a4d-82f0-6e606e67fddd" type="text/javascript">var Ext=Ext||{};Ext.manifest=Ext.manifest||"modern.json";Ext=Ext||{};
Ext.Boot=Ext.Boot||function(f){function l(b){if(b.$isRequest)return b;b=b.url?b:{url:b};var e=b.url,e=e.charAt?[e]:e,a=b.charset||d.config.charset;x(this,b);delete this.url;this.urls=e;this.charset=a}function r(b){if(b.$isEntry)return b;var e=b.charset||d.config.charset,a=Ext.manifest,a=a&&a.loader,k=void 0!==b.cache?b.cache:a&&a.cache,c;d.config.disableCaching&&(void 0===k&&(k=!d.config.disableCaching),!1===k?c=+new Date:!0!==k&&(c=k),c&&(a=a&&a.cacheParam||d.config.disableCachingParam,c=a+"\x3d"+
c));x(this,b);this.charset=e;this.buster=c;this.requests=[]}var n=document,q=[],t={disableCaching:/[?&](?:cache|disableCacheBuster)\b/i.test(location.search)||!/http[s]?\:/i.test(location.href)||/(^|[ ;])ext-cache=1/.test(n.cookie)?!1:!0,disableCachingParam:"_dc",loadDelay:!1,preserveScripts:!0,charset:"UTF-8"},u=/\.css(?:\?|$)/i,w=n.createElement("a"),y="undefined"!==typeof window,v={browser:y,node:!y&&"function"===typeof require,phantom:window&&(window._phantom||window.callPhantom)||/PhantomJS/.test(window.navigator.userAgent)},
g=Ext.platformTags={},x=function(b,e,a){a&&x(b,a);if(b&&e&&"object"===typeof e)for(var d in e)b[d]=e[d];return b},c=function(){var b=!1,e=Array.prototype.shift.call(arguments),a,d,c,m;"boolean"===typeof arguments[arguments.length-1]&&(b=Array.prototype.pop.call(arguments));c=arguments.length;for(a=0;a<c;a++)if(m=arguments[a],"object"===typeof m)for(d in m)e[b?d.toLowerCase():d]=m[d];return e},a="function"==typeof Object.keys?function(b){return b?Object.keys(b):[]}:function(b){var e=[],a;for(a in b)b.hasOwnProperty(a)&&
e.push(a);return e},d={loading:0,loaded:0,apply:x,env:v,config:t,assetConfig:{},scripts:{},currentFile:null,suspendedQueue:[],currentRequest:null,syncMode:!1,useElements:!0,listeners:[],Request:l,Entry:r,allowMultipleBrowsers:!1,browserNames:{ie:"IE",firefox:"Firefox",safari:"Safari",chrome:"Chrome",opera:"Opera",dolfin:"Dolfin",edge:"Edge",webosbrowser:"webOSBrowser",chromeMobile:"ChromeMobile",chromeiOS:"ChromeiOS",silk:"Silk",other:"Other"},osNames:{ios:"iOS",android:"Android",windowsPhone:"WindowsPhone",
webos:"webOS",blackberry:"BlackBerry",rimTablet:"RIMTablet",mac:"MacOS",win:"Windows",tizen:"Tizen",linux:"Linux",bada:"Bada",chromeOS:"ChromeOS",other:"Other"},browserPrefixes:{ie:"MSIE ",edge:"Edge/",firefox:"Firefox/",chrome:"Chrome/",safari:"Version/",opera:"OPR/",dolfin:"Dolfin/",webosbrowser:"wOSBrowser/",chromeMobile:"CrMo/",chromeiOS:"CriOS/",silk:"Silk/"},browserPriority:"edge opera dolfin webosbrowser silk chromeiOS chromeMobile ie firefox safari chrome".split(" "),osPrefixes:{tizen:"(Tizen )",
ios:"i(?:Pad|Phone|Pod)(?:.*)CPU(?: iPhone)? OS ",android:"(Android |HTC_|Silk/)",windowsPhone:"Windows Phone ",blackberry:"(?:BlackBerry|BB)(?:.*)Version/",rimTablet:"RIM Tablet OS ",webos:"(?:webOS|hpwOS)/",bada:"Bada/",chromeOS:"CrOS "},fallbackOSPrefixes:{windows:"win",mac:"mac",linux:"linux"},devicePrefixes:{iPhone:"iPhone",iPod:"iPod",iPad:"iPad"},maxIEVersion:12,detectPlatformTags:function(){var b=this,e=navigator.userAgent,p=/Mobile(\/|\s)/.test(e),k=document.createElement("div"),h=function(){var a=
{},d,p,c,k,h;p=b.browserPriority.length;for(k=0;k<p;k++)d=b.browserPriority[k],h?c=0:(c=b.browserPrefixes[d],(c=(c=e.match(new RegExp("("+c+")([\\w\\._]+)")))&&1<c.length?parseInt(c[2]):0)&&(h=!0)),a[d]=c;a.ie&&(k=document.documentMode,8<=k&&(a.ie=k));c=a.ie||!1;d=Math.max(c,b.maxIEVersion);for(k=8;k<=d;++k)p="ie"+k,a[p+"m"]=c?c<=k:0,a[p]=c?c===k:0,a[p+"p"]=c?c>=k:0;return a}(),m=function(){var d={},p,c,k,h,m,f,z;k=a(b.osPrefixes);m=k.length;for(z=h=0;h<m;h++)c=k[h],p=b.osPrefixes[c],f=(p=e.match(new RegExp("("+
p+")([^\\s;]+)")))?p[1]:null,(p=!f||"HTC_"!==f&&"Silk/"!==f?p&&1<p.length?parseFloat(p[p.length-1]):0:2.3)&&z++,d[c]=p;k=a(b.fallbackOSPrefixes);m=k.length;for(h=0;h<m;h++)c=k[h],0===z?(p=b.fallbackOSPrefixes[c],p=e.toLowerCase().match(new RegExp(p)),d[c]=p?!0:0):d[c]=0;return d}(),f=function(){var d={},p,c,k,h,m;k=a(b.devicePrefixes);m=k.length;for(h=0;h<m;h++)c=k[h],p=b.devicePrefixes[c],p=e.match(new RegExp(p)),d[c]=p?!0:0;return d}(),l=d.loadPlatformsParam();c(g,h,m,f,l,!0);g.phone=!!(g.iphone||
g.ipod||!g.silk&&g.android&&(3>g.android||p)||g.blackberry&&p||g.windowsphone);g.tablet=!(g.phone||!(g.ipad||g.android||g.silk||g.rimtablet||g.ie10&&/; Touch/.test(e)));g.touch=function(b,e){b="on"+b.toLowerCase();e=b in k;!e&&k.setAttribute&&k.removeAttribute&&(k.setAttribute(b,""),e="function"===typeof k[b],"undefined"!==typeof k[b]&&(k[b]=void 0),k.removeAttribute(b));return e}("touchend")||navigator.maxTouchPoints||navigator.msMaxTouchPoints;g.desktop=!g.phone&&!g.tablet;g.cordova=g.phonegap=
!!(window.PhoneGap||window.Cordova||window.cordova);g.webview=/(iPhone|iPod|iPad).*AppleWebKit(?!.*Safari)(?!.*FBAN)/i.test(e);g.androidstock=4.3>=g.android&&(g.safari||g.silk);c(g,l,!0)},loadPlatformsParam:function(){var b=window.location.search.substr(1).split("\x26"),e={},a,d={},c,m,f;for(a=0;a<b.length;a++)c=b[a].split("\x3d"),e[c[0]]=c[1];if(e.platformTags)for(c=e.platformTags.split(","),b=c.length,a=0;a<b;a++)e=c[a].split(":"),m=e[0],f=!0,1<e.length&&(f=e[1],"false"===f||"0"===f)&&(f=!1),d[m]=
f;return d},filterPlatform:function(b,e){b=q.concat(b||q);e=q.concat(e||q);var a=b.length,d=e.length,c=!a&&d,m;for(m=0;m<a&&!c;m++)c=b[m],c=!!g[c];for(m=0;m<d&&c;m++)c=e[m],c=!g[c];return c},init:function(){var b=n.getElementsByTagName("script"),e=b[0],a=b.length,c=/\/ext(\-[a-z\-]+)?\.js$/,h,m,f,l,g;d.hasReadyState="readyState"in e;d.hasAsync="async"in e;d.hasDefer="defer"in e;d.hasOnLoad="onload"in e;d.isIE8=d.hasReadyState&&!d.hasAsync&&d.hasDefer&&!d.hasOnLoad;d.isIE9=d.hasReadyState&&!d.hasAsync&&
d.hasDefer&&d.hasOnLoad;d.isIE10p=d.hasReadyState&&d.hasAsync&&d.hasDefer&&d.hasOnLoad;d.isIE10=10===(new Function("/*@cc_on return @_jscript_version @*/"))();d.isIE10m=d.isIE10||d.isIE9||d.isIE8;d.isIE11=d.isIE10p&&!d.isIE10;for(g=0;g<a;g++)if(h=(e=b[g]).src)m=e.readyState||null,!f&&c.test(h)&&(f=h),d.scripts[l=d.canonicalUrl(h)]||new r({key:l,url:h,done:null===m||"loaded"===m||"complete"===m,el:e,prop:"src"});f||(e=b[b.length-1],f=e.src);d.baseUrl=f.substring(0,f.lastIndexOf("/")+1);d.origin=window.location.origin||
window.location.protocol+"//"+window.location.hostname+(window.location.port?":"+window.location.port:"");d.detectPlatformTags();Ext.filterPlatform=d.filterPlatform},canonicalUrl:function(b){w.href=b;b=w.href;var e=t.disableCachingParam,e=e?b.indexOf(e+"\x3d"):-1,a,d;0<e&&("?"===(a=b.charAt(e-1))||"\x26"===a)&&(d=b.indexOf("\x26",e),(d=0>d?"":b.substring(d))&&"?"===a&&(++e,d=d.substring(1)),b=b.substring(0,e-1)+d);return b},getConfig:function(b){return b?d.config[b]:d.config},setConfig:function(b,
e){if("string"===typeof b)d.config[b]=e;else for(var a in b)d.setConfig(a,b[a]);return d},getHead:function(){return d.docHead||(d.docHead=n.head||n.getElementsByTagName("head")[0])},create:function(b,e,a){a=a||{};a.url=b;a.key=e;return d.scripts[e]=new r(a)},getEntry:function(b,e,a){var c,h;c=a?b:d.canonicalUrl(b);h=d.scripts[c];h||(h=d.create(b,c,e),a&&(h.canonicalPath=!0));return h},registerContent:function(b,e,a){return d.getEntry(b,{content:a,loaded:!0,css:"css"===e})},processRequest:function(b,
a){b.loadEntries(a)},load:function(b){b=new l(b);if(b.sync||d.syncMode)return d.loadSync(b);d.currentRequest?(b.getEntries(),d.suspendedQueue.push(b)):(d.currentRequest=b,d.processRequest(b,!1));return d},loadSync:function(b){b=new l(b);d.syncMode++;d.processRequest(b,!0);d.syncMode--;return d},loadBasePrefix:function(b){b=new l(b);b.prependBaseUrl=!0;return d.load(b)},loadSyncBasePrefix:function(b){b=new l(b);b.prependBaseUrl=!0;return d.loadSync(b)},requestComplete:function(b){if(d.currentRequest===
b)for(d.currentRequest=null;0<d.suspendedQueue.length;)if(b=d.suspendedQueue.shift(),!b.done){d.load(b);break}d.currentRequest||0!=d.suspendedQueue.length||d.fireListeners()},isLoading:function(){return!d.currentRequest&&0==d.suspendedQueue.length},fireListeners:function(){for(var b;d.isLoading()&&(b=d.listeners.shift());)b()},onBootReady:function(b){d.isLoading()?d.listeners.push(b):b()},getPathsFromIndexes:function(b,a){if(!("length"in b)){var d=[],c;for(c in b)isNaN(+c)||(d[+c]=b[c]);b=d}return l.prototype.getPathsFromIndexes(b,
a)},createLoadOrderMap:function(b){return l.prototype.createLoadOrderMap(b)},fetch:function(b,a,d,c){c=void 0===c?!!a:c;var h=new XMLHttpRequest,m,g,l,n=!1,r=function(){h&&4==h.readyState&&(g=1223===h.status?204:0!==h.status||"file:"!==(self.location||{}).protocol&&"ionp:"!==(self.location||{}).protocol?h.status:200,l=h.responseText,m={content:l,status:g,exception:n},a&&a.call(d,m),h.onreadystatechange=f,h=null)};c&&(h.onreadystatechange=r);try{h.open("GET",b,c),h.send(null)}catch(q){return n=q,r(),
m}c||r();return m},notifyAll:function(b){b.notifyRequests()}};l.prototype={$isRequest:!0,createLoadOrderMap:function(b){var a=b.length,d={},c,h;for(c=0;c<a;c++)h=b[c],d[h.path]=h;return d},getLoadIndexes:function(b,a,c,k,h){var m=[],f=[b];b=b.idx;var g,l,n;if(a[b])return m;for(a[b]=m[b]=!0;b=f.shift();)if(g=b.canonicalPath?d.getEntry(b.path,null,!0):d.getEntry(this.prepareUrl(b.path)),!h||!g.done)for(b=k&&b.uses&&b.uses.length?b.requires.concat(b.uses):b.requires,l=0,n=b.length;l<n;l++)g=b[l],a[g]||
(a[g]=m[g]=!0,f.push(c[g]));return m},getPathsFromIndexes:function(b,a){var d=[],c,h;c=0;for(h=b.length;c<h;c++)b[c]&&d.push(a[c].path);return d},expandUrl:function(b,a,d,c,h,f){return a&&(d=d[b])&&(c=this.getLoadIndexes(d,c,a,h,f),c.length)?this.getPathsFromIndexes(c,a):[b]},expandUrls:function(b,a){var d=this.loadOrder,c=[],h={},f=[],g,l,n,r,q,u,t;"string"===typeof b&&(b=[b]);d&&(g=this.loadOrderMap,g||(g=this.loadOrderMap=this.createLoadOrderMap(d)));n=0;for(r=b.length;n<r;n++)for(l=this.expandUrl(b[n],
d,g,f,a,!1),q=0,u=l.length;q<u;q++)t=l[q],h[t]||(h[t]=!0,c.push(t));0===c.length&&(c=b);return c},expandLoadOrder:function(){var b=this.urls,a;this.expanded?a=b:(a=this.expandUrls(b,!0),this.expanded=!0);this.urls=a;b.length!=a.length&&(this.sequential=!0);return this},getUrls:function(){this.expandLoadOrder();return this.urls},prepareUrl:function(b){return this.prependBaseUrl?d.baseUrl+b:b},getEntries:function(){var b=this.entries,a,c,k,h,f;if(!b){b=[];f=this.getUrls();this.loadOrder&&(a=this.loadOrderMap);
for(k=0;k<f.length;k++)h=this.prepareUrl(f[k]),a&&(c=a[h]),h=d.getEntry(h,{buster:this.buster,charset:this.charset},c&&c.canonicalPath),h.requests.push(this),b.push(h);this.entries=b}return b},loadEntries:function(b){var a=this,d=a.getEntries(),c=d.length,h=a.loadStart||0,f,g;void 0!==b&&(a.sync=b);a.loaded=a.loaded||0;a.loading=a.loading||c;for(g=h;g<c;g++)if(f=d[g],h=f.loaded?!0:d[g].load(a.sync),!h){a.loadStart=g;f.onDone(function(){a.loadEntries(b)});break}a.processLoadedEntries()},processLoadedEntries:function(){var b=
this.getEntries(),a=b.length,d=this.startIndex||0,c;if(!this.done){for(;d<a;d++){c=b[d];if(!c.loaded){this.startIndex=d;return}c.evaluated||c.evaluate();c.error&&(this.error=!0)}this.notify()}},notify:function(){var b=this;if(!b.done){var a=b.error,c=b[a?"failure":"success"],a="delay"in b?b.delay:a?1:d.config.chainDelay,f=b.scope||b;b.done=!0;c&&(0===a||0<a?setTimeout(function(){c.call(f,b)},a):c.call(f,b));b.fireListeners();d.requestComplete(b)}},onDone:function(b){var a=this.listeners||(this.listeners=
[]);this.done?b(this):a.push(b)},fireListeners:function(){var b=this.listeners,a;if(b)for(;a=b.shift();)a(this)}};r.prototype={$isEntry:!0,done:!1,evaluated:!1,loaded:!1,isCrossDomain:function(){void 0===this.crossDomain&&(this.crossDomain=0!==this.getLoadUrl().indexOf(d.origin));return this.crossDomain},isCss:function(){if(void 0===this.css)if(this.url){var b=d.assetConfig[this.url];this.css=b?"css"===b.type:u.test(this.url)}else this.css=!1;return this.css},getElement:function(b){var a=this.el;
a||(this.isCss()?(b=b||"link",a=n.createElement(b),"link"==b?(a.rel="stylesheet",this.prop="href"):this.prop="textContent",a.type="text/css"):(a=n.createElement(b||"script"),a.type="text/javascript",this.prop="src",this.charset&&(a.charset=this.charset),d.hasAsync&&(a.async=!1)),this.el=a);return a},getLoadUrl:function(){var b;b=this.canonicalPath?this.url:d.canonicalUrl(this.url);this.loadUrl||(this.loadUrl=this.buster?b+(-1===b.indexOf("?")?"?":"\x26")+this.buster:b);return this.loadUrl},fetch:function(b){var a=
this.getLoadUrl();d.fetch(a,b.complete,this,!!b.async)},onContentLoaded:function(b){var a=b.status,d=b.content;b=b.exception;this.getLoadUrl();this.loaded=!0;!b&&0!==a||v.phantom?200<=a&&300>a||304===a||v.phantom||0===a&&0<d.length?this.content=d:this.evaluated=this.error=!0:this.evaluated=this.error=!0},createLoadElement:function(b){var a=this,c=a.getElement();a.preserve=!0;c.onerror=function(){a.error=!0;b&&(b(),b=null)};d.isIE10m?c.onreadystatechange=function(){"loaded"!==this.readyState&&"complete"!==
this.readyState||!b||(b(),b=this.onreadystatechange=this.onerror=null)}:c.onload=function(){b();b=this.onload=this.onerror=null};c[a.prop]=a.getLoadUrl()},onLoadElementReady:function(){d.getHead().appendChild(this.getElement());this.evaluated=!0},inject:function(b,a){a=d.getHead();var c=this.url,f=this.key,h,g;this.isCss()?(this.preserve=!0,g=f.substring(0,f.lastIndexOf("/")+1),h=n.createElement("base"),h.href=g,a.firstChild?a.insertBefore(h,a.firstChild):a.appendChild(h),h.href=h.href,c&&(b+="\n/*# sourceURL\x3d"+
f+" */"),c=this.getElement("style"),f="styleSheet"in c,a.appendChild(h),f?(a.appendChild(c),c.styleSheet.cssText=b):(c.textContent=b,a.appendChild(c)),a.removeChild(h)):(c&&(b+="\n//# sourceURL\x3d"+f),Ext.globalEval(b));return this},loadCrossDomain:function(){var b=this;b.createLoadElement(function(){b.el.onerror=b.el.onload=f;b.el=null;b.loaded=b.evaluated=b.done=!0;b.notifyRequests()});b.evaluateLoadElement();return!1},loadElement:function(){var b=this;b.createLoadElement(function(){b.el.onerror=
b.el.onload=f;b.el=null;b.loaded=b.evaluated=b.done=!0;b.notifyRequests()});b.evaluateLoadElement();return!0},loadSync:function(){var b=this;b.fetch({async:!1,complete:function(a){b.onContentLoaded(a)}});b.evaluate();b.notifyRequests()},load:function(b){var a=this;if(!a.loaded){if(a.loading)return!1;a.loading=!0;if(b)a.loadSync();else{if(d.isIE10||a.isCrossDomain())return a.loadCrossDomain();if(!a.isCss()&&d.hasReadyState)a.createLoadElement(function(){a.loaded=!0;a.notifyRequests()});else if(!d.useElements||
a.isCss()&&v.phantom)a.fetch({async:!b,complete:function(b){a.onContentLoaded(b);a.notifyRequests()}});else return a.loadElement()}}return!0},evaluateContent:function(){this.inject(this.content);this.content=null},evaluateLoadElement:function(){d.getHead().appendChild(this.getElement())},evaluate:function(){this.evaluated||this.evaluating||(this.evaluating=!0,void 0!==this.content?this.evaluateContent():this.error||this.evaluateLoadElement(),this.evaluated=this.done=!0,this.cleanup())},cleanup:function(){var a=
this.el,c;if(a){if(!this.preserve)for(c in this.el=null,a.parentNode.removeChild(a),a)try{c!==this.prop&&(a[c]=null),delete a[c]}catch(d){}a.onload=a.onerror=a.onreadystatechange=f}},notifyRequests:function(){var a=this.requests,c=a.length,d,f;for(d=0;d<c;d++)f=a[d],f.processLoadedEntries();this.done&&this.fireListeners()},onDone:function(a){var c=this.listeners||(this.listeners=[]);this.done?a(this):c.push(a)},fireListeners:function(){var a=this.listeners,c;if(a&&0<a.length)for(;c=a.shift();)c(this)}};
Ext.disableCacheBuster=function(a,c){var d=new Date;d.setTime(d.getTime()+864E5*(a?3650:-1));d=d.toGMTString();n.cookie="ext-cache\x3d1; expires\x3d"+d+"; path\x3d"+(c||"/")};d.init();return d}(function(){});Ext.globalEval=Ext.globalEval||(this.execScript?function(f){execScript(f)}:function(f){eval.call(window,f)});
Function.prototype.bind||function(){var f=Array.prototype.slice,l=function(l){var n=f.call(arguments,1),q=this;if(n.length)return function(){var t=arguments;return q.apply(l,t.length?n.concat(f.call(t)):n)};n=null;return function(){return q.apply(l,arguments)}};Function.prototype.bind=l;l.$extjs=!0}();Ext.setResourcePath=function(f,l){var r=Ext.manifest||(Ext.manifest={}),n=r.resources||(r.resources={});r&&("string"!==typeof f?Ext.apply(n,f):n[f]=l,r.resources=n)};
Ext.getResourcePath=function(f,l,r){"string"!==typeof f&&(l=f.pool,r=f.packageName,f=f.path);var n=Ext.manifest,n=n&&n.resources;l=n[l];var q=[];null==l&&(l=n.path,null==l&&(l="resources"));l&&q.push(l);r&&q.push(r);q.push(f);return q.join("/")};Ext=Ext||window.Ext||{};
Ext.Microloader=Ext.Microloader||function(){var f=Ext.Boot,l=function(a){console.log("[WARN] "+a)},r="_ext:"+location.pathname,n=function(a,d){return r+a+"-"+(d?d+"-":"")+c.appId},q,t;try{t=window.localStorage}catch(a){}var u=window.applicationCache,w={clearAllPrivate:function(a){if(t){t.removeItem(a.key);var d,b=[],e=a.profile+"-"+c.appId,f=t.length;for(a=0;a<f;a++)d=t.key(a),0===d.indexOf(r)&&-1!==d.indexOf(e)&&b.push(d);for(a in b)t.removeItem(b[a])}},retrieveAsset:function(a){try{return t.getItem(a)}catch(c){return null}},
setAsset:function(a,c){try{null===c||""==c?t.removeItem(a):t.setItem(a,c)}catch(b){}}},y=function(a){this.assetConfig="string"===typeof a.assetConfig?{path:a.assetConfig}:a.assetConfig;this.type=a.type;this.key=n(this.assetConfig.path,a.manifest.profile);a.loadFromCache&&this.loadFromCache()};y.prototype={shouldCache:function(){return t&&this.assetConfig.update&&this.assetConfig.hash&&!this.assetConfig.remote},is:function(a){return!!a&&this.assetConfig&&a.assetConfig&&this.assetConfig.hash===a.assetConfig.hash},
cache:function(a){this.shouldCache()&&w.setAsset(this.key,a||this.content)},uncache:function(){w.setAsset(this.key,null)},updateContent:function(a){this.content=a},getSize:function(){return this.content?this.content.length:0},loadFromCache:function(){this.shouldCache()&&(this.content=w.retrieveAsset(this.key))}};var v=function(a){this.content="string"===typeof a.content?JSON.parse(a.content):a.content;this.assetMap={};this.url=a.url;this.fromCache=!!a.cached;this.assetCache=!1!==a.assetCache;this.key=
n(this.url);this.profile=this.content.profile;this.hash=this.content.hash;this.loadOrder=this.content.loadOrder;this.deltas=this.content.cache?this.content.cache.deltas:null;this.cacheEnabled=this.content.cache?this.content.cache.enable:!1;this.loadOrderMap=this.loadOrder?f.createLoadOrderMap(this.loadOrder):null;a=this.content.tags;var c=Ext.platformTags;if(a){if(a instanceof Array)for(var b=0;b<a.length;b++)c[a[b]]=!0;else f.apply(c,a);f.apply(c,f.loadPlatformsParam())}this.js=this.processAssets(this.content.js,
"js");this.css=this.processAssets(this.content.css,"css")};v.prototype={processAsset:function(a,c){c=new y({manifest:this,assetConfig:a,type:c,loadFromCache:this.assetCache});return this.assetMap[a.path]=c},processAssets:function(a,c){var b=[],e=a.length,f,g;for(f=0;f<e;f++)g=a[f],b.push(this.processAsset(g,c));return b},useAppCache:function(){return!0},getAssets:function(){return this.css.concat(this.js)},getAsset:function(a){return this.assetMap[a]},shouldCache:function(){return this.hash&&this.cacheEnabled},
cache:function(a){this.shouldCache()&&w.setAsset(this.key,JSON.stringify(a||this.content))},is:function(a){return this.hash===a.hash},uncache:function(){w.setAsset(this.key,null)},exportContent:function(){return f.apply({loadOrderMap:this.loadOrderMap},this.content)}};var g=[],x=!1,c={init:function(){Ext.microloaded=!0;var a=document.getElementById("microloader");c.appId=a?a.getAttribute("data-app"):"";Ext.beforeLoad&&(q=Ext.beforeLoad(Ext.platformTags));var d=Ext._beforereadyhandler;Ext._beforereadyhandler=
function(){Ext.Boot!==f&&(Ext.apply(Ext.Boot,f),Ext.Boot=f);d&&d()}},applyCacheBuster:function(a){var c=(new Date).getTime(),b=-1===a.indexOf("?")?"?":"\x26";return a+b+"_dc\x3d"+c},run:function(){c.init();var a=Ext.manifest;if("string"===typeof a){var a=a.indexOf(".json")===a.length-5?a:a+".json",d=n(a);(d=w.retrieveAsset(d))?(a=new v({url:a,content:d,cached:!0}),q&&q(a),c.load(a)):0===location.href.indexOf("file:/")?(v.url=c.applyCacheBuster(a+"p"),f.load(v.url)):(v.url=a,f.fetch(c.applyCacheBuster(a),
function(a){c.setManifest(a.content)}))}else a=new v({content:a}),c.load(a)},setManifest:function(a){a=new v({url:v.url,content:a});a.cache();q&&q(a);c.load(a)},load:function(a){c.urls=[];c.manifest=a;Ext.manifest=c.manifest.exportContent();var d=a.getAssets(),b=[],e,g,k,h;k=d.length;for(g=0;g<k;g++)if(e=d[g],h=c.filterAsset(e))a.shouldCache()&&e.shouldCache()&&(e.content?(h=f.registerContent(e.assetConfig.path,e.type,e.content),h.evaluated&&l("Asset: "+e.assetConfig.path+" was evaluated prior to local storage being consulted.")):
b.push(e)),c.urls.push(e.assetConfig.path),f.assetConfig[e.assetConfig.path]=f.apply({type:e.type},e.assetConfig);if(0<b.length)for(c.remainingCachedAssets=b.length;0<b.length;)e=b.pop(),f.fetch(e.assetConfig.path,function(a){return function(b){c.onCachedAssetLoaded(a,b)}}(e));else c.onCachedAssetsReady()},onCachedAssetLoaded:function(a,d){var b;d=c.parseResult(d);c.remainingCachedAssets--;d.error?(l("There was an error pre-loading the asset '"+a.assetConfig.path+"'. This asset will be uncached for future loading"),
a.uncache()):(b=c.checksum(d.content,a.assetConfig.hash),b||(l("Cached Asset '"+a.assetConfig.path+"' has failed checksum. This asset will be uncached for future loading"),a.uncache()),f.registerContent(a.assetConfig.path,a.type,d.content),a.updateContent(d.content),a.cache());if(0===c.remainingCachedAssets)c.onCachedAssetsReady()},onCachedAssetsReady:function(){f.load({url:c.urls,loadOrder:c.manifest.loadOrder,loadOrderMap:c.manifest.loadOrderMap,sequential:!0,success:c.onAllAssetsReady,failure:c.onAllAssetsReady})},
onAllAssetsReady:function(){x=!0;c.notify();!1!==navigator.onLine?c.checkAllUpdates():window.addEventListener&&window.addEventListener("online",c.checkAllUpdates,!1)},onMicroloaderReady:function(a){x?a():g.push(a)},notify:function(){for(var a;a=g.shift();)a()},patch:function(a,c){var b=[],e,f,g;if(0===c.length)return a;f=0;for(g=c.length;f<g;f++)e=c[f],"number"===typeof e?b.push(a.substring(e,e+c[++f])):b.push(e);return b.join("")},checkAllUpdates:function(){window.removeEventListener&&window.removeEventListener("online",
c.checkAllUpdates,!1);u&&c.checkForAppCacheUpdate();c.manifest.fromCache&&c.checkForUpdates()},checkForAppCacheUpdate:function(){u.status===u.UPDATEREADY||u.status===u.OBSOLETE?c.appCacheState="updated":u.status!==u.IDLE&&u.status!==u.UNCACHED?(c.appCacheState="checking",u.addEventListener("error",c.onAppCacheError),u.addEventListener("noupdate",c.onAppCacheNotUpdated),u.addEventListener("cached",c.onAppCacheNotUpdated),u.addEventListener("updateready",c.onAppCacheReady),u.addEventListener("obsolete",
c.onAppCacheObsolete)):c.appCacheState="current"},checkForUpdates:function(){f.fetch(c.applyCacheBuster(c.manifest.url),c.onUpdatedManifestLoaded)},onAppCacheError:function(a){l(a.message);c.appCacheState="error";c.notifyUpdateReady()},onAppCacheReady:function(){u.swapCache();c.appCacheUpdated()},onAppCacheObsolete:function(){c.appCacheUpdated()},appCacheUpdated:function(){c.appCacheState="updated";c.notifyUpdateReady()},onAppCacheNotUpdated:function(){c.appCacheState="current";c.notifyUpdateReady()},
filterAsset:function(a){a=a&&a.assetConfig||{};return a.platform||a.exclude?f.filterPlatform(a.platform,a.exclude):!0},onUpdatedManifestLoaded:function(a){a=c.parseResult(a);if(a.error)l("Error loading manifest file to check for updates"),c.onAllUpdatedAssetsReady();else{var d,b,e,g,k,h=[],m=new v({url:c.manifest.url,content:a.content,assetCache:!1});c.remainingUpdatingAssets=0;c.updatedAssets=[];c.removedAssets=[];c.updatedManifest=null;c.updatedAssetsReady=!1;if(m.shouldCache())if(c.manifest.is(m))c.onAllUpdatedAssetsReady();
else{c.updatedManifest=m;d=c.manifest.getAssets();b=m.getAssets();for(g in b)a=b[g],e=c.manifest.getAsset(a.assetConfig.path),(k=c.filterAsset(a))&&(!e||a.shouldCache()&&!e.is(a))&&h.push({_new:a,_current:e});for(g in d)e=d[g],a=m.getAsset(e.assetConfig.path),(k=!c.filterAsset(a))&&a&&(!e.shouldCache()||a.shouldCache())||c.removedAssets.push(e);if(0<h.length)for(c.remainingUpdatingAssets=h.length;0<h.length;)(e=h.pop(),a=e._new,e=e._current,"full"!==a.assetConfig.update&&e)?"delta"===a.assetConfig.update&&
(g=m.deltas,g=g+"/"+a.assetConfig.path+"/"+e.assetConfig.hash+".json",f.fetch(g,function(a,b){return function(d){c.onDeltaAssetUpdateLoaded(a,b,d)}}(a,e))):f.fetch(a.assetConfig.path,function(a){return function(b){c.onFullAssetUpdateLoaded(a,b)}}(a));else c.onAllUpdatedAssetsReady()}else c.updatedManifest=m,w.clearAllPrivate(m),c.onAllUpdatedAssetsReady()}},onFullAssetUpdateLoaded:function(a,d){var b;d=c.parseResult(d);c.remainingUpdatingAssets--;d.error?a.uncache():(b=c.checksum(d.content,a.assetConfig.hash))?
(a.updateContent(d.content),c.updatedAssets.push(a)):a.uncache();if(0===c.remainingUpdatingAssets)c.onAllUpdatedAssetsReady()},onDeltaAssetUpdateLoaded:function(a,d,b){var e,f,g;b=c.parseResult(b);c.remainingUpdatingAssets--;if(b.error)l("Error loading delta patch for "+a.assetConfig.path+" with hash "+d.assetConfig.hash+" . This asset will be uncached for future loading"),a.uncache();else try{e=JSON.parse(b.content),g=c.patch(d.content,e),(f=c.checksum(g,a.assetConfig.hash))?(a.updateContent(g),
c.updatedAssets.push(a)):a.uncache()}catch(h){l("Error parsing delta patch for "+a.assetConfig.path+" with hash "+d.assetConfig.hash+" . This asset will be uncached for future loading"),a.uncache()}if(0===c.remainingUpdatingAssets)c.onAllUpdatedAssetsReady()},onAllUpdatedAssetsReady:function(){var a;c.updatedAssetsReady=!0;if(c.updatedManifest){for(;0<c.removedAssets.length;)a=c.removedAssets.pop(),a.uncache();for(c.updatedManifest&&c.updatedManifest.cache();0<c.updatedAssets.length;)a=c.updatedAssets.pop(),
a.cache()}c.notifyUpdateReady()},notifyUpdateReady:function(){"checking"!==c.appCacheState&&c.updatedAssetsReady&&("updated"===c.appCacheState||c.updatedManifest)&&(c.appUpdate={updated:!0,app:"updated"===c.appCacheState,manifest:c.updatedManifest&&c.updatedManifest.exportContent()},c.fireAppUpdate())},fireAppUpdate:function(){Ext.GlobalEvents&&Ext.defer(function(){Ext.GlobalEvents.fireEvent("appupdate",c.appUpdate)},1E3)},checksum:function(a,c){if(!a||!c)return!1;var b=!0,e=c.length,f=a.substring(0,
1);"/"==f?a.substring(2,e+2)!==c&&(b=!1):"f"==f?a.substring(10,e+10)!==c&&(b=!1):"."==f&&a.substring(1,e+1)!==c&&(b=!1);return b},parseResult:function(a){var c={};!a.exception&&0!==a.status||f.env.phantom?200<=a.status&&300>a.status||304===a.status||f.env.phantom||0===a.status&&0<a.content.length?c.content=a.content:c.error=!0:c.error=!0;return c}};return c}();Ext.manifest=Ext.manifest||"bootstrap";Ext.Microloader.run();</script>

</head>
<body></body>
</html>