スマホの入力を簡単にする?「Two Tap Input」
【更新履歴】
・2026/1/30 バージョン1.0公開。
・2026/1/30 バージョン1.1公開。
・2026/1/31 バージョン1.3公開。
・2026/1/31 バージョン1.4公開。(検索、AI、変換強化)
・2026/1/31 バージョン1.4(英語版)公開。
《このツールの概要》
・スマホ画面のテキスト入力方式といえば、
フリック入力が一般的ですが、
このツールでは、独自の方式で入力します。
・まあ、フリック以外の入力方式を試すために作った
という感じですが、意外とはやるかもしれません。^^;
・入力したテキストは、
コピ(コピー)ボタンを押すことで
他のwebページなどで使用できる他、
検索ボタンやAIボタンを押すことで
Googleの検索結果を表示したり、
ChatGptからの解答を表示することができます。
《 PCでスマホの画面を試す方法 》
・htmlファイルをクリックして、Chromeで開く。
・Chromeのメニューバーから、
「設定」→「その他のツール」→「デベロッパーツール」
をクリックして、スマホのマークをクリックすると、
スマホの画面で操作することができます。
・ダウンロードされる方はこちら。↓
・ソースコードはこちら。↓
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>Two Tap Input 1.4.3</title>
<style>
:root {
--bg-color: #121212;
--text-color: #fff;
/* 行カラー */
--c-a: #e60012; --c-k: #004098; --c-s: #40e0d0; --c-t: #ffd900;
--c-n: #009944; --c-h: #ff69b4; --c-m: #800080; --c-y: #f39800;
--c-r: #b08d55; --c-w: #6cbb5a;
--c-sym: #555555;
/* タブカラー */
--tab-hira: #6f4b3e; --tab-kata: #1d2088; --tab-eng: #ff69b4;
--tab-num: #40e0d0;
--tab-history: #8b4513;
--tab-emoji: #ffd900;
/* ボタン色設定 */
--col-ent: #ffd900;
--col-ctl: #40e0d0;
--col-ai: #ff1493;
--col-sch: #00e676;
--col-cpy: #00bfff;
--col-pst: #ff8c00;
--col-silver: #c0c0c0;
--cursor-color: #00ffff;
--bg-inactive-btn: #555555;
--bg-candidate: #444444;
--bg-chip: #666666;
}
@keyframes border-blink {
0% { border-color: var(--cursor-color); box-shadow: 0 0 5px var(--cursor-color); }
50% { border-color: transparent; box-shadow: none; }
100% { border-color: var(--cursor-color); box-shadow: 0 0 5px var(--cursor-color); }
}
body {
margin: 0; padding: 0;
background-color: var(--bg-color);
color: var(--text-color);
font-family: "Hiragino Kaku Gothic ProN", sans-serif;
height: 100vh; height: 100dvh; width: 100vw;
display: flex; flex-direction: column;
overflow: hidden; user-select: none; touch-action: none;
}
/* --- 上部エリア --- */
#top-area {
padding-top: max(env(safe-area-inset-top), 10px);
padding-bottom: 2px; padding-left: 5px; padding-right: 5px;
flex-shrink: 0; z-index: 100; box-sizing: border-box;
background: #000; border-bottom: 2px solid #333;
display: flex; flex-direction: column; gap: 3px;
transition: all 0.3s ease;
}
/* エディタ全画面モード */
#top-area.fullscreen-editor {
position: fixed; top: 0; left: 0; right: 0; bottom: 0;
height: 100dvh; width: 100vw;
padding-bottom: max(env(safe-area-inset-bottom), 20px);
background: #000; z-index: 5000;
}
#top-area.fullscreen-editor #display-wrapper { height: 100%; flex-direction: column; }
#top-area.fullscreen-editor #line-indicator { width: 100%; flex-direction: row; height: 30px; gap: 10px; padding: 0 10px; }
#top-area.fullscreen-editor #input-line { flex: 1; height: auto; font-size: 20px; }
#top-area.fullscreen-editor .side-controls,
#top-area.fullscreen-editor #sub-bar { display: none; }
/* 候補全画面モード */
#top-area.fullscreen-cand {
position: fixed; top: 0; left: 0; right: 0; bottom: 0;
height: 100dvh; width: 100vw;
padding-bottom: max(env(safe-area-inset-bottom), 20px);
background: #000; z-index: 5000;
display: flex; flex-direction: column;
}
#top-area.fullscreen-cand #display-wrapper { display: none; }
#top-area.fullscreen-cand #sub-bar { height: 100%; flex-direction: column; }
#top-area.fullscreen-cand #candidate-bar { flex: 1; align-content: flex-start; }
#top-area.fullscreen-cand .extended-tools {
height: 60px; flex-shrink: 0; flex-direction: row; width: 100%;
justify-content: flex-end; padding: 5px; box-sizing: border-box;
}
#top-area.fullscreen-cand .tool-col { width: 60px; height: 100%; }
#display-wrapper {
display: flex; align-items: stretch;
height: 90px; gap: 4px;
transition: height 0.3s;
}
/* 全画面解除用のフローティングボタン (エディタ用) */
#exit-full-editor-btn {
display: none;
position: absolute; bottom: 20px; right: 20px;
width: 50px; height: 50px; border-radius: 25px;
background: rgba(255, 217, 0, 0.8); color: #000;
font-size: 24px; border: 2px solid #fff;
align-items: center; justify-content: center;
z-index: 5001;
}
#top-area.fullscreen-editor #exit-full-editor-btn { display: flex; }
/* 行番号 */
#line-indicator {
width: 40px;
display: flex; flex-direction: column;
align-items: center; justify-content: flex-start;
padding-top: 6px;
background: #111; border-radius: 4px; flex-shrink: 0;
line-height: 1.0;
}
.ln-num { font-size: 20px; font-weight: bold; color: #fff; }
.ln-label { font-size: 10px; color: #fff; margin-top: 2px; }
/* 入力欄 */
#input-line {
font-size: 18px; line-height: 1.5;
white-space: pre-wrap; overflow-y: auto;
flex: 1;
border: 2px solid #555; background: #ffffff; color: #000000;
padding: 6px; border-radius: 6px;
word-break: break-all;
outline: none; caret-color: #000;
}
[contenteditable] { -webkit-user-select: text; user-select: text; }
.composing-span { background: #d0ebff; border-bottom: 2px solid #00b4d8; }
/* サイドコントロール */
.side-controls {
display: grid;
grid-template-columns: 1fr 1fr;
grid-template-rows: 1fr 1fr;
gap: 2px; width: 70px; flex-shrink: 0;
}
.ctrl-btn {
border: 2px solid transparent; border-radius: 4px;
color: #000; font-size: 16px; font-weight: bold;
cursor: pointer; display: flex; align-items: center; justify-content: center;
background: var(--col-ctl); box-sizing: border-box;
}
.btn-ent { background: var(--col-ent); font-size: 20px; color: #000; }
.btn-full { background: var(--col-ctl); color: #000; font-size: 16px; border: 1px solid #333; }
/* ホバーエフェクト */
.ctrl-btn:hover, .ctrl-btn:active,
.sub-btn:hover, .sub-btn:active,
.key:hover, .key:active,
.cand-chip:hover, .cand-chip:active {
border: 2px solid var(--cursor-color);
animation: border-blink 0.8s infinite;
z-index: 10; position: relative;
}
#sub-bar {
height: 100px;
display: flex; gap: 4px; flex-shrink: 0; align-items: stretch;
}
#candidate-bar {
flex: 1; display: flex; flex-wrap: wrap; align-content: flex-start; overflow-y: auto;
background: var(--bg-candidate); padding: 4px; border-radius: 4px; gap: 4px;
}
.cand-chip {
background: #ffffff; color: #000000;
padding: 6px 14px;
border-radius: 6px;
font-size: 18px; font-weight: bold;
border: 2px solid #ccc;
height: auto; min-height: 36px;
box-sizing: border-box; display: flex; align-items: center;
cursor: pointer;
}
.extended-tools {
display: flex; gap: 3px; flex-shrink: 0;
}
.tool-col {
display: flex; flex-direction: column; gap: 2px;
width: 40px; flex-shrink: 0;
}
.tool-col.col-full { width: 40px; }
.sub-btn {
border: 2px solid transparent; border-radius: 4px;
font-size: 11px; font-weight: bold;
display: flex; align-items: center; justify-content: center;
cursor: pointer; box-sizing: border-box; width: 100%;
}
.tool-col .sub-btn { flex: 1; }
.btn-cpy { background: var(--col-cpy); color: #fff; }
.btn-pst { background: var(--col-pst); color: #fff; }
.btn-cand-full { background: var(--col-ctl); color: #000; font-size: 16px; border: 1px solid #333; }
.btn-sch { background: var(--col-sch); color: #fff; font-size: 10px; }
.btn-ai { background: var(--col-ai); color: #fff; font-size: 12px; }
/* --- メインエリア --- */
#main-stage {
flex: 1; position: relative; overflow: hidden;
background: #111; padding: 2px;
}
#twothumb-board {
display: grid;
grid-template-columns: 1fr 2.8fr;
gap: 6px;
height: 100%; width: 100%; box-sizing: border-box;
}
#left-panel { display: grid; grid-template-columns: 1fr; grid-auto-rows: 1fr; gap: 3px; }
#left-panel.jp-layout { grid-template-columns: 1fr 1fr; grid-template-rows: repeat(6, 1fr); }
#right-panel { display: grid; grid-template-columns: repeat(3, 1fr); grid-template-rows: repeat(4, 1fr); gap: 3px; }
#right-panel.jp-layout { grid-template-columns: repeat(3, 1fr); grid-template-rows: repeat(5, 1fr); }
#right-panel.sym-layout { grid-template-columns: repeat(2, 1fr); grid-template-rows: repeat(4, 1fr); }
#right-panel.list-layout { grid-template-columns: 1fr; grid-template-rows: repeat(4, 1fr); }
/* 英語高密度レイアウト: 5列 x 6行 = 30キー */
#right-panel.eng-layout {
grid-template-columns: repeat(5, 1fr);
grid-template-rows: repeat(6, 1fr);
}
.key {
display: flex; align-items: center; justify-content: center;
font-weight: bold; border-radius: 5px;
cursor: pointer; color: #fff;
box-shadow: 0 2px 0 rgba(0,0,0,0.3);
border: 2px solid transparent; box-sizing: border-box;
text-align: center; line-height: 1.1; overflow: hidden;
}
.key-left.active {
border-color: var(--cursor-color);
animation: border-blink 1s infinite;
filter: brightness(1.2);
z-index: 5;
}
.font-emoji { font-family: "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji", sans-serif; }
.row-a { background: var(--c-a); } .row-k { background: var(--c-k); }
.row-s { background: var(--c-s); color: #000; text-shadow: none; }
.row-t { background: var(--c-t); color: #000; text-shadow: none; }
.row-n { background: var(--c-n); } .row-h { background: var(--c-h); }
.row-m { background: var(--c-m); } .row-y { background: var(--c-y); color: #000; text-shadow: none; }
.row-r { background: var(--c-r); } .row-w { background: var(--c-w); }
.row-sym { background: var(--c-sym); }
.key-del { background: #d62828; font-size: 14px; }
.key-cat { background: var(--bg-inactive-btn); border: 1px solid #777; font-size: 14px; color: #fff; }
.key-cat.active { background: #eee; color: #000; border: 2px solid #fff; }
.key-pager { background: var(--col-silver); font-size: 20px; color: #000; border: 1px solid #999; }
.key-char { background: #333; font-size: 28px; }
.key-jp-char { font-size: 24px; }
/* 英字高密度モード用のスタイル調整 */
.eng-layout .key-char { font-size: 20px; }
.key-hist { background: #333; font-size: 14px; color: #fff; text-align: left; padding: 4px; justify-content: flex-start; white-space: nowrap; text-overflow: ellipsis; display: block; overflow: hidden; }
/* --- 下部タブ --- */
#tab-bar {
height: auto;
min-height: 50px;
background: #111; border-top: 1px solid #333;
display: flex; flex-shrink: 0; z-index: 10;
/* 余白をさらに拡大 (+15px) */
padding-bottom: max(env(safe-area-inset-bottom) + 70px, 70px);
}
.tab {
flex: 1; display: flex; align-items: center; justify-content: flex-start;
font-size: 12px; font-weight: bold; color: #888;
border-right: 1px solid #222; flex-direction: column;
cursor: pointer; background: #222; transition: all 0.2s;
line-height: 1.1;
padding-top: 8px;
}
.tab-lbl-main { font-size: 14px; margin-bottom: 2px;}
.tab-lbl-sub { font-size: 9px; opacity: 0.8; }
.t-hira.active { background: var(--tab-hira); color: #fff; }
.t-kata.active { background: var(--tab-kata); color: #fff; }
.t-eng.active { background: var(--tab-eng); color: #fff; }
.t-num.active { background: var(--tab-num); color: #000; }
.t-hist.active { background: var(--tab-history); color: #fff; }
.t-emoji.active { background: var(--tab-emoji); color: #000; }
</style>
</head>
<body>
<div id="top-area">
<div id="display-wrapper">
<div id="line-indicator">
<div class="ln-num" id="ln-num-text">1</div>
<div class="ln-label">行</div>
</div>
<div id="input-line" contenteditable="true" inputmode="none" spellcheck="false"
oninput="handleInput()" onclick="updateLineNum()" onkeyup="updateLineNum()"></div>
<div class="side-controls">
<button class="ctrl-btn btn-ent no-focus-steal" onmousedown="event.preventDefault()" onclick="insertDirect('\n')">↵</button>
<button class="ctrl-btn no-focus-steal" onmousedown="event.preventDefault()" onclick="moveLine(-1)">▲</button>
<button class="ctrl-btn btn-full no-focus-steal" onmousedown="event.preventDefault()" onclick="toggleEditorFull()">□</button>
<button class="ctrl-btn no-focus-steal" onmousedown="event.preventDefault()" onclick="moveLine(1)">▼</button>
</div>
</div>
<div id="sub-bar">
<div id="candidate-bar"></div>
<div class="extended-tools">
<div class="tool-col col-full">
<button class="sub-btn btn-cand-full no-focus-steal" onmousedown="event.preventDefault()" onclick="toggleCandFull()">□</button>
</div>
<div class="tool-col">
<button class="sub-btn btn-cpy no-focus-steal" onmousedown="event.preventDefault()" onclick="copyAll()">コピ</button>
<button class="sub-btn btn-pst no-focus-steal" onmousedown="event.preventDefault()" onclick="pasteFromClipboard()">貼付</button>
</div>
<div class="tool-col">
<button class="sub-btn btn-sch no-focus-steal" onmousedown="event.preventDefault()" onclick="searchWeb()">検索</button>
<button class="sub-btn btn-ai no-focus-steal" onmousedown="event.preventDefault()" onclick="askAI()">AI</button>
</div>
</div>
</div>
<div id="exit-full-editor-btn" onclick="toggleEditorFull()">×</div>
</div>
<div id="main-stage">
<div id="twothumb-board">
<div id="left-panel"></div>
<div id="right-panel"></div>
</div>
</div>
<div id="tab-bar">
<div class="tab t-hira active" onclick="switchMode('hira', this)"><span class="tab-lbl-main">あ</span><span class="tab-lbl-sub">平仮名</span></div>
<div class="tab t-kata" onclick="switchMode('kata', this)"><span class="tab-lbl-main">ア</span><span class="tab-lbl-sub">カタカナ</span></div>
<div class="tab t-eng" onclick="switchMode('english', this)"><span class="tab-lbl-main">Abc</span><span class="tab-lbl-sub">英字</span></div>
<div class="tab t-num" onclick="switchMode('num', this)"><span class="tab-lbl-main">123</span><span class="tab-lbl-sub">数記</span></div>
<div class="tab t-hist" onclick="switchMode('history', this)"><span class="tab-lbl-main">🕒</span><span class="tab-lbl-sub">履歴</span></div>
<div class="tab t-emoji" onclick="switchMode('emoji', this)"><span class="tab-lbl-main">😀</span><span class="tab-lbl-sub">絵文字</span></div>
</div>
<script>
const rainbowColors = ['#e60012','#f39800','#ffd900','#99cc00','#009944','#00a0e9','#004098','#4b0082','#800080','#e60033'];
const jpRows = [{id:'a',l:'あ',c:'row-a'},{id:'k',l:'か',c:'row-k'},{id:'s',l:'さ',c:'row-s'},{id:'t',l:'た',c:'row-t'},{id:'n',l:'な',c:'row-n'},{id:'h',l:'は',c:'row-h'},{id:'m',l:'ま',c:'row-m'},{id:'y',l:'や',c:'row-y'},{id:'r',l:'ら',c:'row-r'},{id:'w',l:'わ',c:'row-w'}];
const jpMap = {
'hira': {'a':['あ','ぁ','', 'い','ぃ','', 'う','ぅ','', 'え','ぇ','', 'お','ぉ',''], 'k':['か','が','', 'き','ぎ','', 'く','ぐ','', 'け','げ','', 'こ','ご',''], 's':['さ','ざ','', 'し','じ','', 'す','ず','', 'せ','ぜ','', 'そ','ぞ',''], 't':['た','だ','', 'ち','ぢ','', 'つ','づ','っ', 'て','で','', 'と','ど',''], 'n':['な','','', 'に','','', 'ぬ','','', 'ね','','', 'の','',''], 'h':['は','ば','ぱ', 'ひ','び','ぴ', 'ふ','ぶ','ぷ', 'へ','べ','ぺ', 'ほ','ぼ','ぽ'], 'm':['ま','','', 'み','','', 'む','','', 'め','','', 'も','',''], 'y':['や','ゃ','', '','','', 'ゆ','ゅ','', '','','', 'よ','ょ',''], 'r':['ら','','', 'り','','', 'る','','', 'れ','','', 'ろ','',''], 'w':['わ','ゎ','', 'を','','', 'ん','','', '','','', '','',''], 'sym':[' ', '\n', '、', '。', '「', '」', '(', ')']},
'kata': {'a':['ア','ァ','', 'イ','ィ','', 'ウ','ゥ','', 'エ','ェ','', 'オ','ォ',''], 'k':['カ','ガ','', 'キ','ギ','', 'ク','グ','', 'ケ','ゲ','', 'コ','ゴ',''], 's':['サ','ザ','', 'シ','ジ','', 'ス','ズ','', 'セ','ゼ','', 'ソ','ゾ',''], 't':['タ','ダ','', 'チ','ヂ','', 'ツ','ヅ','ッ', 'テ','デ','', 'ト','ド',''], 'n':['ナ','','', 'ニ','','', 'ヌ','','', 'ネ','','', 'ノ','',''], 'h':['ハ','バ','パ', 'ヒ','ビ','ピ', 'フ','ブ','プ', 'ヘ','ベ','ペ', 'ホ','ボ','ポ'], 'm':['マ','','', 'ミ','','', 'ム','','', 'メ','','', 'モ','',''], 'y':['ヤ','ャ','', '','','', 'ユ','ュ','', '','','', 'ヨ','ョ',''], 'r':['ラ','','', 'リ','','', 'ル','','', 'レ','','', 'ロ','',''], 'w':['ワ','ヮ','', 'ヲ','','', 'ン','','', '','','', '','',''], 'sym':[' ', '\n', '、', '。', '「', '」', '(', ')']}
};
const numCategories = [{id:'digits',l:'数字',src:"1234567890".split("")},{id:'math',l:'式',src:"+-*/=<>".split("")},{id:'point',l:'点',src:".,:;".split("")},{id:'bra',l:'カッコ',src:"()[]{}''\"\"".split("")},{id:'other',l:'その他',src:"%$#&@^|~`".split("")}];
// 英字レイアウト設定
const englishPages = [
// Page 0: Lowercase (高密度5x6レイアウト用: 26文字+記号+SP+DEL)
"abcdefghijklmnopqrstuvwxyz,.".split(""),
// Page 1: Uppercase
"ABCDEFGHIJKLMNOPQRSTUVWXYZ,.".split(""),
// Page 2: Symbols (標準3x4レイアウト)
".,!?@_#+-*/=".split("")
];
// 英単語辞書 (簡易版)
const englishWords = [
"the", "be", "to", "of", "and", "a", "in", "that", "have", "I", "it", "for", "not", "on", "with", "he", "as", "you", "do", "at", "this", "but", "his", "by", "from", "they", "we", "say", "her", "she", "or", "an", "will", "my", "one", "all", "would", "there", "their", "what", "so", "up", "out", "if", "about", "who", "get", "which", "go", "me", "when", "make", "can", "like", "time", "no", "just", "him", "know", "take", "people", "into", "year", "your", "good", "some", "could", "them", "see", "other", "than", "then", "now", "look", "only", "come", "its", "over", "think", "also", "back", "after", "use", "two", "how", "our", "work", "first", "well", "way", "even", "new", "want", "because", "any", "these", "give", "day", "most", "us", "is", "are", "was", "were", "hello", "thank", "please", "yes", "sorry"
];
const emojiData = [..."😀😁😂🤣😃😄😅😆😉😊😋😎😍😘🥰😗😙😚🙂🤗🤩🤔🤨😐😑😶🙄😏😣😥😮🤐😯😪😫😴😌😛😜😝🤤😒😓😔😕🙃🤑😲☹️🙁😖😞😟😤😢😭😦😧😨😩🤯😬😰😱🥵🥶😳🤪😵😡😠🤬😷🤒"];
const basicDictionary = [{k:'あ',v:['有る']},{k:'あい',v:['愛','AI']},{k:'か',v:['可','蚊']},{k:'かい',v:['貝','回','会']},{k:'きょう',v:['今日']},{k:'わたし',v:['私']},{k:'あした',v:['明日']}];
let userDictionary = {};
let historyData = [];
let currentMode = 'hira';
let currentSelection = 'a';
let isNumFullWidth = false;
let composingText = "";
const inputEl = document.getElementById('input-line');
const candBar = document.getElementById('candidate-bar');
switchMode('hira', document.querySelector('.t-hira'));
inputEl.focus();
document.addEventListener('selectionchange', () => {
if(document.activeElement === inputEl) updateLineNum();
});
function switchMode(mode, tabEl) {
currentMode = mode;
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
if(tabEl) tabEl.classList.add('active');
const lp = document.getElementById('left-panel');
const rp = document.getElementById('right-panel');
lp.className = ""; rp.className = "";
// 英語以外は確定
if (mode !== 'hira' && mode !== 'kata' && mode !== 'english') confirmComposition();
if (mode === 'hira' || mode === 'kata') {
lp.classList.add("jp-layout"); rp.classList.add("jp-layout"); currentSelection = 'a'; renderLeftJP(); renderRightJP();
} else if (mode === 'num') {
currentSelection = 0; isNumFullWidth = false; renderLeftCategories(); renderRightCategory();
} else if (mode === 'emoji') {
currentSelection = 0; renderLeftEmojiNav(); renderRightEmoji();
} else if (mode === 'history') {
currentSelection = 0; renderLeftHistoryNav(); renderRightHistory();
} else if (mode === 'english') {
currentSelection = 0; renderLeftPagesEnglish(); renderRightPageEnglish();
}
}
function createKey(cls, text, onClick) {
const div = document.createElement('div');
div.className = cls + " no-focus-steal"; div.innerText = text;
div.onmousedown = (e) => e.preventDefault(); div.onclick = onClick; return div;
}
function renderLeftJP() {
const el = document.getElementById('left-panel'); el.innerHTML = "";
jpRows.forEach(row => {
const btn = createKey(`key key-left ${row.c}`, row.l, () => { currentSelection = row.id; renderLeftJP(); renderRightJP(); });
if(row.id === currentSelection) btn.classList.add('active'); el.appendChild(btn);
});
const symBtn = createKey("key key-left row-sym", "記", () => { currentSelection = 'sym'; renderLeftJP(); renderRightJP(); });
if(currentSelection === 'sym') symBtn.classList.add('active'); el.appendChild(symBtn);
el.appendChild(createKey("key key-left key-del", "削除", deleteChar));
}
function renderRightJP() {
const el = document.getElementById('right-panel'); el.innerHTML = "";
if(currentSelection === 'sym') el.className = 'sym-layout'; else el.className = 'jp-layout';
const map = (currentMode === 'kata') ? jpMap['kata'] : jpMap['hira'];
const chars = map[currentSelection] || [];
const activeL = document.querySelector('.key-left.active');
const bg = activeL ? window.getComputedStyle(activeL).backgroundColor : '#333';
const fg = activeL ? window.getComputedStyle(activeL).color : '#fff';
chars.forEach(char => {
let label = char; if(char === ' ') label = '空白'; if(char === '\n') label = '改行';
const btn = createKey("key key-right key-jp-char", label, () => {
if (currentSelection === 'sym') insertDirect(char); else insertComposing(char);
});
if(char || char === ' ' || char === '\n') {
btn.style.backgroundColor = bg; btn.style.color = fg; btn.style.filter = "brightness(0.9)";
} else {
btn.style.background = "transparent"; btn.style.boxShadow = "none"; btn.style.border = "none"; btn.onclick = null; btn.classList.remove('key');
}
el.appendChild(btn);
});
}
// --- 英字モード用 ---
function renderLeftPagesEnglish() {
const el = document.getElementById('left-panel'); el.innerHTML = "";
const labels = ["abc", "ABC", "記号"];
labels.forEach((lbl, idx) => {
const btn = createKey("key key-cat", lbl, () => { currentSelection = idx; renderLeftPagesEnglish(); renderRightPageEnglish(); });
if(idx === currentSelection) btn.classList.add('active');
el.appendChild(btn);
});
}
function renderRightPageEnglish() {
const el = document.getElementById('right-panel'); el.innerHTML = "";
const items = englishPages[currentSelection] || [];
// 0(小文字)と1(大文字)は高密度レイアウト、2(記号)は通常
const isDense = (currentSelection === 0 || currentSelection === 1);
if (isDense) {
el.className = "eng-layout"; // 5x6 grid
} else {
el.className = ""; // 3x4 grid
}
items.forEach(char => {
const btn = createKey("key key-char", char, () => {
// 記号以外は単語予測のためcomposingに入れる
if (isDense) insertComposing(char);
else insertDirect(char);
});
// 色付けロジック
if (!isDense) {
// 記号ページの簡易的な色付け
btn.style.backgroundColor = "var(--col-silver)"; btn.style.color = "#000";
} else {
// アルファベットの色分け
const code = char.toLowerCase().charCodeAt(0) - 97;
if (code >= 0 && code < 26) {
const cIdx = Math.floor(code / 3) % rainbowColors.length;
btn.style.backgroundColor = rainbowColors[cIdx];
if(['#ffd900', '#99cc00', '#f39800', '#40e0d0'].includes(rainbowColors[cIdx])) btn.style.color = "#000";
} else {
btn.style.backgroundColor = "#555";
}
}
el.appendChild(btn);
});
// 残りのスペースに機能キーを追加
if (isDense) {
// 28文字入ったので残り2枠 (30 - 28)
const spc = createKey("key key-char", "Space", () => insertDirect(" "));
spc.style.backgroundColor = "#777"; spc.style.fontSize="12px";
el.appendChild(spc);
const del = createKey("key key-char", "Del", deleteChar);
del.style.backgroundColor = "#d62828"; del.style.fontSize="12px";
el.appendChild(del);
} else {
// 記号ページ用
const spc = createKey("key key-char", "空白", () => insertDirect(" "));
spc.style.backgroundColor = "#777"; spc.style.fontSize="16px";
el.appendChild(spc);
const del = createKey("key key-char", "削除", deleteChar);
del.style.backgroundColor = "#d62828"; del.style.fontSize="16px";
el.appendChild(del);
}
}
function renderLeftCategories() {
const el = document.getElementById('left-panel'); el.innerHTML = "";
numCategories.forEach((cat, idx) => {
const btn = createKey("key key-cat", cat.l, () => { currentSelection = idx; renderLeftCategories(); renderRightCategory(); });
if(idx === currentSelection) btn.classList.add('active'); el.appendChild(btn);
});
}
function renderRightCategory() {
const el = document.getElementById('right-panel'); el.innerHTML = "";
const cat = numCategories[currentSelection];
cat.src.forEach(char => {
let val = char; let display = char;
if (isNumFullWidth) { val = toFullWidth(char); display = val; }
const btn = createKey("key key-char", display, () => insertDirect(val));
btn.style.backgroundColor = "#d1f5fd"; btn.style.color = "#000"; btn.style.textShadow = "none";
el.appendChild(btn);
});
if (cat.id === 'other') {
const fullHalf = createKey("key key-char", isNumFullWidth?"全":"半", () => {
isNumFullWidth = !isNumFullWidth; renderRightCategory();
});
fullHalf.style.backgroundColor = "#40e0d0"; fullHalf.style.color="#000"; fullHalf.style.fontSize="16px";
el.appendChild(fullHalf);
const spc = createKey("key key-char", "空白", () => insertDirect(isNumFullWidth?'\u3000':' '));
spc.style.backgroundColor = "#777"; spc.style.color="#fff"; spc.style.fontSize="16px";
el.appendChild(spc);
const del = createKey("key key-char", "削除", deleteChar);
del.style.backgroundColor = "#d62828"; del.style.color="#fff"; del.style.fontSize="16px";
el.appendChild(del);
}
}
function renderLeftEmojiNav() {
const el = document.getElementById('left-panel'); el.innerHTML = "";
el.appendChild(createKey("key key-pager", "▲", () => { if(currentSelection > 0) { currentSelection--; renderRightEmoji(); } }));
el.appendChild(createKey("key key-pager", "▼", () => { const maxPage = Math.ceil(emojiData.length / 12) - 1; if(currentSelection < maxPage) { currentSelection++; renderRightEmoji(); } }));
}
function renderRightEmoji() {
const el = document.getElementById('right-panel'); el.innerHTML = "";
const items = emojiData.slice(currentSelection * 12, currentSelection * 12 + 12);
items.forEach(char => {
const btn = createKey("key key-char font-emoji", char, () => insertDirect(char));
btn.style.fontSize = "24px"; btn.style.backgroundColor = "#ffffff"; btn.style.color = "#000"; btn.style.textShadow = "none";
el.appendChild(btn);
});
}
// --- 履歴モード関連 ---
function addToHistory(text) {
if(!text) return;
text = text.trim();
if(!text) return;
historyData = historyData.filter(t => t !== text);
historyData.unshift(text);
if(historyData.length > 100) historyData.pop();
if(currentMode === 'history') { renderLeftHistoryNav(); renderRightHistory(); }
}
function renderLeftHistoryNav() {
const el = document.getElementById('left-panel'); el.innerHTML = "";
el.appendChild(createKey("key key-pager", "▲", () => { if(currentSelection > 0) { currentSelection--; renderRightHistory(); } }));
el.appendChild(createKey("key key-pager", "▼", () => { const maxPage = Math.ceil(historyData.length / 4) - 1; if(currentSelection < maxPage) { currentSelection++; renderRightHistory(); } }));
}
function renderRightHistory() {
const el = document.getElementById('right-panel'); el.innerHTML = "";
el.className = "list-layout";
const start = currentSelection * 4;
const items = historyData.slice(start, start + 4);
items.forEach(text => {
const btn = createKey("key key-hist", text, () => insertDirect(text));
btn.style.fontSize = "16px";
btn.style.whiteSpace = "normal";
btn.style.wordBreak = "break-all";
btn.style.lineHeight = "1.2";
btn.style.justifyContent = "flex-start";
btn.style.padding = "8px";
btn.style.textAlign = "left";
el.appendChild(btn);
});
}
// Logic
function insertComposing(char) { composingText += char; updateComposingDisplay(); updateCandidates(); }
function updateComposingDisplay() {
let span = inputEl.querySelector('.composing-span');
if (!span) { span = document.createElement('span'); span.className = 'composing-span'; insertNodeAtCursor(span); }
span.innerText = composingText; placeCaretAfter(span); updateLineNum();
}
function confirmComposition(textToCommit) {
let commitVal = textToCommit;
if (commitVal === undefined) commitVal = composingText;
if (commitVal) {
// 履歴への追加
if(userDictionary[commitVal]) userDictionary[commitVal]++; else userDictionary[commitVal] = 1;
addToHistory(commitVal);
// 英語モードで単語選択時は自動でスペースを入れると便利だが、今回はシンプルな挿入にする
if(currentMode === 'english' && textToCommit) commitVal += " ";
}
// 全画面候補表示なら閉じる
document.getElementById('top-area').classList.remove('fullscreen-cand');
if (!composingText && !textToCommit) return;
let span = inputEl.querySelector('.composing-span');
if (span) {
const textNode = document.createTextNode(commitVal);
span.parentNode.replaceChild(textNode, span);
placeCaretAfter(textNode);
} else {
insertDirect(commitVal);
}
composingText = ""; candBar.innerHTML = ''; updateLineNum();
}
function insertDirect(text) {
if (composingText) confirmComposition();
document.execCommand('insertText', false, text);
if(text.trim().length > 0) addToHistory(text);
updateLineNum();
}
function deleteChar() {
if (composingText) { composingText = composingText.slice(0, -1); if (!composingText) { let span = inputEl.querySelector('.composing-span'); if(span) span.remove(); candBar.innerHTML = ''; } else { updateComposingDisplay(); updateCandidates(); } } else { document.execCommand('delete'); } updateLineNum();
}
function insertNodeAtCursor(node) {
inputEl.focus(); const sel = window.getSelection();
if (sel.rangeCount > 0) { const range = sel.getRangeAt(0); range.deleteContents(); range.insertNode(node); } else { inputEl.appendChild(node); }
}
function placeCaretAfter(node) { const range = document.createRange(); range.setStartAfter(node); range.collapse(true); const sel = window.getSelection(); sel.removeAllRanges(); sel.addRange(range); }
function toFullWidth(str) { return str.replace(/[!-~]/g, s => String.fromCharCode(s.charCodeAt(0) + 0xFEE0)).replace(/ /g, "\u3000"); }
function updateLineNum() {
const sel = window.getSelection();
if (sel.rangeCount === 0) return;
let node = sel.anchorNode;
let isInside = false;
while(node) { if(node === inputEl) { isInside = true; break; } node = node.parentNode; }
if(!isInside) return;
const range = sel.getRangeAt(0);
const preCaretRange = range.cloneRange();
preCaretRange.selectNodeContents(inputEl);
preCaretRange.setEnd(range.endContainer, range.endOffset);
const text = preCaretRange.toString();
const currentLine = text.split('\n').length;
document.getElementById('ln-num-text').innerText = currentLine;
}
function handleInput() { updateLineNum(); }
function copyAll() { navigator.clipboard.writeText(inputEl.innerText).then(()=>alert("コピーしました")); }
async function pasteFromClipboard() { try { const text = await navigator.clipboard.readText(); insertDirect(text); } catch(e) {} }
// エディタ全画面
function toggleEditorFull() {
document.getElementById('top-area').classList.toggle('fullscreen-editor');
document.getElementById('top-area').classList.remove('fullscreen-cand');
}
// 候補全画面
function toggleCandFull() {
document.getElementById('top-area').classList.toggle('fullscreen-cand');
document.getElementById('top-area').classList.remove('fullscreen-editor');
}
function moveLine(d) { inputEl.scrollTop += d * 30; }
function updateCandidates() {
candBar.innerHTML = ""; if (!composingText) return;
let candidates = [];
if (currentMode === 'english') {
// 英単語検索
const lowerComp = composingText.toLowerCase();
const matches = englishWords.filter(w => w.toLowerCase().startsWith(lowerComp));
candidates = candidates.concat(matches);
// 入力そのものも候補に
if(!candidates.includes(composingText)) candidates.unshift(composingText);
} else {
// 日本語検索
basicDictionary.filter(e => e.k.startsWith(composingText)).forEach(h => candidates = candidates.concat(h.v));
candidates.push(composingText);
if (currentMode === 'hira') candidates.push(composingText.replace(/[\u3041-\u3096]/g, c => String.fromCharCode(c.charCodeAt(0) + 0x60)));
}
candidates = [...new Set(candidates)];
candidates.sort((a, b) => (userDictionary[b]||0) - (userDictionary[a]||0));
candidates.forEach(c => {
const chip = document.createElement('div'); chip.className = 'cand-chip no-focus-steal'; chip.innerText = c;
chip.onmousedown = (e) => e.preventDefault(); chip.onclick = () => confirmComposition(c); candBar.appendChild(chip);
});
}
function searchWeb() { const t = inputEl.innerText; if(t) window.open(`https://www.google.com/search?q=${encodeURIComponent(t)}`, '_blank'); }
function askAI() { const t = inputEl.innerText; if(t) window.open(`https://chatgpt.com/?q=${encodeURIComponent(t)}`, '_blank'); }
</script>
</body>
</html>・英語版はこちら。↓
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>Two Tap Input 1.4.2 (EN)</title>
<style>
:root {
--bg-color: #121212;
--text-color: #fff;
/* Row Colors (Rainbow based) */
--c-red: #e60012; --c-org: #f39800; --c-ylw: #ffd900; --c-grn: #009944;
--c-cyn: #00a0e9; --c-blu: #004098; --c-prp: #800080; --c-gry: #555555;
/* Tab Colors */
--tab-eng: #ff69b4;
--tab-num: #40e0d0;
--tab-history: #8b4513;
--tab-emoji: #ffd900;
/* Button Colors */
--col-ent: #ffd900;
--col-ctl: #40e0d0;
--col-ai: #ff1493;
--col-sch: #00e676;
--col-cpy: #00bfff;
--col-pst: #ff8c00;
--col-silver: #c0c0c0;
--cursor-color: #00ffff;
--bg-inactive-btn: #333333;
--bg-candidate: #444444;
--bg-chip: #ffffff;
}
@keyframes border-blink {
0% { border-color: var(--cursor-color); box-shadow: 0 0 5px var(--cursor-color); }
50% { border-color: transparent; box-shadow: none; }
100% { border-color: var(--cursor-color); box-shadow: 0 0 5px var(--cursor-color); }
}
body {
margin: 0; padding: 0;
background-color: var(--bg-color);
color: var(--text-color);
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
height: 100vh; height: 100dvh; width: 100vw;
display: flex; flex-direction: column;
overflow: hidden; user-select: none; touch-action: none;
}
/* --- Top Area --- */
#top-area {
padding-top: max(env(safe-area-inset-top), 10px);
padding-bottom: 2px; padding-left: 5px; padding-right: 5px;
flex-shrink: 0; z-index: 100; box-sizing: border-box;
background: #000; border-bottom: 2px solid #333;
display: flex; flex-direction: column; gap: 3px;
transition: all 0.3s ease;
}
/* Fullscreen Editor Mode */
#top-area.fullscreen-editor {
position: fixed; top: 0; left: 0; right: 0; bottom: 0;
height: 100dvh; width: 100vw;
padding-bottom: max(env(safe-area-inset-bottom), 20px);
background: #000; z-index: 5000;
}
#top-area.fullscreen-editor #display-wrapper { height: 100%; flex-direction: column; }
#top-area.fullscreen-editor #line-indicator { width: 100%; flex-direction: row; height: 30px; gap: 10px; padding: 0 10px; }
#top-area.fullscreen-editor #input-line { flex: 1; height: auto; font-size: 20px; }
#top-area.fullscreen-editor .side-controls,
#top-area.fullscreen-editor #sub-bar { display: none; }
/* Fullscreen Candidate Mode */
#top-area.fullscreen-cand {
position: fixed; top: 0; left: 0; right: 0; bottom: 0;
height: 100dvh; width: 100vw;
padding-bottom: max(env(safe-area-inset-bottom), 20px);
background: #000; z-index: 5000;
display: flex; flex-direction: column;
}
#top-area.fullscreen-cand #display-wrapper { display: none; }
#top-area.fullscreen-cand #sub-bar { height: 100%; flex-direction: column; }
#top-area.fullscreen-cand #candidate-bar { flex: 1; align-content: flex-start; }
#top-area.fullscreen-cand .extended-tools {
height: 60px; flex-shrink: 0; flex-direction: row; width: 100%;
justify-content: flex-end; padding: 5px; box-sizing: border-box;
}
#top-area.fullscreen-cand .tool-col { width: 60px; height: 100%; }
#display-wrapper {
display: flex; align-items: stretch;
height: 90px; gap: 4px;
transition: height 0.3s;
}
#exit-full-editor-btn {
display: none;
position: absolute; bottom: 20px; right: 20px;
width: 50px; height: 50px; border-radius: 25px;
background: rgba(255, 217, 0, 0.8); color: #000;
font-size: 24px; border: 2px solid #fff;
align-items: center; justify-content: center;
z-index: 5001;
}
#top-area.fullscreen-editor #exit-full-editor-btn { display: flex; }
/* Line Indicator */
#line-indicator {
width: 40px;
display: flex; flex-direction: column;
align-items: center; justify-content: flex-start;
padding-top: 6px;
background: #111; border-radius: 4px; flex-shrink: 0;
line-height: 1.0;
}
.ln-num { font-size: 20px; font-weight: bold; color: #fff; }
.ln-label { font-size: 10px; color: #fff; margin-top: 2px; }
/* Input Line */
#input-line {
font-size: 18px; line-height: 1.5;
white-space: pre-wrap; overflow-y: auto;
flex: 1;
border: 2px solid #555; background: #ffffff; color: #000000;
padding: 6px; border-radius: 6px;
word-break: break-word;
outline: none; caret-color: #000;
}
[contenteditable] { -webkit-user-select: text; user-select: text; }
/* Composing Style: Blue underline */
.composing-span { background: #d0ebff; border-bottom: 3px solid #00b4d8; }
/* Side Controls */
.side-controls {
display: grid;
grid-template-columns: 1fr 1fr;
grid-template-rows: 1fr 1fr;
gap: 2px; width: 70px; flex-shrink: 0;
}
.ctrl-btn {
border: 2px solid transparent; border-radius: 4px;
color: #000; font-size: 16px; font-weight: bold;
cursor: pointer; display: flex; align-items: center; justify-content: center;
background: var(--col-ctl); box-sizing: border-box;
}
.btn-ent { background: var(--col-ent); font-size: 20px; color: #000; }
.btn-full { background: var(--col-ctl); color: #000; font-size: 16px; border: 1px solid #333; }
/* Hover & Active Effects */
.ctrl-btn:hover, .ctrl-btn:active,
.sub-btn:hover, .sub-btn:active,
.key:hover, .key:active,
.cand-chip:hover, .cand-chip:active {
border: 2px solid var(--cursor-color);
animation: border-blink 0.8s infinite;
z-index: 10; position: relative;
}
/* Sub Bar (Candidates & Tools) */
#sub-bar {
height: 100px;
display: flex; gap: 4px; flex-shrink: 0; align-items: stretch;
}
#candidate-bar {
flex: 1; display: flex; flex-wrap: wrap; align-content: flex-start; overflow-y: auto;
background: var(--bg-candidate); padding: 4px; border-radius: 4px; gap: 4px;
}
.cand-chip {
background: #ffffff; color: #000000;
padding: 6px 14px;
border-radius: 6px;
font-size: 18px; font-weight: bold;
border: 2px solid #ccc;
height: auto; min-height: 36px;
box-sizing: border-box; display: flex; align-items: center;
cursor: pointer;
}
.extended-tools {
display: flex; gap: 3px; flex-shrink: 0;
}
.tool-col {
display: flex; flex-direction: column; gap: 2px;
width: 40px; flex-shrink: 0;
}
.tool-col.col-full { width: 40px; }
.sub-btn {
border: 2px solid transparent; border-radius: 4px;
font-size: 11px; font-weight: bold;
display: flex; align-items: center; justify-content: center;
cursor: pointer; box-sizing: border-box; width: 100%;
}
.tool-col .sub-btn { flex: 1; }
.btn-cpy { background: var(--col-cpy); color: #fff; }
.btn-pst { background: var(--col-pst); color: #fff; }
.btn-cand-full { background: var(--col-ctl); color: #000; font-size: 16px; border: 1px solid #333; }
.btn-sch { background: var(--col-sch); color: #fff; font-size: 10px; }
.btn-ai { background: var(--col-ai); color: #fff; font-size: 12px; }
/* --- Main Stage --- */
#main-stage {
flex: 1; position: relative; overflow: hidden;
background: #111; padding: 2px;
}
#twothumb-board {
display: grid;
grid-template-columns: 1fr 2.8fr;
gap: 6px;
height: 100%; width: 100%; box-sizing: border-box;
}
#left-panel { display: grid; grid-template-columns: 1fr; grid-auto-rows: 1fr; gap: 3px; }
#right-panel { display: grid; grid-template-columns: repeat(3, 1fr); grid-template-rows: repeat(4, 1fr); gap: 3px; }
#right-panel.list-layout { grid-template-columns: 1fr; grid-template-rows: repeat(4, 1fr); }
/* 5x6 High Density Layout for English */
#right-panel.eng-layout {
grid-template-columns: repeat(5, 1fr);
grid-template-rows: repeat(6, 1fr);
}
.key {
display: flex; align-items: center; justify-content: center;
font-weight: bold; border-radius: 5px;
cursor: pointer; color: #fff;
box-shadow: 0 2px 0 rgba(0,0,0,0.3);
border: 2px solid transparent; box-sizing: border-box;
text-align: center; line-height: 1.1; overflow: hidden;
}
.key-left.active {
border-color: var(--cursor-color);
animation: border-blink 1s infinite;
filter: brightness(1.2);
z-index: 5;
}
.font-emoji { font-family: "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji", sans-serif; }
.key-cat { background: var(--bg-inactive-btn); border: 1px solid #777; font-size: 26px; color: #fff; }
.key-cat.active { background: #eee; color: #000; border: 2px solid #fff; }
.key-pager { background: var(--col-silver); font-size: 32px; color: #000; border: 1px solid #999; }
.key-char { background: #333; font-size: 28px; }
/* Smaller font for 5x6 grid */
.eng-layout .key-char { font-size: 20px; }
.key-hist { background: #333; font-size: 16px; color: #fff; text-align: left; padding: 8px; justify-content: flex-start; white-space: normal; word-break: break-all; display: block; overflow: hidden; }
/* --- Tab Bar --- */
#tab-bar {
height: auto;
min-height: 50px;
background: #111; border-top: 1px solid #333;
display: flex; flex-shrink: 0; z-index: 10;
padding-bottom: max(env(safe-area-inset-bottom) + 70px, 70px);
}
.tab {
flex: 1; display: flex; align-items: center; justify-content: flex-start;
font-size: 12px; font-weight: bold; color: #888;
border-right: 1px solid #222; flex-direction: column;
cursor: pointer; background: #222; transition: all 0.2s;
line-height: 1.1;
padding-top: 8px;
}
.tab-lbl-main { font-size: 14px; margin-bottom: 2px;}
.tab-lbl-sub { font-size: 9px; opacity: 0.8; }
.t-eng.active { background: var(--tab-eng); color: #fff; }
.t-num.active { background: var(--tab-num); color: #000; }
.t-hist.active { background: var(--tab-history); color: #fff; }
.t-emoji.active { background: var(--tab-emoji); color: #000; }
</style>
</head>
<body>
<div id="top-area">
<div id="display-wrapper">
<div id="line-indicator">
<div class="ln-num" id="ln-num-text">1</div>
<div class="ln-label">Line</div>
</div>
<div id="input-line" contenteditable="true" inputmode="none" spellcheck="false"
oninput="handleInput()" onclick="updateLineNum()" onkeyup="updateLineNum()"></div>
<div class="side-controls">
<button class="ctrl-btn btn-ent no-focus-steal" onmousedown="event.preventDefault()" onclick="insertDirect('\n')">↵</button>
<button class="ctrl-btn no-focus-steal" onmousedown="event.preventDefault()" onclick="moveLine(-1)">▲</button>
<button class="ctrl-btn btn-full no-focus-steal" onmousedown="event.preventDefault()" onclick="toggleEditorFull()">□</button>
<button class="ctrl-btn no-focus-steal" onmousedown="event.preventDefault()" onclick="moveLine(1)">▼</button>
</div>
</div>
<div id="sub-bar">
<div id="candidate-bar"></div>
<div class="extended-tools">
<div class="tool-col col-full">
<button class="sub-btn btn-cand-full no-focus-steal" onmousedown="event.preventDefault()" onclick="toggleCandFull()">□</button>
</div>
<div class="tool-col">
<button class="sub-btn btn-cpy no-focus-steal" onmousedown="event.preventDefault()" onclick="copyAll()">Copy</button>
<button class="sub-btn btn-pst no-focus-steal" onmousedown="event.preventDefault()" onclick="pasteFromClipboard()">Paste</button>
</div>
<div class="tool-col">
<button class="sub-btn btn-sch no-focus-steal" onmousedown="event.preventDefault()" onclick="searchWeb()">Web</button>
<button class="sub-btn btn-ai no-focus-steal" onmousedown="event.preventDefault()" onclick="askAI()">AI</button>
</div>
</div>
</div>
<div id="exit-full-editor-btn" onclick="toggleEditorFull()">×</div>
</div>
<div id="main-stage">
<div id="twothumb-board">
<div id="left-panel"></div>
<div id="right-panel"></div>
</div>
</div>
<div id="tab-bar">
<div class="tab t-eng active" onclick="switchMode('english', this)"><span class="tab-lbl-main">abc</span><span class="tab-lbl-sub">English</span></div>
<div class="tab t-num" onclick="switchMode('num', this)"><span class="tab-lbl-main">123</span><span class="tab-lbl-sub">Num/Op</span></div>
<div class="tab t-hist" onclick="switchMode('history', this)"><span class="tab-lbl-main">🕒</span><span class="tab-lbl-sub">History</span></div>
<div class="tab t-emoji" onclick="switchMode('emoji', this)"><span class="tab-lbl-main">😀</span><span class="tab-lbl-sub">Emoji</span></div>
</div>
<script>
const rainbowColors = ['#e60012','#f39800','#ffd900','#99cc00','#009944','#00a0e9','#004098','#4b0082','#800080','#e60033'];
// Simple English Dictionary for demo
const commonEnglishWords = [
"the","be","to","of","and","a","in","that","have","I","it","for","not","on","with","he","as","you","do","at",
"this","but","his","by","from","they","we","say","her","she","or","an","will","my","one","all","would","there","their","what",
"so","up","out","if","about","who","get","which","go","me","when","make","can","like","time","no","just","him","know","take",
"people","into","year","your","good","some","could","them","see","other","than","then","now","look","only","come","its","over","think","also",
"back","after","use","two","how","our","work","first","well","way","even","new","want","because","any","these","give","day","most","us",
"hello","hi","thanks","please","sorry","yes","why","where","right","left","help","love","great","ok","sure"
].sort();
const numCategories = [{id:'digits',l:'Digit',src:"1234567890".split("")},{id:'math',l:'Math',src:"+-*/=<>".split("")},{id:'point',l:'Point',src:".,:;".split("")},{id:'bra',l:'Brckt',src:"()[]{}''\"\"".split("")},{id:'other',l:'Other',src:"%$#&@^|~`".split("")}];
// New 3-Page English Structure
const englishPages = [
// 0: abc (26 letters)
"abcdefghijklmnopqrstuvwxyz".split(""),
// 1: ABC (26 letters)
"ABCDEFGHIJKLMNOPQRSTUVWXYZ".split(""),
// 2: Sym (26 symbols to fit 5x6 layout with 4 footer keys)
"!?'\"@#&()[]:;-+=_/<>{}$%*~|\\".split("")
];
const emojiData = [..."😀😁😂🤣😃😄😅😆😉😊😋😎😍😘🥰😗😙😚🙂🤗🤩🤔🤨😐😑😶🙄😏😣😥😮🤐😯😪😫😴😌😛😜😝🤤😒😓😔😕🙃🤑😲☹️🙁😖😞😟😤😢😭😦😧😨😩🤯😬😰😱🥵🥶😳🤪😵😡😠🤬😷🤒"];
let historyData = [];
let currentMode = 'lower';
let currentSelection = 0;
let composingText = "";
const inputEl = document.getElementById('input-line');
const candBar = document.getElementById('candidate-bar');
// Init
switchMode('english', document.querySelector('.t-eng'));
inputEl.focus();
document.addEventListener('selectionchange', () => {
if(document.activeElement === inputEl) updateLineNum();
});
function switchMode(mode, tabEl) {
currentMode = mode;
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
if(tabEl) tabEl.classList.add('active');
if (composingText) confirmComposition();
const lp = document.getElementById('left-panel');
const rp = document.getElementById('right-panel');
lp.className = ""; rp.className = "";
if (mode === 'num') {
currentSelection = 0; renderLeftCategories(); renderRightCategory();
} else if (mode === 'emoji') {
currentSelection = 0; renderLeftEmojiNav(); renderRightEmoji();
} else if (mode === 'history') {
currentSelection = 0; renderLeftHistoryNav(); renderRightHistory();
} else if (mode === 'english') {
currentSelection = 0; renderLeftPagesEnglish(); renderRightPageEnglish();
}
}
function createKey(cls, text, onClick) {
const div = document.createElement('div');
div.className = cls + " no-focus-steal"; div.innerText = text;
div.onmousedown = (e) => e.preventDefault(); div.onclick = onClick; return div;
}
// --- English Mode Renderers ---
function renderLeftPagesEnglish() {
const el = document.getElementById('left-panel'); el.innerHTML = "";
const labels = ["abc", "ABC", "Sym"];
labels.forEach((lbl, idx) => {
const btn = createKey("key key-cat", lbl, () => { currentSelection = idx; renderLeftPagesEnglish(); renderRightPageEnglish(); });
if(idx === currentSelection) btn.classList.add('active');
el.appendChild(btn);
});
}
function renderRightPageEnglish() {
const el = document.getElementById('right-panel'); el.innerHTML = "";
const items = englishPages[currentSelection] || [];
// Use High Density Layout (5x6 = 30 keys)
el.className = "eng-layout";
items.forEach(char => {
// Letters trigger composing for prediction, Symbols trigger direct insert
const isSymPage = (currentSelection === 2);
const btn = createKey("key key-char", char, () => {
if(isSymPage) insertDirect(char);
else insertComposing(char);
});
// Color logic
if (isSymPage) {
btn.style.backgroundColor = "var(--col-silver)"; btn.style.color = "#000";
} else {
const code = char.toLowerCase().charCodeAt(0) - 97;
if (code >= 0 && code < 26) {
const cIdx = Math.floor(code / 3) % rainbowColors.length;
btn.style.backgroundColor = rainbowColors[cIdx];
if(['#ffd900', '#99cc00', '#f39800', '#40e0d0'].includes(rainbowColors[cIdx])) btn.style.color = "#000";
} else {
btn.style.backgroundColor = "#555";
}
}
el.appendChild(btn);
});
// Add the 4 common footer keys
// Comma
const comma = createKey("key key-char", ",", () => insertDirect(","));
comma.style.backgroundColor = "#777"; comma.style.fontSize="20px";
el.appendChild(comma);
// Period
const period = createKey("key key-char", ".", () => insertDirect("."));
period.style.backgroundColor = "#777"; period.style.fontSize="20px";
el.appendChild(period);
// Space
const spc = createKey("key key-char", "Space", () => insertDirect(" "));
spc.style.backgroundColor = "#777"; spc.style.fontSize="14px";
el.appendChild(spc);
// Delete
const del = createKey("key key-char", "Del", deleteChar);
del.style.backgroundColor = "#d62828"; del.style.fontSize="14px";
el.appendChild(del);
}
// --- Number/Symbol Renderers ---
function renderLeftCategories() {
const el = document.getElementById('left-panel'); el.innerHTML = "";
numCategories.forEach((cat, idx) => {
const btn = createKey("key key-cat", cat.l, () => { currentSelection = idx; renderLeftCategories(); renderRightCategory(); });
if(idx === currentSelection) btn.classList.add('active'); el.appendChild(btn);
});
}
function renderRightCategory() {
const el = document.getElementById('right-panel'); el.innerHTML = "";
const cat = numCategories[currentSelection];
cat.src.forEach(char => {
const btn = createKey("key key-char", char, () => insertDirect(char));
btn.style.backgroundColor = "#d1f5fd"; btn.style.color = "#000"; btn.style.textShadow = "none";
el.appendChild(btn);
});
if (cat.id === 'other') {
const spc = createKey("key key-char", "Space", () => insertDirect(" "));
spc.style.backgroundColor = "#777"; spc.style.color="#fff"; spc.style.fontSize="16px";
el.appendChild(spc);
const del = createKey("key key-char", "Del", deleteChar);
del.style.backgroundColor = "#d62828"; del.style.color="#fff"; del.style.fontSize="16px";
el.appendChild(del);
}
}
// --- Emoji Renderers ---
function renderLeftEmojiNav() {
const el = document.getElementById('left-panel'); el.innerHTML = "";
el.appendChild(createKey("key key-pager", "▲", () => { if(currentSelection > 0) { currentSelection--; renderRightEmoji(); } }));
el.appendChild(createKey("key key-pager", "▼", () => { const maxPage = Math.ceil(emojiData.length / 12) - 1; if(currentSelection < maxPage) { currentSelection++; renderRightEmoji(); } }));
}
function renderRightEmoji() {
const el = document.getElementById('right-panel'); el.innerHTML = "";
const items = emojiData.slice(currentSelection * 12, currentSelection * 12 + 12);
items.forEach(char => {
const btn = createKey("key key-char font-emoji", char, () => insertDirect(char));
btn.style.fontSize = "24px"; btn.style.backgroundColor = "#ffffff"; btn.style.color = "#000"; btn.style.textShadow = "none";
el.appendChild(btn);
});
}
// --- History Renderers ---
function addToHistory(text) {
if(!text) return;
text = text.trim();
if(!text) return;
historyData = historyData.filter(t => t !== text);
historyData.unshift(text);
if(historyData.length > 100) historyData.pop();
if(currentMode === 'history') { renderLeftHistoryNav(); renderRightHistory(); }
}
function renderLeftHistoryNav() {
const el = document.getElementById('left-panel'); el.innerHTML = "";
el.appendChild(createKey("key key-pager", "▲", () => { if(currentSelection > 0) { currentSelection--; renderRightHistory(); } }));
el.appendChild(createKey("key key-pager", "▼", () => { const maxPage = Math.ceil(historyData.length / 4) - 1; if(currentSelection < maxPage) { currentSelection++; renderRightHistory(); } }));
}
function renderRightHistory() {
const el = document.getElementById('right-panel'); el.innerHTML = "";
el.className = "list-layout";
const start = currentSelection * 4;
const items = historyData.slice(start, start + 4);
items.forEach(text => {
const btn = createKey("key key-hist", text, () => {
insertDirect(text + " ");
});
el.appendChild(btn);
});
}
// --- Logic & Prediction ---
function insertComposing(char) {
composingText += char;
updateComposingDisplay();
updateCandidates();
}
function updateComposingDisplay() {
let span = inputEl.querySelector('.composing-span');
if (!span) {
span = document.createElement('span');
span.className = 'composing-span';
insertNodeAtCursor(span);
}
span.innerText = composingText;
placeCaretAfter(span);
updateLineNum();
}
function confirmComposition(textToCommit) {
let commitVal = textToCommit;
if (commitVal === undefined) commitVal = composingText;
if (commitVal) {
let finalStr = commitVal;
// Add space if selected from candidates
if (textToCommit !== undefined) {
finalStr += " ";
addToHistory(commitVal);
}
// Logic to append
let span = inputEl.querySelector('.composing-span');
if (span) {
const textNode = document.createTextNode(finalStr);
span.parentNode.replaceChild(textNode, span);
placeCaretAfter(textNode);
} else {
insertDirect(finalStr);
}
}
// Close fullscreen candidate if open
document.getElementById('top-area').classList.remove('fullscreen-cand');
composingText = "";
candBar.innerHTML = '';
updateLineNum();
}
function updateCandidates() {
candBar.innerHTML = "";
if (!composingText) return;
const lowerInput = composingText.toLowerCase();
let candidates = commonEnglishWords.filter(w => w.toLowerCase().startsWith(lowerInput));
if (candidates[0] !== composingText) candidates.unshift(composingText);
candidates.forEach(c => {
const chip = document.createElement('div');
chip.className = 'cand-chip no-focus-steal';
chip.innerText = c;
chip.onmousedown = (e) => e.preventDefault();
chip.onclick = () => confirmComposition(c);
candBar.appendChild(chip);
});
}
function insertDirect(text) {
if (composingText) confirmComposition();
document.execCommand('insertText', false, text);
updateLineNum();
}
function deleteChar() {
if (composingText) {
composingText = composingText.slice(0, -1);
if (!composingText) {
let span = inputEl.querySelector('.composing-span');
if(span) span.remove();
candBar.innerHTML = '';
} else {
updateComposingDisplay();
updateCandidates();
}
} else {
document.execCommand('delete');
}
updateLineNum();
}
function insertNodeAtCursor(node) {
inputEl.focus(); const sel = window.getSelection();
if (sel.rangeCount > 0) { const range = sel.getRangeAt(0); range.deleteContents(); range.insertNode(node); } else { inputEl.appendChild(node); }
}
function placeCaretAfter(node) { const range = document.createRange(); range.setStartAfter(node); range.collapse(true); const sel = window.getSelection(); sel.removeAllRanges(); sel.addRange(range); }
function updateLineNum() {
const sel = window.getSelection();
if (sel.rangeCount === 0) return;
let node = sel.anchorNode;
let isInside = false;
while(node) { if(node === inputEl) { isInside = true; break; } node = node.parentNode; }
if(!isInside) return;
const range = sel.getRangeAt(0);
const preCaretRange = range.cloneRange();
preCaretRange.selectNodeContents(inputEl);
preCaretRange.setEnd(range.endContainer, range.endOffset);
const text = preCaretRange.toString();
const currentLine = text.split('\n').length;
document.getElementById('ln-num-text').innerText = currentLine;
}
function handleInput() { updateLineNum(); }
function copyAll() { navigator.clipboard.writeText(inputEl.innerText).then(()=>alert("Copied!")); }
async function pasteFromClipboard() { try { const text = await navigator.clipboard.readText(); insertDirect(text); } catch(e) {} }
function toggleEditorFull() {
document.getElementById('top-area').classList.toggle('fullscreen-editor');
document.getElementById('top-area').classList.remove('fullscreen-cand');
}
function toggleCandFull() {
document.getElementById('top-area').classList.toggle('fullscreen-cand');
document.getElementById('top-area').classList.remove('fullscreen-editor');
}
function moveLine(d) { inputEl.scrollTop += d * 30; }
function searchWeb() { const t = inputEl.innerText; if(t) window.open(`https://www.google.com/search?q=${encodeURIComponent(t)}`, '_blank'); }
function askAI() { const t = inputEl.innerText; if(t) window.open(`https://chatgpt.com/?q=${encodeURIComponent(t)}`, '_blank'); }
</script>
</body>
</html>

コメント