スマホの入力を簡単にする?「Two Same Input」

【更新履歴】

・2026/1/30 バージョン1.0公開。
・2026/1/30 バージョン1.1公開。
・2026/1/31 バージョン1.3公開。
・2026/1/31 バージョン1.4公開。(検索、AI、変換強化)

画像
日本語入力(ひらがな)
画像
英字入力(大文字)
画像
数字・記号入力(半角)
画像
絵文字入力(顔文字)

《このツールの概要》

・スマホ画面のテキスト入力方式といえば、
 フリック入力が一般的ですが、
 このツールでは、ツーサム方式で入力します。

・入力したテキストは、
 コピ(コピー)ボタンを押すことで
 他の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 Same Input 1.4</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-lower: #ff69b4;
        --tab-upper: #c71585; --tab-num: #40e0d0; --tab-sym: #009944;
        --tab-emoji: #ffd900;

        /* ボタン色設定 (今回変更箇所) */
        --col-ent: #ffd900;    /* 改行: 黄色 */
        --col-ctl: #40e0d0;    /* 操作系: 水色 */
        
        --col-ai: #ff1493;     /* AI: ピンク */
        --col-sch: #6b8e23;    /* 検索: 濃い黄緑 */
        --col-cpy: #00bfff;    /* コピー: 水色(DeepSkyBlue) */
        --col-pst: #ff8c00;    /* 貼付: オレンジ(DarkOrange) */
        --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), 20px);
        padding-bottom: 5px; 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: 5px;
    }

    #top-area.fullscreen {
        height: 92%; position: absolute; top: 0; left: 0; right: 0;
        background: #000;
    }

    #display-wrapper {
        display: flex; align-items: stretch;
        height: 120px; gap: 5px;
    }

    /* 行番号 */
    #line-indicator {
        width: 45px; 
        display: flex; flex-direction: column; 
        align-items: center; justify-content: flex-start;
        padding-top: 8px;
        background: #111; border-radius: 4px; flex-shrink: 0;
        line-height: 1.0;
    }
    .ln-num { font-size: 22px; font-weight: bold; color: #fff; }
    .ln-label { font-size: 11px; 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: 8px; 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: 3px; width: 90px; 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: 22px; color: #000; } 
    .btn-full { background: var(--col-ctl); color: #000; font-size: 18px; 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: 70px; display: flex; gap: 5px; 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: var(--bg-chip); padding: 6px 12px;
        border-radius: 12px; font-size: 14px; border: 2px solid transparent;
        height: 28px; box-sizing: border-box; display: flex; align-items: center; color: #fff;
        cursor: pointer;
    }
    
    .extended-tools {
        display: flex; gap: 3px;
    }
    .sub-tools {
        display: grid;
        grid-template-columns: 1fr 1fr;
        grid-template-rows: 1fr 1fr;
        gap: 3px; width: 90px; flex-shrink: 0;
    }
    .link-tools {
        display: grid;
        grid-template-columns: 1fr;
        grid-template-rows: 1fr 1fr;
        gap: 3px; width: 45px; flex-shrink: 0;
    }

    .sub-btn {
        border: 2px solid transparent; border-radius: 4px;
        font-size: 12px; font-weight: bold;
        display: flex; align-items: center; justify-content: center;
        cursor: pointer; box-sizing: border-box;
    }
    
    /* ボタン色適用 */
    .btn-mov { background: var(--col-ctl); color: #000; font-size: 14px; }
    .btn-cpy { background: var(--col-cpy); color: #fff; }
    .btn-pst { background: var(--col-pst); color: #fff; }
    .btn-sch { background: var(--col-sch); color: #fff; font-size: 11px; }
    .btn-ai  { background: var(--col-ai);  color: #fff; font-size: 14px; }

    /* --- メインエリア --- */
    #main-stage {
        flex: 1; position: relative; overflow: hidden;
        background: #111; padding: 4px;
    }

    #twothumb-board {
        display: grid;
        grid-template-columns: 1fr 2.8fr; 
        gap: 16px; height: 100%; width: 100%; box-sizing: border-box;
    }
    
    #left-panel { display: grid; grid-template-columns: 1fr; grid-auto-rows: 1fr; gap: 4px; }
    #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: 4px; }
    #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); }

    .key {
        display: flex; align-items: center; justify-content: center;
        font-weight: bold; border-radius: 6px; font-size: 22px; 
        cursor: pointer; color: #fff;
        box-shadow: 0 3px 0 rgba(0,0,0,0.3);
        border: 3px solid transparent; box-sizing: border-box;
        text-align: center; line-height: 1.1;
    }
    .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: 16px; }

    .key-cat { background: var(--bg-inactive-btn); border: 1px solid #777; font-size: 16px; color: #fff; }
    .key-cat.active { background: #eee; color: #000; border: 3px solid #fff; }
    
    /* 絵文字ページャー:シルバー */
    .key-pager { background: var(--col-silver); font-size: 24px; color: #000; border: 1px solid #999; }

    .key-char { background: #333; font-size: 24px; }
    .key-jp-char { font-size: 20px; }

    /* --- 下部タブ --- */
    #tab-bar {
        height: 60px; background: #111; border-top: 1px solid #333;
        display: flex; flex-shrink: 0; z-index: 10;
        padding-bottom: max(env(safe-area-inset-bottom), 0px);
    }
    .tab {
        flex: 1; display: flex; align-items: center; justify-content: center;
        font-size: 14px; 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;
    }
    .tab-lbl-main { font-size: 14px; }
    .tab-lbl-sub { font-size: 10px; opacity: 0.8; }
    .t-hira.active { background: var(--tab-hira); color: #fff; }
    .t-kata.active { background: var(--tab-kata); color: #fff; }
    .t-lower.active { background: var(--tab-lower); color: #fff; }
    .t-upper.active { background: var(--tab-upper); color: #fff; }
    .t-num.active { background: var(--tab-num); color: #000; }
    .t-sym.active { background: var(--tab-sym); 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()"></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="toggleFull()">□</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="sub-tools">
                <button class="sub-btn btn-mov no-focus-steal" onmousedown="event.preventDefault()" onclick="moveLine(-1)">▲</button>
                <button class="sub-btn btn-cpy no-focus-steal" onmousedown="event.preventDefault()" onclick="copyAll()">コピ</button>
                <button class="sub-btn btn-mov no-focus-steal" onmousedown="event.preventDefault()" onclick="moveLine(1)">▼</button>
                <button class="sub-btn btn-pst no-focus-steal" onmousedown="event.preventDefault()" onclick="pasteFromClipboard()">貼付</button>
            </div>
            <div class="link-tools">
                <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>

<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-lower" onclick="switchMode('lower', this)"><span class="tab-lbl-main">abc</span><span class="tab-lbl-sub">英小</span></div>
    <div class="tab t-upper" onclick="switchMode('upper', 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">数記</span><span class="tab-lbl-sub">半角</span></div>
    <div class="tab t-sym" onclick="switchMode('sym', 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 rawPages = {
        'lower': [ "abcdefghi".split(""), "jklmnopqr".split(""), "stuvwxyz".split(""), ".,!?@_".split("") ],
        'upper': [ "ABCDEFGHI".split(""), "JKLMNOPQR".split(""), "STUVWXYZ".split(""), ".,!?@_".split("") ]
    };
    const emojiData = [..."😀😁😂🤣😃😄😅😆😉😊😋😎😍😘🥰😗😙😚🙂🤗🤩🤔🤨😐😑😶🙄😏😣😥😮🤐😯😪😫😴😌😛😜😝🤤😒😓😔😕🙃🤑😲☹️🙁😖😞😟😤😢😭😦😧😨😩🤯😬😰😱🥵🥶😳🤪😵😡😠🤬😷🤒"];
    const basicDictionary = [{k:'あ',v:['有る']},{k:'あい',v:['愛','AI']},{k:'か',v:['可','蚊']},{k:'かい',v:['貝','回','会']},{k:'きょう',v:['今日']},{k:'わたし',v:['私']},{k:'あした',v:['明日']}];
    let userDictionary = {};

    let currentMode = 'hira';
    let currentSelection = 'a';
    let composingText = "";
    const inputEl = document.getElementById('input-line');
    const candBar = document.getElementById('candidate-bar');

    switchMode('hira', document.querySelector('.t-hira'));
    inputEl.focus();

    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') confirmComposition();
        if (mode === 'hira' || mode === 'kata') {
            lp.classList.add("jp-layout"); rp.classList.add("jp-layout"); currentSelection = 'a'; renderLeftJP(); renderRightJP();
        } else if (mode === 'num' || mode === 'sym') {
            currentSelection = 0; renderLeftCategories(); renderRightCategory();
        } else if (mode === 'emoji') {
            currentSelection = 0; renderLeftEmojiNav(); renderRightEmoji();
        } else {
            currentSelection = 0; renderLeftPages(mode); renderRightPage(mode);
        }
    }

    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.onclick = null; btn.classList.remove('key');
            }
            el.appendChild(btn);
        });
    }

    function renderLeftPages(mode) {
        const el = document.getElementById('left-panel'); el.innerHTML = "";
        rawPages[mode].forEach((_, idx) => {
            const btn = createKey("key key-cat", `p${idx+1}`, () => { currentSelection = idx; renderLeftPages(mode); renderRightPage(mode); });
            if(idx === currentSelection) btn.classList.add('active'); el.appendChild(btn);
        });
    }
    function renderRightPage(mode) {
        const el = document.getElementById('right-panel'); el.innerHTML = "";
        const pages = rawPages[mode]; const items = pages[currentSelection] || [];
        let alphaStart = 0; for(let i=0; i<currentSelection; i++) alphaStart += pages[i].length;
        let alphaCounter = alphaStart;
        items.forEach(char => {
            const btn = createKey("key key-char", char, () => insertDirect(char));
            // 英字モードの最終ページ(p4)は記号なのでシルバー、それ以外はレインボー
            if ((mode === 'lower' || mode === 'upper') && currentSelection === 3) {
                btn.style.backgroundColor = "var(--col-silver)"; 
                btn.style.color = "#000";
            } else {
                const cIdx = Math.floor(alphaCounter / 3) % rainbowColors.length;
                const color = rainbowColors[cIdx];
                btn.style.backgroundColor = color;
                if(['#ffd900', '#99cc00', '#f39800', '#40e0d0'].includes(color)) btn.style.color = "#000";
                alphaCounter++;
            }
            el.appendChild(btn);
        });
        // 最終ページの末尾に空白・削除追加
        if (currentSelection === pages.length - 1) {
             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 (currentMode === 'sym') { 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 spc = createKey("key key-char", "空白", () => insertDirect(currentMode==='sym'?'\u3000':' '));
             spc.style.backgroundColor = "#777"; spc.style.color="#fff"; spc.style.fontSize="16px"; spc.style.textShadow="none";
             el.appendChild(spc);
             const del = createKey("key key-char", "削除", deleteChar);
             del.style.backgroundColor = "#d62828"; del.style.color="#fff"; del.style.fontSize="16px"; del.style.textShadow="none";
             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);
        });
    }

    // 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) {
        if (textToCommit !== undefined) { composingText = textToCommit; if(userDictionary[textToCommit]) userDictionary[textToCommit]++; else userDictionary[textToCommit] = 1; }
        if (!composingText) return;
        let span = inputEl.querySelector('.composing-span');
        if (span) { const textNode = document.createTextNode(composingText); span.parentNode.replaceChild(textNode, span); placeCaretAfter(textNode); } else { insertDirect(composingText); }
        if (!textToCommit && composingText) { if(userDictionary[composingText]) userDictionary[composingText]++; else userDictionary[composingText] = 1; }
        composingText = ""; candBar.innerHTML = ''; updateLineNum();
    }
    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 toFullWidth(str) { return str.replace(/[!-~]/g, s => String.fromCharCode(s.charCodeAt(0) + 0xFEE0)).replace(/ /g, "\u3000"); }
    function updateLineNum() { const text = inputEl.innerText; const lines = text.split(/\n/).length; document.getElementById('ln-num-text').innerText = lines; }
    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 toggleFull() { document.getElementById('top-area').classList.toggle('fullscreen'); }
    function moveLine(d) { inputEl.scrollTop += d * 30; }

    function updateCandidates() {
        candBar.innerHTML = ""; if (!composingText) return;
        let candidates = [];
        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>

いいなと思ったら応援しよう!

コメント

コメントするには、 ログイン または 会員登録 をお願いします。
スマホの入力を簡単にする?「Two Same Input」|古井和雄
word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word

mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1