この記事は Sencha Advent Calendar 2014 18日目の記事です。
キーボード入力でボタンを押せる
ExtJSのプラグインを作ってみようと思います。
要件
プラグインの要件を以下と定めます。
- ファンクションキーの押下でボタンのクリックがされること
- ボタンとキーの対応は自由に設定できること
- ボタンがクリックできる条件下(表示・活性)でのみ動作すること
実装
簡単に作り方を解説していきます。
1. プラグインクラス定義
いつも通りExt.defineを使ってクラス定義をしていきます。
extendはExt.plugin.Abstractを指定します。
プラグインのベースクラスです。
これから作るプラグイン名は BtnKeyMapperとします(命名センスのなさはお察し)。
Ext.define('Adventer.plugin.BtnKeyMapper', {
// {{{ extend
extend : 'Ext.plugin.Abstract',
// }}}
// {{{ alias
alias: 'plugin.btnkeymapper'
// }}}
});
これで何もしない空のプラグインがとりあえずできました。
前回作ったフォームにボタンを置いて読み込んでみます。
/* Form.js */
// {{{ tbar
tbar: [
{
text: '保存',
plugins: [{
ptype: 'btnkeymapper'
}]
}
],
// }}}
もちろんまだ何も変わりません。
ここからが本番です。
2. initメソッド作成
initメソッドは、プラグインの起点になります。
pluginを設定したコンポーネントが引数(ここでは client)として渡されます。
このclientになんやかんやしていきます。
ちなみに、initが呼ばれるタイミングは コンポーネントの initComponentが呼ばれた後になります。レンダリング前ですね。
/* plugin.BtnKeyMapper.js*/
// {{{ init
init: function (client) {
var me = this;
console.log(clinent);
},
// }}}
3. どのキーを割り当てるか?
この設定はボタン側でやりますね。
とりあえずプラグインにコンフィグを渡す形を想定。
/* Form.js*/
{
text: '保存',
plugins: [{
ptype: 'btnkeymapper',
keyCode: Ext.event.Event.F1
}]
}
プラグイン側でとれるか確認。
アクセサーメソッドが欲しかったので、configに入れてinitConfigをコンストラクタで呼んでおきます。
/* plugin.BtnKeyMapper.js*/
// {{{ config
config: {
keyCode: null
},
// }}}
// {{{ constructor
constructor: function (config) {
this.callParent(config);
this.initConfig(config);
},
// }}}
// {{{ init
init: function (client) {
var me = this;
console.log('KeyCode:' + me.getKeyCode());
// }}}
},
無事とれたので、このキーコードのイベントをまず拾います。
4. キーイベントを拾う
キーイベントを拾うには Ext.util.KeyMapを使うと簡単です。
/* plugin.BtnKeyMapper.js*/
// {{{ init
init: function (client) {
var me = this,
keyCode = me.getKeyCode(),
bind;
bind = {
target: document,
key : keyCode,
fn : me.doEmulate,
defaultEventAction: 'stopEvent',
scope : me
};
me.keyMap = new Ext.util.KeyMap(bind);
},
// }}}
// {{{ doEmulate
// キーイベントハンドラー
doEmulate: function(keyCode, e) {
var me = this;
console.log('type : ' + keyCode);
}
// }}}
Ext.util.KeyMap にコンフィグオブジェクトを渡して、キーイベントと、呼び出す関数を設定します。
ひとまず、document全体のイベントですが拾えました。
この辺はあとで調整したい。
5. ボタンをクリックさせる
実はpluginは、cmp というプロパティに設定されたコンポーネントを持っています。
そいつに対してクリックの動作をしてやればOKです。
まずはクリックイベントのハンドラーを用意しておきます。
本来はViewControllerに書くべきですが、試しなのでViewに直接書いてしまいます。
/* Form.js*/
{
text: '保存',
plugins: [{
ptype: 'btnkeymapper',
keyCode: Ext.event.Event.F1
}],
listeners: {
'click': function(btn) {
Ext.Msg.alert(
'イベント',
'保存ボタンがクリックされました!'
);
}
}
}
そして、プラグイン側でクリックイベントに変換します。
/* plugin.BtnKeyMapper.js*/
// {{{ doEmulate
// キーイベントハンドラー
doEmulate: function(keyCode, e) {
var me = this,
button = me.cmp;// ボタン
button.fireEvent('click', button, e);
}
// }}}
ボタンクリック・F1キー入力でこれが出ます。
問題発生!
/* Form.js*/
{
text: '保存',
plugins: [{
ptype: 'btnkeymapper',
keyCode: Ext.event.Event.F1
}],
handler: function(btn) {
Ext.Msg.alert(
'イベント',
'保存ボタンがクリックされました!'
);
}
}
これが動きませんでした。。
修正!
/* plugin.BtnKeyMapper.js*/
// キーイベントハンドラー
doEmulate: function(keyCode, e) {
var me = this,
button = me.cmp;// ボタン
// TODO: 見た目変える
// event発火/handler実行
button.fireHandler(e);
}
// }}}
fireHandlerでclickイベントの発火とhandlerの呼び出しが行われます。
見た目
見た目上で押されたように見せるのはちょっと無理矢理ですが、
クラスの付け替えをdeferを使って行います。
ここはもう少しなんとかしたい所……
// 見た目変える
button.addCls('x-btn-pressed’);// 押された
Ext.defer(function() {
button.removeCls('x-btn-pressed’);// 離れた
},300);
ここまでで
要件1と要件2はクリアかな。
6. ボタンの状態チェック
ボタンの状態チェックを入れます。
以下2点をチェック。
- ボタンが表示されていること
- ボタンがenableであること
チェックは doEmulate の前でやります。
1の条件はこのボタンをもつコンテナーが表示されているかまで見ないといけません。
どこまで見るかは設定できるようにしましょうか。
parentQuery というコンフィグで設定できるようにします。
/* Form.js*/
{
text: '保存',
plugins: [{
ptype: 'btnkeymapper',
keyCode: Ext.event.Event.F1,
parentQuery: 'form'
}],
handler: function(btn) {
プラグイン側ではそのクエリをもとに親コンテナの状態を確認します。
/* plugin.BtnKeyMapper.js*/
// {{{ config
config: {
keyCode: null,
parentQuery: 'container'
},
// }}}
…
// {{{ doEmulate
// キーイベントハンドラー
doEmulate: function(keyCode, e) {
var me = this,
button = me.cmp;// ボタン
if (!me.checkBtnEnable(button, keyCode, e)) {
return false;
}
// 見た目変える
button.addCls('x-btn-pressed');
Ext.defer(function() {
button.removeCls('x-btn-pressed');
},300);
// event発火/handler実行
button.fireHandler(e);
},
// }}}
// {{{ checkBtnEnable
checkBtnEnable: function(button, keyCode, e) {
var me = this,
parent = button.up(me.getParentQuery()),
parentIsVisible = true,
clickable = true;
// 親コンテナーの状態チェック
parentIsVisible = parent.isVisible();
// 表示状態 かつ 活性状態であること
clickable = parentIsVisible && button.isVisible() && !button.disabled;
return clickable;
}
// }}}
要件3 もクリアです!
7. お掃除
いろいろお掃除します。
ボタンが破棄されたらイベントハンドラも破棄したりとか。
以下、全コードです。
Ext.define('Adventer.plugin.BtnKeyMapper', {
// {{{ extend
extend : 'Ext.plugin.Abstract',
// }}}
// {{{ alias
alias: 'plugin.btnkeymapper',
// }}}
// {{{ mixins
mixins: {
observable: 'Ext.util.Observable'
},
// }}}
// {{{ requires
requires: [
'Ext.util.KeyMap'
],
// }}}
// {{{ config
config: {
/**
* @cfg {Number} keyCode
* 監視するキーイベント
* @accessor
*/
keyCode: null,
/**
* @cfg {String} parentQuery
* ボタンが表示状態かを確認する親コンテナ
* @accessor
*/
parentQuery: 'container'
},
// }}}
// {{{ constructor
constructor: function (config) {
this.callParent(config);
this.mixins.observable.constructor.call(this, config);
this.initConfig(config);
},
// }}}
// {{{ init
init: function (client) {
var me = this,
bind;
// ボタンがレンダリングされている間だけ実行するようにする
me.mon(client, {
destroy : me.stopEmulation,
afterrender: me.startEmulation,
scope: me
});
},
// }}}
// {{{ startEmulation
startEmulation: function(button) {
var me = this,
keyCode = me.getKeyCode(),
bind = {
target: document,
key : keyCode,
fn : me.doEmulate,
defaultEventAction: 'stopEvent',
scope : me
};
me.keyMap = new Ext.util.KeyMap(bind);
},
// }}}
// {{{ stopEmulation
stopEmulation: function(button) {
var me = this;
// keyイベント削除
me.keyMap.destroy();
},
// }}}
// {{{ doEmulate
// キーイベントハンドラー
doEmulate: function(keyCode, e) {
var me = this,
button = me.cmp;// ボタン
if (!me.checkBtnEnable(button, keyCode, e)) {
return false;
}
// 押されて
button.addCls('x-btn-pressed');
// 離す
Ext.defer(function() {
button.removeCls('x-btn-pressed');
},300);
// event発火/handler実行
button.fireHandler(e);
},
// }}}
// {{{ checkBtnEnable
checkBtnEnable: function(button, keyCode, e) {
var me = this,
parent = button.up(me.getParentQuery()),
parentIsVisible = true,
clickable = true;
// 親コンテナーの状態チェック
parentIsVisible = parent.isVisible();
// 表示状態 かつ 活性状態であること
clickable = parentIsVisible && button.isVisible() && !button.disabled;
return clickable;
}
// }}}
});
まとめ
- pluginは Ext.plugin.Abstract を継承して作ります。
- plugin内で this.cmp に対してなんやかんやして作っていきます。
- keyイベントを拾うのは Ext.util.KeyMap を使うと楽。
作る過程を書いていった結果、無駄に長くなってしまいました。反省。
コードの無駄も多いのでまだまだ調整の必要ありですがご勘弁を。
明日は toshimitsusato さんです。