
電脳麻将 は ver.2.0.0 からキーボードで打牌選択できるようになりました。これを実現しているのが selector です。リストを表すDOMノードに selector を適用すると、
- 矢印キーなどで1つ1つ選択し、Enterキーで確定
- マウスを合わせることで選択し、クリックで確定
- タッチデバイスではタップで選択し、再度タップで確定
という選択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
- 「確定」の動作に使用するキーの キー名。デフォルト値は Enter。
- prev
- 「前を選択」の動作に使用するキーのキー名。デフォルト値は ArrowLeft。
- next
- 「次を選択」の動作に使用するキーのキー名。デフォルト値は ArrowRight。
- tabindex
- 要素の tabindex 属性に設定する値。タブ移動の対象外としたい場合は -1 を指定する。デフォルト値は 0。
- focus
- 最初に選択状態にする要素のインデックス。負の値を指定すると末尾からのインデックスとして解釈する。初期状態で選択なしとする場合は null を指定する。デフォルト値は 0。
- touch
- 真の場合、タッチを選択と解釈する。偽の場合はタッチで即確定する。デフォルト値は true。
confirm、prev / next に null を指定すると、それぞれのキー操作が無効になります。
以下で実際の動作を確認できます。