Vueディレクティブでクリックで数字入力できるパネルをつくる方法

さてさて、ここのところPythonやElectronの記事を重点的にお届けしてきましたが、ある開発で必要となる機能があったので今回はVueのお話をすることにします。

その機能とは、ずばり「クリックで数字入力ができる<input>タグ」です。

なぜかというと、例えば金額を入力する場合にはいちいちマウスからキーボードに手を移動させないといけない上に、テンキー入力や数字キーは慣れていないため、作業のスピード感が失われてしまうからです。(たまにNumLockのせいで入力がおかしくなったりもします ^^;)

もちろん文字を入力する場合はキーボードを使わざるを得ませんが、数字って0〜9の10個だけなので、なんとか画面上のバーチャルキーボードで入力したかったりするわけですね。

そこで、白羽の矢が立ったのがVueの独自ディレクティブです。

以前、Vue Component で和暦から西暦に変換するセレクトボックスをつくるのように今回もVueコンポーネントでも良かったのですが、コンポーネントだとクラスやスタイルシートの適用が比較的難しくなるため、ディレクティブを採用しました。

ということで、はじめにVueディレクティブの基本的な説明も載せていますので、ぜひ学習者さんは参考にしてみてくださいね。(最後に今回の教材ソースコードをダウンロードできます)

Vueディレクティブとは?

まずは基本的な内容から。(すでに知っている方は次の項目まで読み飛ばしてください!)

例えば、テキストの文字を赤い色にしたい場合を見てみましょう。

<div v-red>赤い文字になります。</div>

この場合はv-redというディレクティブを作成することになります。
実際に見てみましょう。

Vue.directive('red', {
    bind: function(el) {

        el.style.color = '#ff0000';

    }
});

たったこれだけで、使い回しができるディレクティブが完成します。

もちろん使えるのは、ひとつだけじゃなく、次のように使いたい場所でどこでも使うことができます。

<div v-red>赤い文字になります。</div>
<div v-red>ここも赤い文字になります。</div>

そして、CSSだけでなく、JavaScriptでできることならなんでもできます。

例えば、クリックしたら「赤い文字がクリックされました!」とアラート表示してみましょう。
新しくv-red-clickが使えるように次のディレクティブを作ります。

Vue.directive('red-click', {
    bind: function(el) {

        el.addEventListener('click', function() {

            alert('赤い文字がクリックされました!');

        });

    }
});

そして、先ほどのv-redがついているdivタグに追加してみましょう。

<div v-red v-red-click>赤い文字になります。</div>

表示はさっきと変わりませんが、これをクリックするとどうなるでしょうか。

はい、ご想像のとおりアラートが表示されました。

なお、Vueディレクティブにはデータを指定することもできます。
例えば、次のようにアラートのテキストを指定することができます。

<div v-red v-red-click="{text: '青い文字もいいよね!'}">赤い文字になります。</div>

この場合、独自ディレクティブはこうなります。

Vue.directive('red-click', {
    bind: function(el, binding) {

        el.addEventListener('click', function() {

            alert(binding.value.text);

        });

    }
});

つまり、bindに先ほどのデータが入っているので、そこから呼び出すだけでOKです。

もちろんstyleclassとの共存もできるので、Vueコンポーネントよりデザイン的にはよりフレキシブルといっていいでしょう。

<div style="background:#ccc;" v-red v-red-click="{text: '青い文字もいいよね!'}">赤い文字になります。</div>

と、少し前置きが長くなりましたがこれがVueディレクティブです。
では、次からは本題の数字入力できる独自ディレクティブを作っていきましょう!

クリックで数字入力できるVueディレクティブをつくる

やりたいこと

今回つくるディレクティブで実現したい内容は次のようになります。

  • <input>タグが選択されたらバーチャルキーボードを表示
  • クリックで数字が入力できる
  • クリアボタンもつける
  • 閉じるボタンもつける
  • フォーカスが外れたら自動的にバーチャルキーボードは消える
  • 入力された位置に数字を入力(選択されていたら、削除してそこに入力)
  • ハイフンやアスタリスクなど、その他の文字も追加できるようにする

見た目でいうとこんな感じです。

ディレクティブの基本をつくる

まずはディレクティブの基本形を作ります。
ディレクティブの名前はnumber-panelです。

Vue.directive('number-panel', {
    bind: function(el, binding) {

        // ここにコード

    }
});

つまり、このような形で呼び出します。

<input type="text" v-number-panel>

バーチャルキーボード部分をつくる

次にバーチャルキーボードを作る部分ですが、この部分はディレクティブが複数呼び出されても、結局はひとつだけあれば十分なので、既に作成済みの場合は次のように処理をスキップします。

Vue.directive('number-panel', {
    bind: function(el, binding) {

        var panelId = 'vue-number-panel';

        if(document.getElementById(panelId) == null) {

            // 「ひとつだけつくる部分」

        }

    }
});

なお、ここからの各ブロックで前後してしまっていますが、この部分を「ひとつだけつくる部分」とします。

ボタンをつくる(ひとつだけつくる部分)

まずは数字とクリア、そして閉じるボタンを作るコードです。

var panelButtons = [];
var numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 0];

for(var i in numbers) {

    var number = numbers[i];
    panelButtons.push('<button class="vue-number-panel-button" input="'+ number +'">'+ number +'</button>');

}

if(binding.value && typeof binding.value.extraInputs == 'object') {

    for(var key in binding.value.extraInputs) {

        var text = binding.value.extraInputs[key];
        panelButtons.push('<button class="vue-number-panel-button" input="'+ key +'">'+ text +'</button>');

    }

}

panelButtons.push('<button class="vue-number-panel-button" input="c">c</button>');
panelButtons.push('<button class="vue-number-panel-button" input="x">&times;</button>');

panelButtonsという配列にひとつずつ必要となるボタンを追加しています。

<button>タグの中にあるinputプロパティの中身は次のとおりです。

  • 0〜9 ・・・ 入力する数字
  • c ・・・ クリア
  • x ・・・ 閉じる

また、binding.value.extraInputsの部分では、独自の入力ができるボタン(例えば電話番号入力の場合に「-」など)を追加しています。そのため、次のようにするとボタンが増えることになります。

<input type="text" v-number-panel="numberPanelOptions">
new Vue({
    el: '#app',
    data: {
        numberPanelOptions: {
            extraInputs: {
                '-': 'ハイフン',
                '*': 'アスタリスク'
            }
        }
    }
});

本体をつくる(ひとつだけつくる部分)

では次にバーチャルキーボード本体のHTMLを作っていきます。

var panel = document.createElement('div');
panel.id = panelId;
panel.innerHTML = '<div>'+ panelButtons.join('') +'</div>';
panel.style.display = 'none';
panel.style.position = 'absolute';

document.body.appendChild(panel);

createElementdivタグを作り、さっきのボタンを結合してHTML化。そして、CSSを設定してbodyタグに追加します。

入力ボックスにフォーカスされた時のコードをつくる

では、続いてv-number-panelが付けられた入力ボックスが選択された(フォーカスされた)時のコードを作っていきましょう。

var randomKey = Date.now() +'-'+ Math.random().toString(36).substring(7);

el.setAttribute('vue-number-panel-input', randomKey);
el.addEventListener('focus', function(e) {

    var rect = e.target.getBoundingClientRect();
    var top = rect.top;
    var left = rect.left;
    var width = rect.width;
    var height = rect.height;
    var scrollTop  = window.pageYOffset || document.documentElement.scrollTop;
    var scrollLeft = window.pageXOffset || document.documentElement.scrollLeft;
    var panel = document.getElementById(panelId);
    panel.style.top = (top+height+scrollTop) +'px';
    panel.style.left = (left+scrollLeft) +'px';
    panel.style.width = width +'px';
    panel.style.minWidth = '150px';
    panel.style.display = 'block';
    panel.style.zIndex = '10000';
    panel.setAttribute('input-id', e.target.getAttribute('vue-number-panel-input'))

});

まず1行目でランダムな文字列をつくり、入力ボックスのvue-number-panel-inputに格納します。これは入力ボックスを識別するIDです。

そして、focusイベントの中でフォーカスされたときに、その入力ボックスの位置を取得して、先ほどのバーチャルキーボードがすぐ下にくるようにCSSを操作します。そして、先ほどのIDをバーチャルキーボードのinput-idに格納します。

選択中の入力ボックスを取得する関数をつくる

コードをより少なくするために、共通部分を関数にしておきましょう。
次のコードではpanel、つまりバーチャルキーボードに保持されている入力ボックスIDを取得し、そのIDをを使って選択中の入力ボックスを返しています。

var getTargetInput = function() {

    var panel = document.getElementById(panelId);
    var targetInputId = panel.getAttribute('input-id');
    return document.querySelector('[vue-number-panel-input="'+ targetInputId +'"]');

};

※ Vueディレクティブ自体に力技でメソッドを登録する方法もありますが、あまりスマートではないのでこの形を採用しました。

ボタンをクリックしたときのコードを作る(ひとつだけつくる部分)

では、getTargetInput()を使って、ボタンのクリックイベントを作っていきます。

var buttons = document.getElementsByClassName('vue-number-panel-button');

for(var i = 0; i < buttons.length; i++) {

    var button = buttons[i];
    var input = button.getAttribute('input');

    if(input == 'c') {

        // inputが「c」の場合

    } else if(input == 'x') {

        // inputが「x」の場合

    } else {

        // 数字(とその他の文字)がクリックされた時

    }

}

まず1行目で全ボタンを取得して、ループさせています。

そして、inputプロパティの内容によって分岐しますので、それぞれを見ていきましょう。

inputが「c」の場合

クリアボタンがクリックされた時のイベントですので、次のように選択中の入力ボックスを取得、中身を空白にします。そして、入力ボックスへフォーカスを戻し、最後に変更されたことを知らせるinputイベントを送出します。

button.addEventListener('click', function() {

    var targetInput = getTargetInput(panelId);
    targetInput.value = '';
    targetInput.focus();
    targetInput.dispatchEvent(new Event('input'));

});

inputが「x」の場合

閉じるボタンがクリックされた時のコードです。
CSSを操作してバーチャルキーボードを消すだけです。

button.addEventListener('click', function() {

    var panel = document.getElementById(panelId);
    panel.style.display = 'none';

});

数字(とその他の文字)がクリックされた時

最後に数字とユーザーによって追加された文字を入力ボックスへ代入するコードです。

button.addEventListener('click', function(e) {

    var input = e.target.getAttribute('input');
    var targetInput = getTargetInput(panelId);
    var currentValue = targetInput.value;
    var selectionStart = targetInput.selectionStart;
    var selectionEnd = targetInput.selectionEnd;

    if(selectionStart != selectionEnd) {

        currentValue = [
            currentValue.substring(0, selectionStart),
            currentValue.substring(selectionEnd)
        ].join('');

    }

    var newValues = [
        currentValue.substring(0, selectionStart),
        input,
        currentValue.substring(selectionStart)
    ];
    targetInput.value = newValues.join('');
    targetInput.focus();
    targetInput.selectionStart = selectionStart + 1;
    targetInput.selectionEnd = selectionStart + 1;
    targetInput.dispatchEvent(new Event('input'));

});

まずクリックされたボタンに登録されている入力すべき数字(もしくは文字)を取得し、選択中の入力ボックスから現在の入力位置(つまりキャレット)を取得します。

このとき、もしキャレットの開始と終了が違っている場合は一部が選択されている状態になりますので、この部分を削除。そして、開始位置に入力すべき数字を挿入することになります。

つまり、次のように「234」が選択中で「9」をクリックした場合の挙動は、

12345

まず「234」を削除して

15

にし、さらに「1」と「5」の間に「9」を挿入して最終的に

195

となります。

これが終われば、新しい値を入力ボックスに代入し、文字数が変更になったのでキャレット位置も変更、最後にinputイベントを送出して完了です。

フォーカスがはずれたらバーチャルキーボードを消すコード(ひとつだけつくる部分)

バーチャルキーボードを消すには、念の為に閉じるボタンをつくってありますが、いちいち狭い部分をクリックしないと消えないようではユーザービリティがいいとはいえません。

そのため、フォーカスが外れたらバーチャルキーボードを自動的に消すようにしたいところですが、ここでひとつ工夫をしておく必要があります。

なぜなら、入力ボックスにblurイベントをつけてこの中でバーチャルキーボードを消すようにしただけでは、数字ボタンをクリックした時点でblurイベントが実行され、「まだ入力の途中なのにキーボードが消えてしまった!」となってしまいます。

これを解決するためにバーチャルキーボードにmouseentermouseleaveイベントをつけ、入力中かどうかを判断できるパラメータを保持しておく必要があります。

panel.addEventListener('mouseenter', function(e){

    e.target.setAttribute('mouse-hover', 'yes');

});
panel.addEventListener('mouseleave', function(e){

    e.target.setAttribute('mouse-hover', '');

});

これは、マウスがバーチャルキーボードの中に入ってきたら、(つまり入力中)mouse-hoeveryesにし、マウスが出ていったら、空白にするというコードです。

このパラメータを使えば、blurイベントでバーチャルキーボードの誤消去を防ぐことができます。

el.addEventListener('blur', function() {

    var panel = document.getElementById(panelId);
    var mouseHover = panel.getAttribute('mouse-hover');

    if(mouseHover != 'yes') {

        panel.style.display = 'none';

    }

});

panelから先ほどのmouse-hoverを取得してチェックをしています。

GitHubにアップしました

今後の開発でも使えそうだったので、GitHubにアップしておきました。

https://github.com/SUKOHI/v-number-panel

教材ソースコードをダウンロードできます

今回実際に開発したソースコード一式を以下からダウンロードすることができます。展開して、index.htmlをブラウザで開くだけでOKです。

Vueディレクティブ: クリックで数字入力できるパネルをつくるソースコード