search
LoginSignup
0

More than 1 year has passed since last update.

posted at

updated at

inputやtextarea要素に対して数値入力を行うUI

inputtextarea要素に対して数値入力を行うためのテンキー型のUIです。

動作デモ

適用方法

対応させたいinputtextarea要素のHTMLタグにdata-numKeyPadを追記することで適用できます。

<input type='text' data-numKeyPad>

該当要素をクリックすることでUIが表示されます。
01.jpg

data-numKeyPadnumericを指定、または日時入力系のtypeに対して指定した場合は、数字とバックスペースキーのみのレイアウトになります。

<input type='text' data-numKeyPad='numeric'>
<input type='time' data-numKeyPad>

02.jpg

data-numKeyPadnumericを指定した場合は/*-+.キーが省略されたレイアウトになりますが、forcedで追加表示指定ができます。
numericを指定しつつ '.' と '-' を追加する例

<input type='text' data-numKeyPad='numeric; forced:".-"'>

03.jpg
スマートフォンではUI上のキータップ、PCではそれに加えてキーボードで入力可能です。
UIの表示位置はドラッグで変更可能、UI以外の部分をタップorクリック、またはEnterキーでクローズします。

スクリプト

numkeypad.js
'use strict';
{
    const
        obj = {},
        opt = {},
        handler = (function() {
            const events = {};
            let key = 1;
            return {
                add: function(target, type, listener, capture) {
                    target.addEventListener(type, listener, capture);
                    events[key] = {
                        target: target,
                        type: type,
                        listener: listener,
                        capture: capture
                    };
                    return key++;
                },
                remove: function(key) {
                    if(key in events) {
                        const e = events[key];
                        e.target.removeEventListener(e.type, e.listener, e.capture);
                        delete(events[key]);
                    }
                }
            };
        }()),
        touchFlg = window.ontouchstart !== undefined ? true : false;

    window.addEventListener('DOMContentLoaded', function() {
        document.querySelector('body').insertAdjacentHTML('beforeend',
            "<div id='numKeyPad' hidden>" +
            "    <div id='nkpBackground'></div>" +
            "    <div id='nkpPad'>" +
            "        <input type='text' id='nkpText'>" +
            "        <input type='checkbox' id='nkpCb'>" +
            "        <div class='nkpL nkpNan'>" +
            "            <button id='nkpKeySlash'>/</button>" +
            "            <button id='nkpKeyAsterisk'>*</button>" +
            "            <button id='nkpKeyBackspace' class='nkpWl'>&lt;</button>" +
            "        </div>" +
            "        <div class='nkpL'>" +
            "            <button id='nkpKey7'>7</button>" +
            "            <button id='nkpKey8'>8</button>" +
            "            <button id='nkpKey9'>9</button>" +
            "            <button id='nkpKeyHyphen' class='nkpVl nkpNan'>-</button>" +
            "            <div class='nkpForce'>" +
            "                <button id='nkpKeySlash'>/</button>" +
            "                <button id='nkpKeyAsterisk'>*</button>" +
            "                <button id='nkpKeyHyphen'>-</button>" +
            "                <button id='nkpKeyPlus'>+</button>" +
            "            </div>" +
            "        </div>" +
            "        <div class='nkpL'>" +
            "            <button id='nkpKey4'>4</button>" +
            "            <button id='nkpKey5'>5</button>" +
            "            <button id='nkpKey6'>6</button>" +
            "        </div>" +
            "        <div class='nkpL'>" +
            "            <button id='nkpKey1'>1</button>" +
            "            <button id='nkpKey2'>2</button>" +
            "            <button id='nkpKey3'>3</button>" +
            "            <button id='nkpKeyPlus' class='nkpVl nkpNan'>+</button>" +
            "        </div>" +
            "        <div class='nkpL'>" +
            "            <button id='nkpKey0' class='nkpWl'>0</button>" +
            "            <button id='nkpKeyPeriod'>.</button>" +
            "            <button id='nkpKeyBackspace' class='nkpNum'>&lt;</button>" +
            "        </div>" +
            "    </div>" +
            "</div>");

        const e = document.querySelectorAll('#numKeyPad [id^="nkpKey"]');
        for(let i in e) {
            if(typeof e[i] !== 'object') continue;
            e[i].addEventListener(touchFlg ? 'touchstart' : 'mousedown', function() {
                obj.inputId = this.id;
            }, false);
            e[i].addEventListener(touchFlg ? 'touchend' : 'mouseup', function() {
                setTimeout(function(){
                    document.querySelector('#numKeyPad #nkpCb').focus();
                    if(obj.releaseTime !== undefined && Date.now() - obj.releaseTime < 100) return;
                    padKeyInput(obj.inputId);
                });
            }, false);
        }
        const bg = document.querySelector('#numKeyPad #nkpBackground');
        bg.style.position = 'fixed';
        bg.style.top = '0px';
        bg.style.left = '0px';
        bg.style.width = '100%';
        bg.style.height = '100%';
        bg.addEventListener('click', function(){
            closeNumKeyPad();
        }, false);

        const t =  document.querySelectorAll('[data-numKeyPad]');
        for(let i in t) {
            if(t[i].nodeName === undefined) continue;
            if(['input', 'textarea'].indexOf(t[i].nodeName.toLowerCase()) < 0) continue;
            t[i].addEventListener(touchFlg ? 'touchstart' : 'mousedown', function(){
                if(/(time|date)/.test(this.type)) {
                    if(touchFlg) this.disabled = true;
                }
            }, false);
            t[i].addEventListener(touchFlg ? 'touchend' : 'mouseup', function(){
                if(/(time|date)/.test(this.type)) {
                    if(!touchFlg) setTimeout(function(){this.blur()},500);
                }
                viewNumKeyPad(this);
                this.disabled = false;
            }, false);
            if(touchFlg) {
                t[i].addEventListener('click', function(){
                    this.disabled = true;
                    viewNumKeyPad(this);
                    this.disabled = false;
                }, false);
            }
        }

        const np = document.querySelector('#numKeyPad #nkpPad');
        np.addEventListener(touchFlg ? 'touchstart' : 'mousedown', function(e) {
            obj.moveFlg = true;
            obj.x = (touchFlg ? e.touches[0].clientX : e.clientX);
            obj.y = (touchFlg ? e.touches[0].clientY : e.clientY);
            obj.sx = obj.x - this.offsetLeft;
            obj.sy = obj.y - this.offsetTop;
        }, false);

        window.addEventListener(touchFlg ? 'touchmove' : 'mousemove', function(e) {
            if(obj.moveFlg) {
                obj.ex = touchFlg ? e.touches[0].clientX : e.clientX;
                obj.ey = touchFlg ? e.touches[0].clientY : e.clientY;
                if(Math.abs(obj.ex - obj.x) > 24 || Math.abs(obj.ey - obj.y) > 24 || obj.moved) {
                    const
                        rx = obj.ex - obj.sx,
                        ry = obj.ey - obj.sy;
                    np.style.left = rx + 'px';
                    np.style.top = ry + 'px';
                    obj.moved = true;
                }
            }
        }, false);

        const cb = document.querySelector('#numKeyPad #nkpCb');
        cb.style.position = 'fixed';
        cb.style.top = '-200px';

        window.addEventListener(touchFlg ? 'touchend' : 'mouseup', function(e) {
            obj.moveFlg = false;
            if(obj.moved) {
                obj.moved = false;
                obj.releaseTime = Date.now();
            }
        }, false);
    }, false);

    function padKeyInput(id) {
        if(obj.view === undefined || obj.view === false) return;
        const text = document.querySelector('#numKeyPad #nkpText');
        let tmp = text.value;
        let m;
        if((m = id.match(/^nkpKey(\d)/)) && m[1] !== undefined) tmp += m[1];
        else if(id === 'nkpKeyPeriod')    tmp += '.';
        else if(id === 'nkpKeyHyphen')    tmp += '-';
        else if(id === 'nkpKeySlash')     tmp += '/';
        else if(id === 'nkpKeyAsterisk')  tmp += '*';
        else if(id === 'nkpKeyPlus')      tmp += '+';
        else if(id === 'nkpKeyBackspace') tmp = tmp.slice(0, -1);

        if(obj.target.type === 'time') {
            tmp = tmp.replace(/\D/g, '');
            tmp = tmp.padStart(4, '0').slice(-4);
            const t = tmp.slice(0, 2) + ':' + tmp.slice(2);
            if(!isNaN(new Date('2000-01-01T' + t + ':00Z').getTime())) {
                obj.target.value = text.value = t;
                text.style.color = '';
            }
            else {
                text.value = t;
                text.style.color = '#f00';
            }
        }
        else if(obj.target.type === 'date') {
            tmp = tmp.replace(/\D/g, '');
            tmp = tmp.padStart(8, '0').slice(-8);
            const t = tmp.slice(0, 4) + '-' + tmp.slice(4, 6) + '-' + tmp.slice(6);
            if(!isNaN(new Date(t + 'T00:00:00Z').getTime())) {
                obj.target.value = text.value = t;
                text.style.color = '';
            }
            else {
                text.value = t;
                text.style.color = '#f00';
            }
        }
        else if(obj.target.type === 'month') {
            tmp = tmp.replace(/\D/g, '');
            tmp = tmp.padStart(6, '0').slice(-6);
            const t = tmp.slice(0, 4) + '-' + tmp.slice(4);
            if(!isNaN(new Date(t + '-01T00:00:00Z').getTime())) {
                obj.target.value = text.value = t;
                text.style.color = '';
            }
            else {
                text.value = t;
                text.style.color = '#f00';
            }
        }
        else if(obj.target.type === 'datetime-local') {
            tmp = tmp.replace(/\D/g, '');
            tmp = tmp.padStart(12, '0').slice(-12);
            const d = tmp.slice(0, 4) + '-' + tmp.slice(4, 6) + '-' + tmp.slice(6, 8);
            const t = tmp.slice(8, 10) + ':' + tmp.slice(10);
            if(!isNaN(new Date(d + 'T' + t + 'Z').getTime())) {
                obj.target.value = d + 'T' + t;
                text.value = d + ' ' + t;
                text.style.color = '';
            }
            else {
                text.value = d + ' ' + t;
                text.style.color = '#f00';
            }
        }
        else if(obj.target.type === 'number') {
            text.style.color = '';
            if(tmp === '') {
                text.value = tmp;
                obj.target.value = '0';
            }
            else if(tmp.length > 1 && tmp[0] === '0' && tmp[1] !== '.') {
                obj.target.value = text.value = tmp.slice(1);
            }
            else if(String(parseFloat(tmp)) === String(tmp)) {
                obj.target.value = text.value = tmp;
            }
            else if(!isNaN(tmp) || tmp === '-') {
                text.value = tmp;
                text.style.color = '#f00';
            }
            else if(tmp.length > 1 && tmp[0] !== '-' && tmp.slice(-1) === '-') {
                obj.target.value = text.value = tmp.slice(-1) + tmp.slice(0, -1);
            }
            else if(tmp.length > 1 && tmp[0] === '-' && tmp.slice(-1) === '-') {
                obj.target.value = text.value = tmp.slice(1, -1);
            }
        }
        else {
            obj.target.value = text.value = tmp;
        }
    }

    window.addEventListener('keydown', function(e) {
        if(obj.view === undefined || obj.view === false) return;
        const key = e.key.toLowerCase();
        function isAvailable(s) {
            return !opt.numeric || opt.forcedKey[s];
        }
        if(key === 'enter') {
            closeNumKeyPad();
            return;
        }
        else if(key >= '0'  && key <= '9')              padKeyInput('nkpKey' + key);
        else if(key === '.' && isAvailable('Period'))   padKeyInput('nkpKeyPeriod');
        else if(key === '-' && isAvailable('Hyphen'))   padKeyInput('nkpKeyHyphen');
        else if(key === '+' && isAvailable('Plus'))     padKeyInput('nkpKeyPlus');
        else if(key === '/' && isAvailable('Slash'))    padKeyInput('nkpKeySlash');
        else if(key === '*' && isAvailable('Asterisk')) padKeyInput('nkpKeyAsterisk');
        else if(key === 'backspace')                    padKeyInput('nkpKeyBackspace');
    }, false);

    function viewNumKeyPad(e) {
        const pad   = document.querySelector('#numKeyPad');
        const np    = document.querySelector('#numKeyPad #nkpPad');
        const text  = document.querySelector('#numKeyPad #nkpText');
        const force = document.querySelector('#numKeyPad .nkpForce');
        const eValue = e.value;
        text.value = e.value;

        const numKeySetParams = e.dataset['numkeypad'].split(';');
        opt.numeric = /(time|date|month)/.test(e.type) ? 1 : 0;
        opt.forced = 0;
        opt.forcedKey = {};
        const forcedKeys = {'/': 'Slash', '*': 'Asterisk', '-': 'Hyphen', '+': 'Plus', '.': 'Period'};
        for(let i in forcedKeys) {
            opt.forcedKey[forcedKeys[i]] = false;
        }

        for(let i = 0; i < numKeySetParams.length; ++i) {
            const param = numKeySetParams[i].trim();
            const name = param.split(':')[0].toLowerCase();
            const value = param.split(':')[1];
            if(name === 'numeric') opt.numeric = 1;
            if(name === 'forced') {
                for(let i in forcedKeys) {
                    if(value.indexOf(i) !== -1) {
                        opt.forcedKey[forcedKeys[i]] = true;
                        opt.forced++;
                    }
                }
            }
        }
        force.style.display = opt.numeric && opt.forced ? 'block' : 'none';
        pad.querySelector('#nkpKeyPeriod').style.display =
            !opt.numeric || opt.forcedKey['Period'] ? 'block' : 'none';
        pad.querySelector('#nkpKey0').style.width =
            opt.numeric && opt.forcedKey['Period'] ? '60px' : '124px';
        for(let i in opt.forcedKey) {
            if(i !== 'Period') {
                pad.querySelector('.nkpForce #nkpKey' + i).style.display =
                    opt.forcedKey[i] ? 'block' : 'none';
            }
        }

        const nanKeys = pad.querySelectorAll('.nkpNan,.nkpNum');
        for(let i in nanKeys) {
            if(typeof nanKeys[i] !== 'object') continue;
            let flg = opt.numeric;
            if(nanKeys[i].className.indexOf('nkpNum') !== -1) flg ^= 1;
            nanKeys[i].style.display = flg ? 'none' : '';
        }

        if(/(time|date|number|month)/.test(e.type)) {
            text.style.position = 'relative';
            text.style.top = '';
            text.style.color = '';
            text.readOnly = true;
            const ph = {
                time: 'HH:MM',
                date: 'YYYY-MM-DD',
                'datetime-local': 'YYYY-MM-DD HH:MM',
                month: 'YYYY-MM',
            };
            text.placeholder = ph[e.type] !== undefined ? ph[e.type] : '';
            text.value = text.value.replace(/[^\d-:\/.]/g, ' ');

            if(!touchFlg && e.type !== 'number') {
                const now = Date.now();
                function blur() {
                    if(Date.now() - now < 1000) {
                        setTimeout(function(){
                            e.blur();
                            blur();
                            e.value = eValue;
                        },100);
                    }
                }
                blur();
            }
        }
        else {
            text.style.position = 'absolute';
            text.style.top = '-100px';
        }

        np.style.overflow = 'hidden';
        np.style.top = (6 + e.offsetTop + e.clientHeight) + 'px';
        np.style.left = e.offsetLeft + 'px';

        pad.style.display = 'block';
        disabledDefaultPicker(e);

        obj.targetStyleBackBorder = e.style.border;
        e.style.border = '2px solid black';

        obj.target = e;
        obj.view = true;

        obj.dblclickId = handler.add(np, 'dblclick', function(e) {
            e.preventDefault();
        }, { passive: false });
        obj.touchmoveId = handler.add(np, 'touchmove', function(e) {
            e.preventDefault();
        }, { passive: false });

        if(!touchFlg) {
            document.querySelector('#numKeyPad #nkpCb').type = 'text';
            if(/(date|time)/.test(e.type)) {
                const typeBuf = e.type;
                e.type = 'text';
                setTimeout(function(){
                    e.type = typeBuf;
                });
            }
        }
        setTimeout(function(){
            document.querySelector('#numKeyPad #nkpCb').focus();
        });
        obj.openTime = Date.now();
    }

    function closeNumKeyPad() {
        if(Date.now() - obj.openTime < 500) return;
        if(obj.view !== undefined && obj.view === true) {
            obj.view = false;
            document.querySelector('#numKeyPad').style.display = 'none';
        }
        if(obj.touchmoveId) {
            handler.remove(obj.touchmoveId);
            obj.touchmoveId = 0;
        }
        if(obj.dblclickId) {
            handler.remove(obj.dblclickId);
            obj.dblclickId = 0;
        }
        obj.target.style.border = obj.targetStyleBackBorder;
    }

    function disabledDefaultPicker(e) {
        if(e.tagName !== 'INPUT') return;
        const cType = e.type;
        const cn = e.cloneNode();
        e.before(cn);
        e.type = 'hidden';
        setTimeout(function() {
            e.value = cn.value;
            e.type = cType;
            cn.remove();
        });
    }
}

CSS

numkeypad.css
#numKeyPad {
    user-select: none;
    -webkit-user-select: none;
    -moz-user-select: none;
}
#numKeyPad #nkpPad {
    position: absolute;
    width: auto;
    height: auto;
    border: 1px gray solid;
    border-radius: 4px;
    cursor: move;
    background: #fff;
    opacity: 0.95;
    padding: 4px 4px 0 4px;
}
#numKeyPad .nkpL {
    display: flex;
    height: 64px;
}
#numKeyPad [id^='nkpKey'] {
    box-sizing: border-box;
    width: 60px;
    height: 60px;
    margin-left: 4px;
    padding: 0px;
    font-size: 25px;
    color: gray;
    border-radius: 4px;
    border: 1px solid gray;
    background: #fff;
    -webkit-appearance: none;
    -webkit-user-select: none;
    cursor: pointer;
}
#numKeyPad [id^='nkpKey']:first-child {
    margin-left: 0px;
}
#numKeyPad .nkpVl {
    height: 124px;
}
#numKeyPad .nkpWl {
    width: 124px;
}
#numKeyPad [id='nkpText'] {
    box-sizing: border-box;
    width: 100%;
    border-radius: 4px;
    border: 1px solid gray;
    margin: 0 0 4px 0;
}

#numKeyPad .nkpForce {
    display: grid;
    margin-left: 4px;
}
#numKeyPad .nkpForce button {
    display: inline-block;
    margin-left: 0px;
    margin-bottom: 4px;
}

使用例

index.html
<!DOCTYPE html>
<html lang='ja'>
<head>
    <meta name='viewport' content='user-scalable=no,width=device-width,initial-scale=1'>
    <meta charset='utf-8'>
    <link rel='stylesheet' href='./numkeypad.css'>
    <script src='./numkeypad.js'></script>
</head>
<body>
    text: <input type='text' data-numKeyPad><br>
    text: <input type='text' data-numKeyPad='numeric'>(numericを指定)<br>
    text: <input type='text' data-numKeyPad='numeric; forced:"-"'>(numericを指定 forcedで"-"を許可)<br>
    text: <input type='text' data-numKeyPad='numeric; forced:".-+"'>(numericを指定 forcedで".-+"を許可)<br>
    time: <input type='time' data-numKeyPad><br>
    date: <input type='date' data-numKeyPad><br>
    month: <input type='month' data-numKeyPad><br>
    datetime-local: <input type='datetime-local' data-numKeyPad><br>
    number: <input type='number' data-numKeyPad><br>
    textarea: <textarea data-numKeyPad></textarea><br>
</body>
</html>

本来inputtextarea要素にフォーカスすると表示されるブラウザ側のソフトウェアキーボード及びデート/タイムピッカーはできるだけ表示を抑制するよう対応していますが、ブラウザごとに挙動が異なるため完全には非表示にはできず表示されてしまう場合もあります。

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
0