input
、textarea
要素に対して数値入力を行うためのテンキー型のUIです。
###適用方法###
対応させたいinput
、textarea
要素のHTMLタグにdata-numKeyPad
を追記することで適用できます。
<input type='text' data-numKeyPad>
data-numKeyPad
にnumeric
を指定、または日時入力系のtypeに対して指定した場合は、数字とバックスペースキーのみのレイアウトになります。
<input type='text' data-numKeyPad='numeric'>
<input type='time' data-numKeyPad>
data-numKeyPad
にnumeric
を指定した場合は**/*-+.**キーが省略されたレイアウトになりますが、forced
で追加表示指定ができます。
numericを指定しつつ '.' と '-' を追加する例
<input type='text' data-numKeyPad='numeric; forced:".-"'>
スマートフォンではUI上のキータップ、PCではそれに加えてキーボードで入力可能です。
UIの表示位置はドラッグで変更可能、UI以外の部分をタップorクリック、またはEnterキーでクローズします。
###スクリプト###
'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'><</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'><</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 {
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;
}
###使用例###
<!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>
本来input
、textarea
要素にフォーカスすると表示されるブラウザ側のソフトウェアキーボード及びデート/タイムピッカーはできるだけ表示を抑制するよう対応していますが、ブラウザごとに挙動が異なるため完全には非表示にはできず表示されてしまう場合もあります。