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

【更新履歴】

・2026/1/30 バージョン1.0公開。
・2026/1/30 バージョン1.1公開。
・2026/1/31 バージョン1.3公開。


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

《 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.3.2</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;
        
        /* カーソル色 */
        --cursor-color: #00ffff;
        
        /* UI背景色 (明るく調整) */
        --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;
    }
    
    .sub-tools {
        display: grid;
        grid-template-columns: 1fr 1fr;
        grid-template-rows: 1fr 1fr;
        gap: 3px; width: 90px; 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: #2a9d8f; color: #fff; }
    .btn-pst { background: #e76f51; color: #fff; }

    /* --- メインエリア --- */
    #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); /* #555 */
        border: 1px solid #777; 
        font-size: 16px; /* 文字サイズUP */
        color: #fff; 
    }
    .key-cat.active { 
        background: #eee; 
        color: #000; 
        border: 3px solid #fff; /* 選択中は白枠 */
    }
    
    .key-pager { background: #fff; font-size: 24px; color: #000; }

    .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="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>
</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':['わ','ゎ','', 'を','','', 'ん','','', '','','', '','',''],
            // 記号 (2列 4行) 空白,改行,、,。,「,」,(,)
            '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 = [..."😀😁😂🤣😃😄😅😆😉😊😋😎😍😘🥰😗😙😚🙂🤗🤩🤔🤨😐😑😶🙄😏😣😥😮🤐😯😪😫😴😌😛😜😝🤤😒😓😔😕🙃🤑😲☹️🙁😖😞😟😤😢😭😦😧😨😩🤯😬😰😱🥵🥶😳🤪😵😡😠🤬😷🤒"];

    // --- 状態 ---
    let currentMode = 'hira';
    let currentSelection = 'a';
    let composingText = "";
    
    // DOM
    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);
        }
    }

    // --- UI レンダリング ---
    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);
        
        const delBtn = createKey("key key-left key-del", "削除", deleteChar);
        el.appendChild(delBtn);
    }

    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 renderLeftPages(mode) {
        const el = document.getElementById('left-panel');
        el.innerHTML = "";
        let srcPages = rawPages[mode] || [];
        srcPages.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));
            
            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);
        });
    }

    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];
        const chars = cat.src;
        
        chars.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);
        });
    }

    function renderLeftEmojiNav() {
        const el = document.getElementById('left-panel');
        el.innerHTML = "";
        const up = createKey("key key-pager", "▲", () => {
            if(currentSelection > 0) { currentSelection--; renderRightEmoji(); }
        });
        const down = createKey("key key-pager", "▼", () => {
             const maxPage = Math.ceil(emojiData.length / 12) - 1;
             if(currentSelection < maxPage) { currentSelection++; renderRightEmoji(); }
        });
        el.appendChild(up); el.appendChild(down);
    }
    
    function renderRightEmoji() {
        const el = document.getElementById('right-panel');
        el.innerHTML = "";
        const start = currentSelection * 12;
        const items = emojiData.slice(start, start+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);
        });
    }

    // --- 入力ロジック (contenteditable) ---
    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); 
        const range = document.createRange();
        range.selectNodeContents(span);
        range.collapse(false);
        const sel = window.getSelection();
        sel.removeAllRanges();
        sel.addRange(range);
        updateLineNum();
    }

    function confirmComposition(textToCommit) {
        if (textToCommit !== undefined) composingText = textToCommit;
        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);
        }
        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 updateCandidates() {
        candBar.innerHTML = "";
        if (!composingText) return;
        const list = [composingText];
        if (currentMode === 'hira') {
            list.push(composingText.replace(/[\u3041-\u3096]/g, c => String.fromCharCode(c.charCodeAt(0) + 0x60)));
        }
        list.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 toFullWidth(str) {
        return str.replace(/[!-~]/g, function(s) {
            return 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; }

</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