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

【更新履歴】

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


画像
日本語入力の画面
画像
英語入力の画面
画像
数字・記号(半角)入力の画面


・ダウンロードされる方はこちら。↓

・ソースコードはこちら。↓

<!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>ツーサム v22 (キャレット対応&数記全角)</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; /* 水色 */
    }

    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; }

    /* 入力欄 (contenteditable化) */
    #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;
    }
    /* ソフトキーボード抑制 (iOS/Android対応) */
    [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: none; 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-shadow: 0 2px 0 rgba(0,0,0,0.3);
    }
    /* ボタンを押してもフォーカスを奪わないおまじない */
    .no-focus-steal { touch-action: manipulation; }
    
    .ctrl-btn:active { transform: translateY(1px); box-shadow: none; opacity: 0.9; }
    .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; }

    /* 候補バー */
    #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: #222; padding: 4px; border-radius: 4px; gap: 4px;
    }
    .cand-chip {
        background: #444; padding: 6px 12px;
        border-radius: 12px; font-size: 14px; border: 1px solid #666;
        height: 28px; box-sizing: border-box; display: flex; align-items: center; color: #fff;
        cursor: pointer;
    }
    .cand-chip:active { background: #fff; color: #000; }
    
    /* サブツール */
    .sub-tools {
        display: grid;
        grid-template-columns: 1fr 1fr;
        grid-template-rows: 1fr 1fr;
        gap: 3px; width: 90px; flex-shrink: 0;
    }
    .sub-btn {
        border: none; border-radius: 4px;
        font-size: 12px; font-weight: bold;
        display: flex; align-items: center; justify-content: center;
        cursor: pointer;
    }
    .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: 4px; 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);
        transition: transform 0.05s;
        text-align: center; line-height: 1.1;
        user-select: none;
    }
    .key:active { transform: translateY(2px); box-shadow: none; }
    
    .font-emoji { font-family: "Apple Color Emoji", "Segoe UI Emoji", "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: #333; border: 1px solid #555; font-size: 13px; color: #fff; }
    .key-cat.active { background: #eee; color: #000; border: 2px 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':['わ','ゎ','', 'を','','', 'ん','','', '','','', '','',''],
            'sym':[' ','\n', '、', '。', '「', '」', '(', ')']
        },
        'kata': {
            'a':['ア','ァ','', 'イ','ィ','', 'ウ','ゥ','', 'エ','ェ','', 'オ','ォ',''],
            'k':['カ','ガ','', 'キ','ギ','', 'ク','グ','', 'ケ','ゲ','', 'コ','ゴ',''],
            's':['サ','ザ','', 'シ','ジ','', 'ス','ズ','', 'セ','ゼ','', 'ソ','ゾ',''],
            't':['タ','ダ','', 'チ','ヂ','', 'ツ','ヅ','ッ', 'テ','デ','', 'ト','ド',''],
            'n':['ナ','','', 'ニ','','', 'ヌ','','', 'ネ','','', 'ノ','',''],
            'h':['ハ','バ','パ', 'ヒ','ビ','ピ', 'フ','ブ','プ', 'ヘ','ベ','ペ', 'ホ','ボ','ポ'],
            'm':['マ','','', 'ミ','','', 'ム','','', 'メ','','', 'モ','',''],
            'y':['ヤ','ャ','', '','','', 'ユ','ュ','', '','','', 'ヨ','ョ',''],
            'r':['ラ','','', 'リ','','', 'ル','','', 'レ','','', 'ロ','',''],
            'w':['ワ','ヮ','', 'ヲ','','', 'ン','','', '','','', '','',''],
            'sym':[' ','\n', '、', '。', '「', '」', '(', ')']
        }
    };

    // 数字・記号カテゴリ
    const numCategories = [
        {id:'digits', l:'123\n数字', src:"1234567890".split("")},
        {id:'math',   l:'+-*/\n式',   src:"+-*/=<>".split("")},
        {id:'point',  l:'.,:;\n点',   src:".,:;".split("")},
        {id:'bra',    l:'()[]\n括弧', src:"()[]{}''\"\"".split("")},
        {id:'unit',   l:'%$#\n単位',  src:"%$#&@".split("")},
        {id:'other',  l:'^|\n他',     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 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.onclick = null;
            }
            el.appendChild(btn);
        });
    }

    // --- 英字 UI ---
    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);
        });
    }

    // --- 数記 (半角/全角) カテゴリUI ---
    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);
        });
    }

    // --- 絵文字 UI ---
    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() {
        // 未確定spanを探す
        let span = inputEl.querySelector('.composing-span');
        if (!span) {
            // 現在のカーソル位置にspanを挿入
            span = document.createElement('span');
            span.className = 'composing-span';
            insertNodeAtCursor(span);
        }
        span.innerText = composingText;
        placeCaretAfter(span); // カーソルをspanの後ろではなく中に置くべきだが、今回は簡易的に表示更新
        // カーソルをspanの末尾に
        const range = document.createRange();
        range.selectNodeContents(span);
        range.collapse(false);
        const sel = window.getSelection();
        sel.removeAllRanges();
        sel.addRange(range);
        
        updateLineNum();
    }

    // 確定 (未確定spanをテキストノード化)
    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 = '<span style="color:#666;font-size:12px;">変換候補なし</span>';
        updateLineNum();
    }

    // 即時入力 (カーソル位置に挿入)
    function insertDirect(text) {
        // 未確定があれば先に確定
        if (composingText) confirmComposition();
        
        document.execCommand('insertText', false, text);
        updateLineNum();
    }

    // 削除 (未確定があればそれを、なければカーソル前を削除)
    function deleteChar() {
        if (composingText) {
            composingText = composingText.slice(0, -1);
            if (!composingText) {
                // span削除
                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 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 toFullWidth(str) {
        return str.replace(/[!-~]/g, function(s) {
            return String.fromCharCode(s.charCodeAt(0) + 0xFEE0);
        }).replace(/ /g, "\u3000"); // スペースも全角に
    }

    function updateLineNum() {
        // 現在の行数を計算 (簡易的)
        // 本来はキャレット位置の行を取得すべきだが、全体の行数で代用
        const text = inputEl.innerText;
        // 改行の数を数える
        // contenteditableの改行は <div><br></div> 等ブラウザ依存だが、
        // innerTextなら \n になることが多い
        const lines = text.split(/\n/).length;
        document.getElementById('ln-num-text').innerText = lines;
        
        // スクロール追従
        // 簡易実装: 入力時は常に一番下へ
        // inputEl.scrollTop = inputEl.scrollHeight; 
    }
    
    // キーボード入力の監視 (直接入力防止用だが、inputmode=noneで防げるはず)
    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