
電脳麻将 は ver.2.0.0 からキーボードで打牌選択できるようになりました。 これを実現しているのが selector です。 リストを表すDOMノードに selector を適用すると、
という選択UIが実現できます。
マウスを想定したUIでは、ボタンにマウスを合わせたときに「選択」状態*1にし、クリックで「確定」するというUIが使われることがありますが、これをタッチデバイスで使うと「選択」と「確定」が同時に発生してしまいます。 タッチデバイスではタップすると、touchstart → mouseover → click と連続してイベントが発生するためです。 タップの際にclickイベントの発生を抑止するには、それ以前のイベントハンドラで preventDefault() を呼び出す必要があります。
つまり、touchstart のイベントハンドラで、
を行います。 これで選択後のタップで click イベントが発生するようになります。
次に「選択」の動作を focus に集約します。 キーボードでは keydown、マウスでは mouseover、タッチデバイスでは touchstart とバラバラなので、それぞれのイベントハンドラから focus イベントを発生させれば「フォーカスされたら選択」と統一できます。
最後に「選択解除」の動作を blur に集約します。 他の要素にフォーカスが移れば自然と blur イベントが発生しますが、mouseout の際にも blur イベントを発生させるのがよいでしょう。 blur イベントのハンドラでは touchstart のいったんキャンセルしたイベントハンドラを再登録する必要があります。
これらを考慮した @kobalab/majiang-ui のクラス Majiang.UI.Util の selector の実装は以下の通りです。
/*
* selector.js
*/
"use strict";
const selectors = {};
function setSelector(list, namespace, param = {}) {
clearSelector(namespace);
let opt = {
confirm: 'Enter', prev: 'ArrowLeft', next: 'ArrowRight',
tabindex: 0, focus: 0, touch: true
};
Object.assign(opt, param);
if (namespace[0] != '.') namespace = '.' + namespace;
selectors[namespace] = list;
let i = null;
let len = list.length
function touchstart(ev) {
if (opt.touch) {
$(ev.target).off('touchstart' + namespace).trigger('focus');
return false;
}
else {
$(ev.target).trigger('focus');
}
}
list.attr('tabindex', opt.tabindex).attr('role','button')
.on('touchstart' + namespace, touchstart)
.on('focus' + namespace, (ev)=>{ i = list.index($(ev.target)) })
.on('blur' + namespace, (ev)=>{ i = null;
$(ev.target).on('touchstart' + namespace, touchstart)})
.on('mouseover' + namespace, (ev)=>$(ev.target).trigger('focus'))
.on('mouseout' + namespace, (ev)=>$(ev.target).trigger('blur'));
if (opt.confirm) {
$(window).on('keyup' + namespace, (ev)=>{
if (ev.key == opt.confirm && i != null) {
list.eq(i).trigger('click');
return false;
}
});
}
if (opt.prev && opt.next) {
$(window).on('keydown' + namespace, (ev)=>{
if (ev.key == opt.prev) {
i = (i == null) ? len - 1 :
(i <= 0) ? 0 : i - 1;
list.eq(i).trigger('touchstart');
return false;
}
else if (ev.key == opt.next) {
i = (i == null) ? 0 :
(i >= len - 1) ? len - 1 : i + 1;
list.eq(i).trigger('touchstart');
return false;
}
});
}
if (opt.focus != null) {
list.eq(opt.focus).trigger('touchstart');
}
return list;
}
function clearSelector(namespace) {
if (namespace[0] != '.') namespace = '.' + namespace;
if (! selectors[namespace]) return;
selectors[namespace].removeAttr('tabindex role').off(namespace);
$(window).off(namespace);
delete selectors[namespace];
}
module.exports = {
setSelector: setSelector,
clearSelector: clearSelector
}
list で操作対象となるDOMノードのリストをjQueryオブジェクトとして指定します。 namespace にはこの操作のネームスペースを指定します。この値は selector を解除するときに使用します。 param には以下のオプションを指定できます。
confirm、prev / next に null を指定すると、それぞれのキー操作が無効になります。
以下で実際の動作を確認できます。